2021考研的第二章:线性表!!!冲冲冲(代码的大题,每一章更新完了统一更新)
文章目录
第二章 线性表
【考纲的内容】
(一)线性表的定义和基本操作
(二)线性表的实现
(三)顺序存储:链式存储;线性表的应用
【知识框架】
2.1 线性表的定义和基本操作
2.1.1 线性表的定义
线性表是具有相同数据类型的n(你>=0)个数据元素的有限序列,其中n为表长,当n=0时,线性表是一个空表。若用L命名线性表,则其一般
表示表示为:
L=(a1,a2,...,ai,a(i+1),...,an)
式中,a1是唯一的“第一个”数据元素,又称表头元素;an是唯一的“最后一个”数据元素,又称表尾元素。除第一个元素以外,每个元素有且仅
有一个直接前驱。除最后一个元素以外,每个元素有且仅有一个直接后驱。根据上述的定义中我们可以得出线性表的如下特点:
1.表中元素的个数有限
2.表中元素具有逻辑上的顺序性,表中元素有其先后次序
3.表中元素都是数据元素,每个元素都是单个元素
4.表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
5.表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容
注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念,因此不要将其混淆。
2.1.2 线性表的基本操作
一个数据结构的基本操作是指其最核心,最基本的操作。其他较复杂的操作可通过调用其基本操作来实现。线性表的主要操作如下:
InitList(&L):初始化表。构建一个空的线性表
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值
ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e
ListDelete(&L,i,&e):删除操作。输出表L中第i个位置的元素,并用e返回删除的元素
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值
Empty(L):判空操作。若L为空表,则返回true,否则返回false
DestroyList(&L):销毁操作。毁掉线性表,并释放线性表L所占用的内存空间
2.2 线性表的顺序表示
2.2.1 顺序表的定义
线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储子线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置
上也相邻。第一个元素存储在线性表的起始位置,第i个元素的存储位置后面紧接着存储的是第i+1个元素,称i为元素ai在线性表中的位序。
因为,顺序表的特点是表中元素的逻辑顺与其物理顺序相同。
假设线性表L存储的起始位置为LOC(A),sizeof(ElemType)是每个数据元素所占用存储空间的大小,则表L所对应的顺序如下图所示:
==注意:==线性表中的元素的位序是从一开始的,而数组中元素的下标谁从0开始的
假定线性表的元素类型为ElemType,则线性表的顺序存储类型描述为:
#define MaxSize 50 //定义线性表的最大长度
typedef struct{
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的长度
}SqList; //顺序表的类型定义
在上面的写法中是静态分配的,当然一堆数组也可以动态的进行分配。在静态分配时会产生一些问题,由于数组的大小和空间已经固定。一旦
空间占满,再加入新的元素就会产生移溢出,进而导致程序崩溃。
在动态分配中,存储数组的空间是在程序执行过程中通过动态分配语句分配的,一旦数据空间占满,就会另外的申请更大的空间。从而达到扩
充空间的目的,而不需要为线性表一次性地划分所有空间。
#define InitSize 100 //表长度的初始定义
typedef struct{
ElemType *data; //指针动态分配数组的指针
int MaxSize,length; //数组的最大容量和当前个数
}SqList; //动态分配数组顺序表的定义类型
C语言的动态分配语句为:
L.data=(ElemType *)malloc(sizeof(ElemType) *InitSize);
C++的初始动态分配语句为:
L.data=new ElemType[InitSize];
==注意:==动态分配并不是链式存储,他同样属于顺序存储结构,物理结构没有变化,依然是随机存取方式,只是分配的空间大小可以在运行时决定。
下面是总结下顺序结构的特点:
1.顺序结构最主要的特点是随机访问,及通过首地址和元素序号可在时间O(1)内找到指定的元素。
2.顺序表的存储密度高,每个节点只存储数据元素
3.顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素
2.2.2 顺序表上基本操作的实现
接下来分别给出插入、删除以及按值查找的算法
(1) 插入操作
在顺序表L的第i(1<=i<=L.length+1)个位置插入新元素额。若i的输入不合法,则返回false,表示插入失败;否则,将顺序表的第i个元素及后面的所有元素右移一个位置,腾出一个空位置插入新元素e,顺序表的长度+1,插入成功,返回true
bool ListInsert(SqList &L,int i,ELemType e){
if(i<1||i>L.length+1){
//判断i的范围是否有效
return false;
}
if(L.length>=MaSize){
//判断存储空间是否已满
return false;
}
for(int j=L.length;j>=i;j--){
//将的第i个元素及以后的元素后移
L.data[j]=L.data[j-1];
}
L.data[i-1]=e; //在位置i出放入e
L.length++; //线性表长度+1
return true;
}
==注意:==区别顺序表的位序和数组的下标。为何判断插入位置是否合法时if 语句中用length+1,而在移动元素的for语句中只用length?
答案:因为插入可以插入到顺序表的末尾,而整个顺序表中有length个元素,查到最后就是插入第length+1个位置,而在移动元素时,是判断的下标就不是第几个位置了,最后一个元素的下标就是length-1,将最后一个元素后移就是移动到下表为length的位置。大家理解了没?欢迎评论哦!
下面我们来分析下进行插入操作时的最好情况、最坏情况以及平均情况。
最好情况:在表尾插入,即i=length+1,元素后移语句将不执行,时间复杂度为O(1)。
最坏情况:在表头插入,即i=1,元素后移语句执行length次,时间复杂度为O(n)。
平均情况:假设pi(pi=1/(n+1))是在第i个位置上插入一个节点的概率,则在长度为n的线性表中插入一个节点时,所需移动节点的平均次数为:
因此,线性表插入算法的平均复杂度为O(n).
(2) 删除操作
删除顺序表中第i(1<=i<=L.length) 个位置的元素,若成功则返回true,并将被删除的元素用引用变量e返回,否则返回false。
bool ListInsert(SqList &L,int i,ELemType &e){
if(i<1||i>L.length){
//判断i的范围是否有效
return false;
}
e=L.data[i-1]; //将被删除的元素赋值给e
for(int j=i;j<L.length;j++){
//将的第i个元素后的元素前移
L.data[j-1]=L.data[j];
}
L.length11; //线性表长度-1
return true;
下面我们来分析下进行删除操作时的最好情况、最坏情况以及平均情况。
最好情况:删除表尾元素(即i=n),无需移动元素,时间复杂度为O(1)。
最坏情况:删除表头元素(即i=1),需移动第一个元素以外的所有元素,时间复杂度为O(n)。
平均情况:假设pi(pi=1/n)是删除第i个位置上节点的概率,则在长度为n的线性表中删除一个节点时,
所需移动节点的平均次数为:
因此,线性表删除算法的平均时间复杂度为O(n)。
下图为一个顺序表进行插入和删除操作前后的状态,以及其数据元素在存储空间中的位置变化和表长的变化。在a图中,将第4个至第7个元素
从后往前依次后移一个位置,在b图中,将第5个至第7个元素依次前移一个位置。
(3) 按值查找(顺序查找)
在顺序表L中查找第一个元素等于e的元素,并返回其位序。
int LocateElem(SqList L,ElemType e){
int i;
for(i=0;i<L.length;i++){
if(L.data[i]==e){
return i+1;
}
}
return 0;
}
下面我们来分析下进行按值查找操作时的最好情况、最坏情况以及平均情况。
最好情况:查找的元素就在表头,仅需比较一次,时间复杂度为O(1)。
最坏情况:查找的元素在表尾或者不存在,需要比较n次,时间复杂度为O(n)。
平均情况:假设pi(pi=1/n)是查找的元素在第i(i<=i<=L.length)个位置上的概率,则在长度为n的线性表中查找值为e的元素所需要比较的平均
次数为:
因此,线性表按值查找算法的平均时间复杂度为O(n).
2.2.3 本节习题(选择题我列举几题,综合应用题会全部写出来)
一、单项选择题
1.下列(A)是顺序结构的优点。
A.存储密度大 B.插入运算方便
C.删除运算方便 D.方便地运用于各种逻辑结构的存储表示
解析:B和C大家应该都明白吧,至于D选项比如树形结构就不适用于用顺序结构表示更加的适合用链式结构表示。
2.线性表的顺序结构是一种(A)。
A.随机存取的存储结构 B.顺序存取的存储结构
C.索引存取的存储结构 D.元素中各字段的类型
解析:存取是指读写的方式,我们在顺序表的使用过程中显然可以存取任意位置的元素,很多会误选B。
3.若线性表最常用的操作是存取第i个元素以及其前驱和后继元素的值,为了提高效率,应采用(D)的存储方式。
A.单链表 B.双向连表
C.但循环列表 D.顺序表
解析:A.B.C三个选项都只能从头结点依次寻找,时间复杂度为O(n),顺序表可以通过下标直接访问。
4.设线性表有n个元素,严格来说,以下操作中,©在顺序表上实现要比链表上实现的效率高。
Ⅰ.输出第i(1<=i<=n)个元素值
Ⅱ.交换第3个元素与第4个元素的值
Ⅲ.顺序输出这n个元素的值
A.Ⅰ B.Ⅰ、Ⅲ C.Ⅰ、Ⅱ D.Ⅱ、Ⅲ
解析:输出第i个元素顺序表可以直接通过下标访问进行输出,对于Ⅱ交换元素这一操作,在链表中要分别找到前驱节点,然后还要进行断链,再次连接起来,而顺序表可以直接进行交换。对于Ⅲ的顺序输出而言,都是一次访问所有的元素,并没有很大的区别。
二、综合应用题
1.从顺序表中删除具有最小值的元素(假设唯一)并且由函数返回被删元素的值。空出的位置有最后一个元素填补,若顺序表为空则显示出错误信息并退出运行。
算法思想:搜索整个顺序表,查找最小值元素并记住其位置,搜索结束后用最后一个元素填补出的原最小值的位置 。
代码如下:
bool Del_Min(spList &L,ElemType &value){
//删除顺序表L中最小值元素结点,并通过引用型参数value返回其值
//若删除成功,则返回true,否则返回false
if(L.length==0){
return 0; //表空,中止操作返回
}
value=L.data[0];
int pos=0; // 假定0号元素的值最小
for(int i=1;i<L.length;i++){
//循环.寻找具有最小值的元素
if(L.data[i]<value){
//让value记忆当前具有最小值的元素
value=L.data[i];
pos=i;
}
}
L.data[pos]=L.data[L.length-1]; //空出的位置由最后一个元素填补
return true; //此时,value的即为最小
}
2.设计一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1)
思想:扫描顺序表L的前半部分,对于元素L.data[i](o≤i<L.length/2),将其与后半部分的对应元素L.data[L.length-i-1]进行交换。
代码如下:
void Reverse(SqList &L){
Elemtype temp; //辅助变量
for(i=0;i<L.length/2;i++){
temp=L.data[i]; //交换L.data[i]与L.data[L.length-i-1]
L.data[i]=L.data[L.length-i-1];
L.data[L.length-i-1]=temp;
}
}
3.对长度为n的顺序表L,编写一个时间复杂度为O(n),空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据元素
思想:用k记录顺序表L中不等于x的元素个数(即需要保存的元素个数),边扫描L边统计K,并将不等于x的元素向前移动k个位置,最后修改L的长度。
代码如下:
void del_x_l(Sqlist &L,Elemtype x){
//本算法实现删除顺序表L中所有值为x的数据元素
int k=0; //记录值不等于x的元素个数
for(i=0;i<L.length;i++){
if( L.data[i]!=x){
L.data[k]=L.data[i];
k++; //不等于x的元素增1
}
}
L.length=k; //顺序表L的长度等于k
}
4.从有序顺序列表中删除其值在给定值s与t之间(要求s<t)的所有元素,如果s或t不合理或顺序表为空,则显示出错误并退出运行。
思想:先寻找出值大于等于s的第一个元素(第一个删除的元素),然后找到值待遇t的第一个元素(最后一个删除的元素的下一个元素),要将这段删除,只需直接将后面的元素前移。
代码如下:
bool Del_s_t(Sqlist &L,Elemtype s,Elemtype t){
//删除有序列表L中值在给定值s与t之间的所有元素
int i,j;
if(s≥t||L.length==0){
return false;
}
for(int i=0;i<L.length&&L.data[i]<s;i++)//寻找到大于等于s的第一个元素
if(i>=L.length){
return false; //所有元素都小于s,返回
}
for(int j=i;j<L.length&&L.data[j]<t;j++)//寻找值大于t的第一个元素
for(;j<L.length;i++;j++){
L.data[i]=L.data[j]; //前移,填补被删元素位置
}
L.length=i;
return true;
}
5.从顺序表中删除其值在给定s与t之间(包含s和t,要求s<t)的所有元素,如果s或t不合理或顺序表为空,则显示出错误信息并退出运行
思想:从前向后扫描顺序表L,用k记录下元素值在s到t之间元素的个数(初始时k=0).对于当前扫描的元素,若其值不在s到t之间,则前移k个位置;否则执行k++;由于这样每个不在s到t之间的元素仅移动一次,所以算法效率高。
代码如下:
bool Del_s_t(Sqlist &L,Elemtype s,Elemtype t){
//删除顺序表L中值在给定值s与t之间(要求s<t)的所有元素
int i,k=0;
if(L.length==0||s>=t){
return false; //线性表为空或s、t不合法,返回
}
for(i=0;i<L.length;i++){
if(L.data[i] >=s&&L.data[i]<=t){
k++;
} else{
L.data[i-k]=L.data[i];//当前元素前移k个位置
}
}
L.length-=k;//长度减小
return true;
}
6.从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同。
思想:注意是有序顺序表,值相同的元素一定在连续的位置上,用类型于直接插入排序的思想,初始时将第一个元素视为非重复的有序表。之后依
次判断后面的元素是否与前面非重复有序表的最后一个元素相同,若相同则继续向后判断,若不同则插入到前面的非重复有序表的最后,直至判断
到表尾为止
代码如下:
bool Delete_Same(SeqList &L){
if(L.length==0){
return false;
}
int i,j; //i存储第一个不相同的元素,j为工作指针
for(i=0,j=1;j<L.length;j++){
if(L.data[i]!=L.data[j]){
//查找下一个与上个元素值不同的元素
L.data[++i]=L.data[j]; //找到后,将元素前移
}
}
L.length=i+1;
return true;
}
7.将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表
思想:首先,按顺序不断取下两个顺序表表头较小 的结点存入新的顺序表中。然后,看哪个表还有剩余,将剩余的部分加到新的顺序表后面。
代码如下:
bool Merge(SeqList A,SeqList B,SeqList C){
//将有序顺序表A与B合并为一个新的有序顺序表C
if(A.length+B.length>C.length){
//大于顺序表的最大长度
return false;
}
int i=0,j=0,k=0;
while(i<A.length&&j<B.length){
//循环,两两比较,小者存入结果表
if(A.data[i]<=B.data[j]){
C.data[k++]=A.data[i++];
}else
C.data[k++]=A.data[j++];
}
while(i<A.length){
//还剩一个没有比较完的顺序表
C.data[k++]=A.data[i++];
}
while(j<B.length){
C.data[k++]=B.data[j++];
}
C.length=c;
return true;
}
8.已知在 一维数组A[m+n]中依次存放两个线性表(a1,a2…am)和(b1,b2…bn)。试编写一个函数,将数组中两个顺序表的位置互换,即将(b1,b2…bn)放在(a1,a2…am)的前面。
思想:先将数组A[m+n]中的全部元素(a1,a2,a3,...,am,b1,b2,b3,...bn)原地逆置(bn,bn-1,...b1,am,am-1,...a1),再对前n个元素和后m
个元素分别使用逆置算法,即可得到(b1,b2,...,bn,a1,a2,a3,...am),从而实现顺序表的位置互换
代码如下:
typedef int DataType;
void Reverse(DataType A[],int left,