第2章线性表.docx
- 文档编号:6829585
- 上传时间:2023-01-10
- 格式:DOCX
- 页数:36
- 大小:182.39KB
第2章线性表.docx
《第2章线性表.docx》由会员分享,可在线阅读,更多相关《第2章线性表.docx(36页珍藏版)》请在冰豆网上搜索。
第2章线性表
第2章线性表
线性表是最简单、最基本、也是最常用的一种线性结构。
它有两种存储方法:
顺序存储和链式存储,它的主要基本操作是插入、删除和检索等。
2.1线性表的逻辑结构
2.1.1线性表的定义
线性表是一种线性结构。
线性结构的特点是数据元素之间是一种线性关系,数据元素“一个接一个的排列”。
在一个线性表中数据元素的类型是相同的,或者说线性表是由同一类型的数据元素构成的线性结构。
在实际问题中线性表的例子是很多的,如学生情况信息表是一个线性表:
表中数据元素的类型为学生类型;一个字符串也是一个线性表:
表中数据元素的类型为字符型,等等。
综上所述,线性表定义如下:
线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,通常记为:
(a1,a2,…ai-1,ai,ai+1,…an)(2-1)
其中n为表长,n=0时称为空表。
表中相邻元素之间存在着顺序关系。
将ai-1称为ai的直接前驱,ai+1称为ai的直接后继。
就是说:
对于ai,当i=2,...,n时,有且仅有一个直接前驱ai-1.,当i=1,2,...,n-1时,有且仅有一个直接后继ai+1,而a1是表中第一个元素,它没有前驱,an是最后一个元素,它没有后继。
需要说明的是:
ai为序号为i的数据元素(i=1,2,…,n),通常我们将它的数据类型抽象为DataType,DataType根据具体问题而定,如在学生情况信息表中,它是用户自定义的学生类型;在字符串中,它是字符型;等等。
2.1.2线性表的基本操作
在第一章中我们已知道,数据结构的运算是定义在逻辑结构层次上的,而运算的具体实现是建立在存储结构上的,因此下面定义的线性表的基本运算作为逻辑结构的一部分,每一个操作的具体实现只有在确定了线性表的存储结构之后才能完成。
线性表上的基本操作有:
⑴Init_List(L):
线性表初始化
操作结果是构造一个空的线性表
⑵Length_List(L):
求线性表的长度
操作结果是返回线性表中所含元素的个数
⑶Get_List(L,i):
取表元
表L存在且1≤i≤Length_List(L),操作结果是返回线性表L中的第i个元素的值或地址
⑷Locate_List(L,x):
按值查找,x是给定的一个数据元素。
线性表L存在,操作结果是在表L中查找值为x的数据元素,其结果返回在L中首次出现的值为x的那个元素的序号或地址,称为查找成功;否则,在L中未找到值为x的数据元素,返回一特殊值表示查找失败。
⑸Insert_List(L,i,x):
插入操作
线性表L存在,插入位置1≤i≤n+1(n为插入前的表长),操作结果是在线性表L的第i个位置上插入一个值为x的新元素,这样使原序号为i,i+1,...,n的数据元素的序号变为i+1,i+2,...,n+1,插入后表长=原表长+1。
⑹Delete_List(L,i):
删除操作
线性表L存在,删除位置1≤i≤n(n为删除前的表长),操作结果是在线性表L中删除序号为i的数据元素,删除后使序号为i+1,i+2,...,n的元素变为序号为i,i+1,...,n-1,新表长=原表长-1。
需要说明的是:
⑴某数据结构上的基本运算,不是它的全部运算,而是一些常用的基本的运算,而每一个基本运算在实现时也可能根据不同的存储结构派生出一系列相关的运算来。
比如线性表的删除运算还会有删除某个特定值的元素;再如插入运算,也可能是将新元素x插入到适当位置上等等,不可能也没有必要定义出它的全部运算,读者掌握了某一数据结构上的基本运算后,其它的运算可以通过基本运算来实现,也可以直接去实现。
⑵在上面各操作中定义的线性表L仅仅是一个抽象在逻辑结构层次的线性表,尚未涉及到它的存储结构,因此每个操作在逻辑结构层次上尚不能用具体的某种程序设计语言写出具体的算法,而算法只有在存储结构确立之后才能实现。
2.2线性表的顺序存储及运算实现
2.2.1顺序表
线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素,用这种存储形式存储的线性表称为顺序表。
因为内存中的地址空间是线性的,因此,用物理上的相邻实现数据元素之间的逻辑相邻关系是既简单,又自然的。
如图2-1所示。
设a1的存储地址为Loc(a1),每个数据元素占d个存储地址,则第i个数据元素的地址为:
Loc(ai)=Loc(a1)+(i-1)*d1≤i≤n
这就是说只要知道顺序表首地址和每个数据元素所占地址单元的个数就可求出第i个数据元素的地址来,这也是顺序表具有按数据元素的序号随机存取的特点。
在程序设计语言中,一维数组在内存中占用的存储空间就是一组连续的存储区域,因此,用一维数组来表示顺序表的数据存储区域是再合适不过的。
考虑到线性表的运算有插入、删除等运算,即表长是可变的,因此,数组的容量需设计的足够大,设用:
data[MAXSIZE]来表示,其中MAXSIZE是一个根据实际问题定义的足够大的整数,线性表中的数据从data[0]开始依次顺序存放,但当前线性表中的实际元素个数可能未达到MAXSIZE多个,因此需用一个变量last记录当前线性表中最后一个元素在数组中的位置,即last起一个指针的作用,始终指向线性表中最后一个元素,因此,表空时last=-1。
这种存储思想的具体描述可以是多样的。
如可以是:
DataTypedata[MAXSIZE];
intlast;
这样表示的顺序表如图2-1所示。
表长为last+1,第一个到第n个数据元素分别存放在data[0]到data[last]中。
这样使用简单方便,但有时不便管理。
0
1
…
i-1
i
…
n-1
…MAXSIZE1-1
Data
a1
a2
…
ai-1
ai
ai+1
…
an
…
图2-1线性表的顺序存储
从结构性上考虑,通常将data和last封装成一个结构作为顺序表的类型:
typedefstruct{
DataTypedata[MAXSIZE];
intlast;
}SeqList;
SeqListL;//定义一个顺序表
这样表示的线性表如图2-2(a)所示。
表长=L.last+1,线性表中的数据元素a1至an分别存放在L.data[0]至L.data[L.last]中。
由于我们选用C语言表述数据结构的存储及算法的实现,根据C语言中的一些规则,有时定义一个指向SeqList类型的指针更为方便:
SeqList*L;
L是一个指针变量,线性表的存储空间通过L=newSeqList操作(VC语句,不同的语言版本可能有所不同)来获得。
L中存放的是顺序表的地址,这样表示的线性表如图2-2(b)所示。
表长表示为(*L).last+1或L->last+1,线性表的存储区域为L->data,线性表中数据元素的存储空间为:
L->data[0]~L->data[L->last]。
在以后的算法中多用这种方法表示,读者在读算法时注意相关数据结构的类型说明。
2.2.2顺序表上基本运算的实现
⒈顺序表的初始化
顺序表的初始化即构造一个空表,这对表是一个加工型的运算。
因此,将L设为指针参数,首先动态分配存储空间,然后,将表中last指针置为-1,表示表中没有数据元素。
算法如下:
SeqList*init_SeqList(){
SeqList*L;
L=newSeqList;
L->last=-1;
returnL;
}
【算法2-1】顺序表的初始化算法
设调用函数为主函数,主函数对初始化函数的调用如下:
main(){
SeqList*L;
L=Init_SeqList();
...
}
⒉插入运算
线性表的插入是指在表的第i个位置上插入一个值为x的新元素,插入后使原表长为n的表:
(a1,a2,...,ai-1,ai,ai+1,...,an)
成为表长为n+1表:
(a1,a2,...,ai-1,x,ai,ai+1,...,an )。
i的取值范围为1≤i≤n+1。
顺序表上完成这一运算则通过以下步骤进行:
⑴将ai~an顺序向下移动,为新元素让出位置;
⑵将x置入空出的第i个位置;
⑶修改last指针(相当于修改表长),使之仍指向最后一个元素。
算法如下:
intInsert_SeqList(SeqList*L,inti,DataTypex){
intj;
if(L->last==MAXSIZE-1)
{cout<<”表满”< return-1; }//表空间已满,不能插入 if(i<1||i>L->last+2) //检查插入位置的正确性 {cout<<”位置错”; return(0); } for(j=L->last;j>=i-1;j--) L->data[j+1]=L->data[j];//结点移动 L->data[i-1]=x; //新元素插入 L->last++; //last仍指向最后元素 return1; //插入成功,返回 } 【算法2-2】顺序表的插入算法 性能分析: 顺序表上的插入运算,时间主要消耗在了数据的移动上,在第i个位置上插入x,从ai到an都要向下移动一个位置,共需要移动n-i+1个元素,而i的取值范围为: 1≤i≤n+1,即有n+1个位置可以插入。 设在第i个位置上作插入的概率为Pi,则平均移动数据元素的次数: (2-2) 设: Pi=1/(n+1),即为等概率情况,则: (2-3) 这说明: 在顺序表上做插入操作需移动表中一半的数据元素。 显然时间复杂度为O(n)。 a1 a2 … ai-1 ai ai+1 … an … a1 a2 … ai-1 x ai ai+1 … an … 0 1 i-2 i-1 i n-1 MAXSIZE-1 插入前插入后 图2-3顺序表中的插入 本算法中注意以下问题: ⑴顺序表中数据区域有MAXSIZE个存储单元,所以在向顺序表中做插入时先检查表空间是否满了,在表满的情况下不能再做插入,否则产生溢出错误。 ⑵要检验插入位置的有效性,这里i的有效范围是: 1≤i≤n+1,其中n为原表长。 ⑶注意数据的移动方向。 ⒊删除运算DeleteList(L,i) 线性表的删除运算是指将表中第i个元素从线性表中去掉,删除后使原表长为n的线性表: (a1,a2,...,ai-1,ai,ai+1,...,an) 成为表长为n-1的线性表: (a1,a2,...,ai-1,ai+1,...,an)。 i的取值范围为: 1≤i≤n。 顺序表上完成这一运算的步骤如下: ⑴将ai+1~an顺序向上移动。 ⑵修改last指针(相当于修改表长)使之仍指向最后一个元素。 a1 a2 … ai-1 ai+1 an … 0 a1 1 a2 … i-2 ai-1 i-1 ai i ai+1 ... n-1 an … MAXSIZE-1 删除前删除后 图2-4顺序表中的删除 算法如下: intDelete_SeqList(SeqList*L,inti){ intj; if(i<1||i>L->last+1)//检查空表及删除位置的合法性 {cout<<”不存在第i个元素”< return0; } for(j=i;j<=L->last;j++) L->data[j-1]=L->data[j];//向上移动 L->last--; return1;//删除成功 } 【算法2-3】顺序表的删除算法 性能分析: 与插入运算相同,其时间主要消耗在了移动表中元素上,删除第i个元素时,其后面的元素ai+1~an都要向上移动一个位置,共移动了n-i个元素,所以平均移动数据元素的次数: (2-4) 在等概率情况下,pi=1/n,则: (2-5) 这说明顺序表上作删除运算时大约需要移动表中一半的元素,显然该算法的时间复杂度为O(n)。 本算法注意以下问题: ⑴删除第i个元素,i的取值为1≤i≤n,否则第i个元素不存在,因此,要检查删除位置的有效性。 ⑵当表空时不能做删除,因表空时L->last的值为-1,条件(i<1||i>L->last+1)也包括了对表空的检查。 ⑶删除ai之后,该数据已不存在,如果需要,先取出ai,再做删除。 ⒋按值查找 线性表中的按值查找是指在线性表中查找与给定值x相等的数据元素。 在顺序表中完成该运算最简单的方法是: 从第一个元素a1起依次和x比较,直到找到一个与x相等的数据元素,则返回它在顺序表中的存储下标或序号(二者差一);或者查遍整个表都没有找到与x相等的元素,返回-1。 算法如下: intLocation_SeqList(SeqList*L,DataTypex){ inti; i=0; while(i<=L->last&&L->data[i]! =x) i++; if(i>L->last) return-1; else returni;//返回的是存储位置 } 【算法2-4】顺序表的按值查找算法 本算法的主要运算是比较。 显然比较的次数与x在表中的位置有关,也与表长有关。 当a1=x时,比较一次成功。 当an=x时比较n次成功。 平均比较次数为(n+1/2,时间性能为O(n)。 2.2.3顺序表应用举例 【例2-1】将顺序表(a1,a2,...,an)重新排列为以a1为界的两部分: a1前面的值均比a1小,a1后面的值都比a1大(这里假设数据元素的类型具有可比性,不妨设为整型),操作前后如图2-5所示,这一操作称为划分,a1也称为基准。 划分的方法有多种,下面介绍的划分算法其思路简单,但性能较差。 基本思路: 从第二个元素开始到最后一个元素,逐一向后扫描: ⑴当前数据元素ai比a1大时,表明它已经在a1的后面,不必改变它与a1之间的位置,继续比较下一个。 ⑵当前结点若比a1小,说明它应该在a1的前面,此时将它上面的元素都依次向下移动一个位置,然后将它置入最上方。 25 30 20 60 10 35 15 . . . 15 10 20 25 30 60 35 . . . 划分前划分后 图2-5顺序表的划分 根据上述思想,划分算法如下: voidpart(SeqList*L){ inti,j; DataTypex,y; x=L->data[0];//将基准置入x中 for(i=1;i<=L->last;i++) if(L->data[i] {y=L->data[i]; for(j=i-1;j>=0;j--)//移动 L->data[j+1]=L->data[j]; L->data[0]=y; } } 【算法2-5】顺序表划分算法 本算法中,有两重循环,外循环执行n-1次,内循环中移动元素的次数与当前数据的大小有关,当第i个元素小于a1时,要移动它上面的i-1个元素,再加上当前结点的保存及置入,所以移动i-1+2次,在最坏情况下,a1后面的结点都小于a1,故总的移动次数为: (2-6) 即最坏情况下移动数据时间性能为O(n2)。 这个算法简单但效率低,在第10章的快速排序中时我们将介绍另一种划分算法,它的性能为O(n)。 【例2-2】有顺序表A和B,其元素均按从小到大的升序排列,编写一个算法将它们合并成一个顺序表C,要求C的元素也是从小到大的升序排列。 算法思路: 依次扫描A和B的元素,比较当前的元素的值,将较小值的元素赋给C,如此直到一个线性表扫描完毕,然后将未完的那个顺序表中余下部分赋给C即可。 C的容量要能够容纳A、B两个线性表相加的长度。 算法如下: voidmerge(SeqListA,SeqListB,SeqList*C){ inti,j,k; i=0; j=0; k=0; while(i<=A.last&&j<=B.last) if(A.data[i] C->data[k++]=A.data[i++]; else C->data[k++]=B.data[j++]; while(i<=A.last) C->data[k++]=A.data[i++]; while(j<=B.last) C->data[k++]=B.data[j++]; C->last=k-1; } 【算法2-6】有序表的合并算法 算法的时间性能是O(m+n),其中m是A的表长,n是B的表长。 【例2-3】比较两个线性表的大小。 两个线性表的比较依据下列方法: 设A、B是两个线性表,均用向量表示,表长分别为m和n。 A’和B’分别为A和B中除去最大共同前缀后的子表。 例如A=(x,y,y,z,x,z),B=(x,y,y,z,y,x,x,z),两表最大共同前缀为(x,y,y,z)。 则A’=(x,z),B’=(y,x,x,z);若A’=B’=空表,则A=B;若A’=空表且B’≠空表,或两者均不空且A’首元素小于B’首元素,则AB。 算法思路: 首先找出A、B的最大共同前缀;然后求出A’和B’,之后在按比较规则进行比较,A>B函数返回1;A=B返回0;A 算法如下: intcompare(intA[],intB[],intm,intn){ inti,j,AS[],BS[],ms,ns;//AS,BS作为A’,B’ i=0; ms=ns=0; while(i<=m&&i<=n&&A[i]==B[i]) i++;//找最大共同前缀 for(j=i;j {AS[j-i]=A[j]; ms++; }//求A’,ms为A’的长度 for(j=i;j {BS[j-i]=B[j]; ns++; }//求B’,ms为B’的长度 if(ms==ns&&ms==0) return0; else if(ms==0&&ns>0||ms>0&&ns>0&&AS[0] return–1; else return1; } 【算法2-7】比较线性表大小算法 算法的时间性能是O(m+n)。 2.3线性表的链式存储和运算实现 由于顺序表的存贮特点是用物理上的相邻实现了逻辑上的相邻,它要求用连续的存储单元顺序存储线性表中的各元素,因此,对顺序表插入、删除时需要通过移动数据元素来实现,影响了运行效率。 本节介绍线性表链式存储结构,它不需要用地址连续的存储单元来实现,因为它不要求逻辑上相邻的两个数据元素物理上也相邻,它是通过“链”建立起数据元素之间的逻辑关系来,因此对线性表的插入、删除不需要移动数据元素。 2.3.1单链表 链表是通过一组任意的存储单元来存储线性表中的数据元素的,那么怎样表示出数据元素之间的线性关系呢? 为建立起数据元素之间的线性关系,对每个数据元素ai,除了存放数据元素的自身的信息ai之外,还需要和ai一起存放其后继ai+1所在的存贮单元的地址,这两部分信息组成一个“结点”,结点的结构如图2-6所示,每个元素都如此。 存放数据元素信息的称为数据域,存放其后继地址的称为指针域。 因此n个元素的线性表通过每个结点的指针域拉成了一个“链子”,称之为链表。 因为每个结点中只有一个指向后继的指针,所以称其为单链表。 链表是由一个个结点构成的,结点定义如下: typedefstructNode{ DataTypedata; structNode*next; }Lnode,*LinkList; 定义头指针变量: LinkListH; 如图2-7是线性表(a1,a2,a3,a4,a5,a6,a7,a8)对应的链式存储结构示意图。 当然必须将第一个结点的地址160放到一个指针变量如H中,最后一个结点没有后继,其指针域必需置空,表明此表到此结束,这样就可以从第一个结点的地址开始“顺藤摸瓜”,找到每个结点。 作为线性表的一种存储结构,我们关心的是结点间的逻辑结构,而对每个结点的实际地址并不关心,所以通常的单链表用图2-8的形式而不用图2-7的形式表示。 通常我们用“头指针”来标识一个单链表,如单链表L、单链表H等,是指某链表的第一个结点的地址放在了指针变量L、H中,头指针为“NULL”则表示一个空表。 110 a5 200 … … 150 a2 190 160 a1 150 … … 190 a3 210 200 210 a6 260 a4 110 … …. 240 a8 NULL … ... 260 a7 240 图2.8链表示意图 图2.7链式存储结构 需要进一步指出的是: 上面定义的LNode是结点的类型,LinkList是指向Lnode类型结点的指针类型。 为了增强程序的可读性,通常将标识一个链表的头指针说明为LinkList类型的变量,如LinkListL;当L有定义时,值要么为NULL,则表示一个空表;要么为第一个结点的地址,即链表的头指针;将操作中用到指向某结点的指针变量说明为LNode*类型,如LNode*p;则语句: p=newLNode; 则完成了申请一块LNode类型的存储单元的操作,如果申请成功,并将其地址赋值给变量p,如图2-9所示。 P所指的结点为*p,*p的类型为LNode型,所以该结点的数据域为(*p).data 或p->data,指针域为(*p).next或p->next。 Deletep则表示释放p所指的结点。 如果申请失败,返回空指针。 因此申请结点后应判断其返回地址是否为空,空指针则表明没得到结点,不能继续进行。 在本教材中为了突出算法的逻辑,方便读者,认为每次申请结点成功,因此不再判断。 2.3.2单链表上基本运算的实现 ⒈建立单链表 ⑴在链表的头部插入结点建立单链表 链表与顺序表不同,它是一种动态管理的存储结构,链表中的每个结点占用的存储空间不是预先分配,而是运行时系统根据需求而生成的,因此建立单链表从空表开始,每读入一个数据元素则申请一个结点,然后插在链表的头部,如图2-10展现了线性表: (25,45,18,76,29)之链表的建立过程,因为是在链表的头部插入,读入数据的顺序和线性表中的逻辑顺序是相反的。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第2章 线性表 线性