第二章的主要学习内容是线性表;
在这里我想回顾从开学到现在所学的所有知识;
第一章 绪论
1、学习了一些基本的概念和术语
(1)数据、数据元素、数据项和数据对象
1)数据(Data):是客观事物的符号表示,是所有输入到计算机中的并被计算机程序处理的符号的总称;
如数学计算机中用到的整数和实数、多媒体程序处理的图形、图像、声音及动画等通过特殊编码定义后的数据;
2)数据元素(Data Element):是数据的基本单位,在计算机中通常作为一个整体进行考虑和处理。数据元素用于完整地描述一个对象。
如一名学生的记录、图中的一个格局;
3)数据项(Data Item):是组成数据元素的、有独立含义的、不可分割的最小单位。
例如,学生基本信息表中的学号、姓名、性别等都是数据项。
4)数据对象(Data Object) :是性质相同的数据元素的集合,是数据的一个子集。
如学生基本信息表也可以是一个数据对象。
2、学习了数据结构相关知识
(1)数据结构(Data Structure):是相互之间存在一种或多种特定关系的数据元素的集合。
通俗讲就是:数据结构是带“结构”的数据元素的集合,“结构”就是指数据元素之间存在的关系。
分类:
数据结构又分为:逻辑结构和存储结构两个层次;
1)逻辑结构有两个要素:一是数据元素,二是关系;
根据元素之间不同的特性,可分为四类
集合结构(离散关系)、线性结构(一对一关系)、树结构(一对多关系)、图结构(多对多关系)
形状大致如下(用电脑画图画的)画的不太标准;
线性结构:一对一关系;
图结构或网状结构:存在多对多关系;

2)存储结构
存储结构分为顺序存储结构和链式存储结构;
存储结构也称为物理结构
(1)顺序存储结构: 数据元素与数据元素之间的存储空间连续;
优点:随机存取(查询);
缺点:不方便插入、删除、扩容;
图如下:
(2)链式存储结构:数据元素与数据元素之间的存储空间可以不连续;
链式存储结构是顺序存取;
优点 :方便插入、删除、扩容;
缺点:不方便查询;
🔺每个结点占用两个连续的存储单元:一个存放结点的信息,另一个存放后继结点的首地址;每个结点附加一个“下一个结点地址”,即后继指针字段,用于存放后继结点的首地址;
图如下:
抽象数据类型(Abstract Data Type,ADT) 具体包括三个部分:数据对象、数据对象上关系的集合以及数据对象的基本操作的集合
3、学习了算法和算法分析。
(1)算法(Algorithm):是为了解决某类问题而规定的一个有限长的操作序列;
五个特性: 1)有穷性; 2)确定性; 3)可行性; 4)输入 ;5)输出;
(2)评价算法优劣的基本标准
1)正确性 2)可读性 3)健壮性 4)高效性;
🔺(3)算法的时间复杂度:执行算法所需的计算工作量; T(n)
1)影响算法时间代价的最主要因素是问题规模。
问题规模是求解问题的输入量的多少,是问题大小的本质表示,一般用整数n表示。
2)一个算法的执行时间大致等于其所有语句执行的时间的总和,而语句的执行时间则为该条语句的重复执行次数和执行一次所需时间的乘积;
3)算法的时间复杂定义
所谓“基本语句” 指的是算法中重复执行次数和算法的执行时间成正比的语句;它对算法运行时间的贡献最大;
🔺我们用“O”来表示数量级
一般情况下,算法中的基本语句重复执行的次数是问题规模n的某个函数f(n)的增长率相同,算法的时间量度记作 T(n) = O(f(n));
它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度(Time Complexity);
例 常量阶示例
{ x++;s = 0;}
两条语句频度均为1,算法的执行时间是一个与问题规模n无关的常数,所以算法的时间复杂度为T(n) = O(1),称为常量阶;
🔺实际上,如果算法的执行时间不随问题规模n的增加而增长,算法中的语句频度就是某个常数,即使这个常数再大,算法的时间复杂度都是O(1)。
例 for( i = 0 ; i < 1000 ;i++) { x++; s = 0 ;} 该算法的时间复杂度仍然为O(1);
例 线性阶示例
例 for ( i = 0 ; i < n ; i++) { x++; s = 0;} 循环体内两条基本语句的频度均为f(n) = n,所以算法的时间复杂度为T(n) = O(n),称为线性阶;
例 平方阶示例
例 x = 0 ; y = 0 ; ...(1)
for (k = 1 ; k <= n ;k++) ...(2)
x++; ...(3)
for(i = 1 ; i <= n;i++) ...(4)
for(j = 1;j <= n;j++) ...(5)
y++; ...(6)
以上程序段中频度最大的是语句(6),其频度为f(n) = n² ,所以该算法的时间复杂度为T(n) = O(n²),成为平方阶;
🔺多数情况下,当有若干个循环语句时,算法的时间复杂度是由最深层循环内的基本语句的频度f(n)决定的;
(4)最好、最坏和平均时间复杂度
1)最好时间复杂度:算法在最好情况下的时间复杂度,指的是算法计算量可能达到的最小值;
2)最坏时间复杂度:算法在最坏情况下的时间复杂度,指的是算法计算量可能达到的最大值;
3)平均时间复杂度:算法在所有可能的情况下,按输入实例以等概率出现时,算法计算量的加权平均值;
我们常常考虑的是最坏的时间复杂度。
假设我们以数组查询某个数为例;
例:对于数组查询,(假设数组有n个元素,我们要在数组中找一个数),
最好的情况下是:第一个便是我们要查找的,时间复杂度为O(1);
最坏的情况下是:找到最尾端,第n个值才是我们要查找的,时间复杂度为O(n);也可能找到末端都找不到这个数,也就是数组中没有这个数;
平均时间复杂度:O((1+n)/2)≈ O(n/2),
(5)算法的空间复杂度
1)类似于算法的时间复杂度,我们采用渐进空间复杂度作为算法所需存储空间的量度,简称为空间复杂度,它也是问题规模n的函数,记作 : S(n) = O(f(n));
2)若算法执行时所需要的辅助空间相对于输入数据量而言是个常数,则称这个算法为原地工作,辅助空间为O(1);
下面以数组逆序为例;例 1 例2
1 for( i = 0 ; i < n/2 ;i++) for( i = 0 ; i < n ;i++) 2 { t = a[i]; { b[i] = a[n-i-1]; 3 a[i] = a[n-i-1]; for( i = 0 ; i < n ;i++); 4 a[n-i-1] = t; a[i] = b [i]; 5 } }
例1 仅需要另外借助一个变量t ,与问题规模n大小无关,所以其空间复杂度为O(1);
例2 需要另外借助一个大小为n的辅助数组b,其空间复杂度为O(n);
总结:当追求一个较好的时间复杂度时,可能会导致占用较多的存储空间,即可能使空间复杂度性能变差,反之亦然;
不过通常情况下,鉴于运算空间较为充足,人们都以算法的时间复杂度作为优劣的衡量指标。
第二章 线性表
1、学习了线性表的定义和特点
线性表可用顺序表和链表表示;
(1)线性表:由n(n>=0)个数据特性相同的元素构成的有限序列;
称作顺序存储结构或顺序映像; 像这种存储结构的线性表称为顺序表(Sequential List)
则存储位置的表示有下列关系 : LOC(a(i+1)) = LOC(a(i) + L;
🔺只要确定了存储线性表的起始位置,线性表中的任一数据元素都可以随机存取;所以线性表的顺序存储结构是一种随机存取的存储结构。
假设元素的类型为int型
1 #define max 100 2 typedef struct 3 { 4 int *elem; //存储空间的基地址; 5 int length //当前长度; 6 }sqList; //为这个结构题起了个类型名叫做SqList;
上述代码也可以写成
1 #define max 100 2 struct sqList 3 { 4 int *elem; //存储空间的基地址; 5 int length //当前长度; 6 };
更简便;
🔺假设以数组为例,这里要特别注意数据过大的情况;
解决方案:1)定义为全局变量; 2)动态申请; int *a = new int[ ]; 注意如果动态申请空间,最后一定要delete,如果不delete,则称为内存泄漏。
3、学习了顺序表中基本操作的实现;
1)初始化;//构造一个空的顺序表
1 2 #define max 100 3 4 struct SqList 5 6 { 7 8 int *elem; //存储空间的基地址; 9 10 int length //当前长度; 11 12 }; 13 SqList L; 14 bool InitList (SqList L) 15 { 16 L.elem = new int [max]; //为顺序表分配一个大小为max的数组空间; 17 if(L.elem==NULL) exit(0); //存储分配失败退出 18 L.length = 0; //初始化表的长度为0;即为空表; 19 return true; 20 }
动态分配线性表的存取区域可以更有效的利用系统资源,当不需要该线性表的时,可以使用delete操作及时释放占用的存储空间。
2)取值 假设取第i个元素;
我们以下标从0 开始,一共n个元素;
1 bool getElem(SqList L, int i ; int tmp) 2 { 3 if( i < 0 || i >= L.length) //判断 i 的值是否合理 ,如果 i < 0 或者 i >= L.length 则越界了。 4 return false; 5 tmp = L.elem[ i - 1]; //取第 i 个元素,下标时 i - 1; 6 return true; 7 }
3)查找 假设元素为int 类型
1 int chazhao (SqList L, int tmp) 2 { 3 for( i = 0 ; i < L.length ;i++) 4 if(L.elem[i] == tmp) return i + 1; 5 else return -1; 6 }
分析:从第一个元素开始,依次与tmp比较,若找到与tmp 相等的元素L.elem[i],则查找成功,返回该元素的序号 i+1;
若查遍整个顺序表都没有找到,则查找失败,返回 -1;
🔺算法分析:当在顺序表中查找一个元素时,其时间主要耗费在数据的比较上,而比较的次数取决于被查元素在线性表中的位置;
这可以前面那个时间复杂度的分析联系在一起;
4)插入 假设我们在下标为i的位置插入新的元素tmp;
1 bool LinstInsert (SqList L, int i ,int tmp) 2 { 3 if( i < 0|| i >= L.length) 4 return false; //插入新元素的合理位置是从 i = 0 到 i = L.length - 1 之间,因为我们的下标是从 i= 0 开始; 5 if(L.length == max) 6 return false; //如果顺序表的长度已满,则不能再插入;这个条件要写在插入之前,因为插入后L.length+1;但是若本身表已满,我们还插入,就错误了; 7 for( int j = L.length - 1 ; j >= i; j--) 8 L.elem [ j + 1 ] = L.elem [ j ]; // 插入位置及之后的元素都要后移; 9 L.elem[i] = tmp; 10 L.length++; //添加一个元素,表长+1; 11 return true; 12 }
🔺算法分析:当顺序表中某个位置上插入一个数据元素时,其时间主要耗费在移动元素上;而移动元素的个数取决于插入元素的位置;
与前面的时间复杂度的分析联系在一起;
5)删除 在下标为i的位置删除一个元素
1 bool ListDelete (SqList L,int i) 2 { 3 if( i < 0 || i >= L.length) //删除元素的合理位置是从 i = 0 到 i = L.length - 1 之间,因为我们的下标是从 i= 0 开始; 4 return false; 5 for( int j = i ; j < L.length ;j++) 6 { 7 L.elem[ j - 1 ] = L.elem [ j ]; 8 } 9 L.length --; 10 return true; 11 }
4、学习了线性表的链式表示和实现
1)特点:逻辑上相邻,物理存储结构可以不相邻;
2)除了存储本身的信息外,还需要存储一个指示其直接后继的信息 ,示意图大致如下:
这两部分信息组称数据元素ai的存储映像,称为结点(node)。它包括两个域:
(1)存储数据元素的域称为数据域 ; (2)存储直接后继存储位置的域称为指针域; 指针域中存储的信息称作指针或链。
n个结点链结成一个链表,即为线性表;
由于此链表的每个结点只包含一个指针域,故又称为线性链表或单链表;
单链表中的最后一个结点的指针为空(NULL);
(2)单链表可由头指针唯一确定; 由下图可看出; 下图是只有头指针没有头结点的示意图;
单链表的存储结构: 这里假设元素数据类型为 int 类型;
1 typedef struct LNode 2 { 3 int data ; //这个结点的数据; 4 struct LNode *next; //这个结点的指针域 ,用next表示 5 }LNode,*LinkList; //给LNode起了两个别名,叫做LNode 和LinkList;
实际上我们写下面那种方式更加简便 直接写个结构体,不用起别名;
1 struct LNode 2 { 3 int data; //这个结点的数据; 4 struct LNode *next; //这个结点的指针域 ,用next表示 5 }
一般情况下,为了处理方便,在单链表的第一个结点之前附设一个结点,称之为头结点 ; 示意图如下:
链表增加头结点的作用:
1)便于首元结点的处理
增加了头结点后,首元结点的地址保存在头结点 (即其”前驱“结点)的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理;
2)便于空表和非空表的统一处理
(1)当链表不设头结点,假设L为单链表的头指针,它应该指向首元结点,则当单链表为长度n为0的空表时,L指针为空(判定空表的条件可记为:L == NULL);这里的L是头指针;
不设头结点的非空表;
不设头结点的空表;
(2)增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。
如下图 ,非空单链表,头指针指向头结点。 空表,则头结点的指针域为空,(判定空表的条件可记为:L->next == NULL);
设头结点的非空表;
设头结点的空表;
5、学习了单链表基本操作的实现 下面都是以带头结点来实现;
(1)初始化
1 struct LNode 2 { 3 int data; //这个结点的数据; 4 struct LNode *next; //这个结点的指针域 ,用next表示 5 } 6 7 LNode *L ; //定义一个类型为LNode 的头指针L; 8 bool InitList (LNode *L) //传入头指针 9 { 10 L = new Node; //为头指针开辟一个存储空间; 11 L-> next = NULL; //头结点的指针域为空,构造一个空的单链表 12 return true; 13 }
可看下面的图方便理解;
2)取值
1 bool GetElem( LNode *L ; int i ; int tmp) // 传入三个参数,第一个是L头指针,第二个是取第 i 个元素 , 第三个是传入tmp ,方便后面将第i个元素赋值给tmp 2 { 3 LNode *p ; //定义一个移动的指针; 4 p = L ->next ; // 初始化时, 将L->next赋值给p,使 p 指向首元结点; 可见下图; 5 int j = 1; //将 j 初值赋为1 ;方面和 计算与 i 的关系; 6 while( p ! = NULL&& j <i ) //顺链域向后扫描,直到p为空或者p指向第i个元素; 7 { 8 p = p -> next; //p指向下一个结点; 9 j++; // 计数器 j 相应加1; 10 } 11 if( p == NULL || j > i) //当上面的while循环结束后 p 已为空或者 j 已经大于 i 了 则找不到第 i 个元素; 取值失败; 12 return false; 13 tmp = p -> data; //找到第 i 个元素, 将其 赋值给tmp ; 14 return true; 15 }
🔺 算法分析:该算法的基本操作是比较 j 和 i 并 后移指针 p 。while 循环体中的语句频度与位置 i 有关。 若 1 <= i <= n;则频度为 i - 1,一定能取值成功;
若 i > n ,则频度为 n ,取值失败,因此算法的时间复杂度为 O(n);
3)查找 下面代码返回的是一个指针
1 LNode *LocateElem (LNode *L , int tmp ) //传入头指针和要查找的值; 2 { 3 p = L -> next; //初始化,p指向首元结点,与上面取值是类似的; 4 while( p != NULL && p -> data != tmp) //顺链表向后扫描,直到p为空或者p所指结点的数据域等于tmp; 5 { 6 p = p -> next; //p指向下一个; 7 } 8 return p; //查找成功返回数值为tmp的结点地址p; 查找失败则返回NULL; 9 }
🔺算法分析:该算法的执行时间与待查找的tmp值由关,其时间复杂度也为O(n)
4)插入
1 bool ListInsert (LNode *L; int i ; int tmp) //传入头指针, 传入i,指在第i个插入,传入插入的值tmp; 2 { 3 LNode *p ; //定义一个可移动的指针; 4 LNode *s; //定义一个指向tmp的指针; 5 p = L; //将p指向头指针; 6 int j = 1; //将 j 初值赋值为1 ;起一个计数器的作用; 7 while(p != NULL && j < i-1) 8 { 9 p = p ->next; //p指针顺着链表不断后移; 10 j++; // j 的记数相应+1; 11 } 12 if( p == NULL || j > i-1) //如果找到最后找不到,p指针指向空 或者 j 已经大于 i-1 了 ,则插入失败; 13 return false; 14 //若没有进入上面那个if 中,则可插入 ,则 15 s = new LNode; //为新元素开辟一个空间; 16 s ->data = tmp; //将tmp 的指赋给该结点的数据域; 17 s -> next = p -> next; //在i - 1 和 i 之间插入, 则将指针指向换一个即可;下面会有图解释这里; 18 p -> next = s; 19 return true; //插入成功,返回true; 20 } 21
插入前:
插入后:
🔺算法分析:单链表的插入操作虽然不需要像顺序表那样插入后要移动,但是时间复杂度仍为O(n);
因为为了在第 i 个结点之前插入一个新结点 ,必须先找到 i - 1 个结点;
5)删除
1 bool ListDelete(LNode *L , int i ) //传入两个参数,一个是头指针,一个是在第 i 个位置删除的 i; 2 { 3 LNode *p ; //定义一个可移动的指针; 4 int j = 1; //将 j 的初值赋值为1 ; 5 LNode * tmp; //定义一个暂时的中间指针; 6 while( p ! = NULL && j < i ) 7 { 8 p = p -> next ; //p 顺着链表向后移动; 9 j++; //计数器 j 相应加1; 10 } 11 if( p == NULL || j > i) //若p找到最后都指向空了还找不到 i 或者 j > i 了,则删除失败; 12 return false; 13 tmp = p ->next; 14 p->next = tmp ->next; 15 16 delete tmp; 17 return true; 18 }
删除前:
删除后:
6)创建单链表 前插法和后插法;
(1)前插法:
1 void CreatList (LNode *L ,int n) 2 { 3 LNode *p; //先定义一个可移动的指针; 4 L = new LNode; 5 L -> next = NULL; //先建立一个带头结点的空链表; 6 for(int i = 0 ; i < n ; i++) 7 { 8 p = new LNode; //为新结点开辟一个空间; 9 cin>>p -> data; //输入新结点的数据域; 10 p ->next = L ->next ; // 把L的下一个赋给 p的下一个; 11 L - >next = p; // 把p赋给 L 的下一个; 12 } 13 }
🔺算法分析:时间复杂度为O(n);
(2)后插法:
1 void CreatList(LNode *L ,int n ); //传入头指针 ,和长度; 2 { 3 LNode *tail; //实际上这是个尾指针; 4 LNode *tmp //定义一个暂时中间的指针; 5 L = new LNode; 6 L->next = NULL; 7 tail = L; //初始化,尾指针和头指针指向相同; 8 for(int i = 0 ; i < n ; i ++) 9 { 10 tmp = new LNode ; //每新插入一个结点,为它开辟一个空间; 11 cin>>tmp->data; //输入新结点的数据域的数值; 12 tmp ->next = NULL; //尾插,最后tmp的next是为空的; 13 tail ->next = tmp; //tail 指向新的尾结点tmp; 14 } 15 }
🔺算法分析:时间复杂度为O(n);
除此之外还简单学习了循环链表和双向链表,理解较粗浅,就不在这里写了;
6、作业:pintia上第二章的作业题是建立顺序表和链表,具体详细的知识在上面都有;
7、双向链表的建立。这里先不展开,以后有精力再写有关内容。有同学写解释的挺好的,所以这里就先不写出来了。
8、作业:pintia上的实践题;
题目描述如下:
7-1 还是求集合交集 (30 分)
给定两个整数集合(每个集合中没有重复元素),集合元素个数<=100000,求两集合交集,并按非降序输出。
输入格式:
第一行是n和m,表示两个集合的元素个数; 接下来是n个数和m个数。
输出格式:
第一行输出交集元素个数; 第二行按非降序输出交集元素,元素之间以空格分隔,最后一个元素后面没有空格。
输入样例:
在这里给出一组输入。例如:
5 6
8 6 0 3 1
1 8 9 0 4 5
输出样例:
在这里给出相应的输出。例如:
3
0 1 8
解题思路:这里不能暴力的就排序找相同,因为题目给的数据比较大,会超时;
而且如果写选择排序或者冒泡排序的话,两个for循环时间复杂度上到n²去了,也就是10000000000;而题目给的是0.4s 肯定超时;
解法一:
排序的话可以之间用sort 时间复杂度是logn;
然后比较的话,可先将两个集合先排序再比较,优化了算法;

代码如下:
1 #include<iostream> 2 #include<algorithm> 3 using namespace std; 4 5 6 7 int a[200000]; 8 int b[200000]; 9 int c[200000]; 10 int count1 = 0; 11 int main() 12 { 13 int n , m ; 14 cin>>n>>m; 15 for(int i = 0 ; i < n ;i++) 16 { 17 cin>>a[i]; //输入a集合 18 } 19 for(int j = 0 ; j < m; j++) 20 { 21 cin>>b[j]; //输入b集合 22 } 23 sort(a,a+n); //对于a集合排序 ,sort 头文件为algorithm 24 sort(b,b+m); //对于b集合排序 25 //对两个集合排序是为了后面方便比较 26 int i = 0 ; 27 int j = 0; 28 while(i < n && j < m) 29 { 30 if(a[i]<b[j]) 31 { 32 i++; //一对一进行比较,直到找到相同 33 }else 34 if(b[j]<a[i]) 35 { 36 j++; //同理,一对一进行比较,直到找到相同 37 } 38 else 39 if(a[i]==b[j]) 40 { 41 c[count1] = a[i];//如果两个均相同,则两个均向前移动,c集合可将a集合对应的数放进去,也可将b集合对应的数放进去 42 count1++; 43 i++; //a集合对应的向前移动 44 j++; //b集合对应的向前移动 45 } 46 } 47 cout<<count1<<endl; 48 for(int i = 0 ; i < count1 ;i++ ) 49 { 50 cout<<c[i]; //输出c集合 51 if(i!=count1-1) 52 cout<<" "; //注意规范 53 } 54 return 0; 55 }
解法二:在输入a集合的同时将a中的元素标记,在输入b集合的同时看是否有与a集合相同的元素,有的话就存到c集合中去,这里不能 用单纯的int 数组标记,因为会数字太大,会爆掉;所以可用map来标记;、
1 #include<iostream> 2 #include<algorithm> 3 #include<map> 4 using namespace std; 5 6 map<int,int>vis; //利用map数组;头文件为map 7 int a[200000]; 8 int b[200000]; 9 int c[200000]; 10 int count1 = 0; 11 int main() 12 { 13 14 int n ,m ; 15 cin>>n>>m; 16 for(int i = 0 ;i < n ; i++) 17 { 18 cin>>a[i]; 19 vis[a[i]] = 1; //标记a集合中的元素; 20 21 } 22 for(int i = 0 ; i < m ;i++) 23 { 24 cin>>b[i]; 25 if(vis[b[i]]==1) //如果b集合中有与a集合相同的元素 ,则存到c集合中; 26 { 27 c[count1] = b[i]; 28 count1++; 29 } 30 } 31 32 33 sort(c,c+count1); //排序; 34 cout<<count1<<endl; 35 for(int i = 0 ; i < count1 ;i++) 36 { 37 cout<<c[i]; 38 if(i!=count1-1) 39 { 40 printf(" "); 41 }else cout<<endl; 42 43 } 44 }
虽然总的这篇回顾写了10多个小时,但是觉得还是有所收获!
这段时间的不足之处:对链表的掌握程度一般,不是那么熟练;
下一章的学习:在熟练掌握链表的同时,将栈与队列熟练掌握,灵活运用。