严蔚敏《数据结构》(C语言版)【教材精讲+考研真题解析】讲义与视频课程【36小时高清视频】
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.3 线性表的链式表示与实现

视频二维码(扫码观看)

一、线性表的链式存储结构

链式存储:用一组任意的存储单元存储线性表中的数据元素。用这种方法存储的线性表简称线性链表。

存储链表中结点的一组存储单元可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。

链表中结点的逻辑顺序和物理顺序不一定相同。

为了正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其直接后继结点的地址(或位置),称为指针(pointer)或链(link),这两部分组成了链表中的结点结构,如图2-2所示。

图2-2 链表结点结构

链表是通过每个结点的指针域将线性表的n个结点按其逻辑次序链接在一起的。每一个结点只包含一个指针域的链表,称为单链表。为操作方便,总是在链表的第一个结点之前附设一个头结点(头指针)head指向第一个结点。头结点的数据域可以不存储任何信息(或存储链表长度等信息)。

单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名。

【例1】线性表L=(bat,cat,eat,fat,hat),其带头结点的单链表的逻辑状态和物理存储方式,如图2-3所示。

图2-3 带头结点的单链表的逻辑状态、物理存储方式

1结点的描述与实现

C语言中用带指针的结构体类型来描述

2结点的实现

结点是通过动态分配和释放来实现的,即在需要时分配内存,不需要时释放内存。实现时分别使用C语言提供的标准函数:malloc(),realloc(),sizeof(),free()。

动态分配:p=(LNode*)malloc(sizeof(LNode));

函数malloc申请了一大小为LNode的内存空间作为结点空间,并将其首地址放入指针变量p中。

动态释放:free(p);

系统回收由指针变量p所指向的内存区。p必须是最近一次调用malloc函数时的返回值。

3最常用的基本操作及其示意图

(1)结点的赋值

(2)常见的指针操作

q=p;

q=p->next;

p=p->next;

q->next=p;

q->next=p->next;

二、单线性链式的基本操作

1建立单链表

假设线性表中结点的数据类型是整型,以32767作为结束标志。动态地建立单链表的常用方法有如下两种:头插入法,尾插入法。

(1)头插入法建表

从一个空表开始,重复读入数据,生成新结点,将读入数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。即每次插入的结点都作为链表的第一个结点。

算法描述

(2)尾插入法建表

头插入法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插法建表。该方法是将新结点插入到当前链表的表尾,使其成为当前链表的尾结点。

算法描述

无论是哪种插入方法,如果要插入建立的单线性链表的结点是n个,算法的时间复杂度均为O(n)。

对于单链表,无论是哪种操作,只要涉及到钩链(或重新钩链),如果没有明确给出直接后继,钩链(或重新钩链)的次序必须是“先右后左”。

2单链表的查找

(1)按序号查找

取单链表中的第i个元素。对于单链表,不能像顺序表中那样直接按序号i访问结点,而只能从链表的头结点出发,沿链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构。

设单链表的长度为n,要查找表中第i个结点,仅当1≤i≤n时,i的值是合法的。

算法描述

移动指针p的频度:

当i<1时,0次;

当1≤i≤n时,i-1次;

当i>n时,n次。

因此时间复杂度:O(n)。

(2)按值查找

按值查找是在链表中,查找是否有结点值等于给定值key的结点?若有,则返回首次找到的值为key的结点的存储位置;否则返回NULL。查找时从开始结点出发,沿链表逐个将结点的值和给定值key作比较。

算法描述

算法的执行与形参key有关,平均时间复杂度为O(n)。

3单链表的插入

插入运算是将值为e的新结点插入到表的第i个结点的位置上,即插入到ai1与ai之间。因此,必须首先找到ai1所在的结点p,然后生成一个数据域为e的新结点q,q结点作为p的直接后继结点。

算法描述

设链表的长度为n,合法的插入位置是1≤i≤n。算法的时间主要耗费移动指针p上,故时间复杂度亦为O(n)。

4单链表的删除

(1)按序号删除

删除单链表中的第i个结点。

为了删除第i个结点ai,必须找到结点的存储地址。该存储地址是在其直接前驱结点ai1的next域中,因此,必须首先找到ai1的存储位置p,然后令p->next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间,将其归还给“存储池”。

设单链表长度为n,则删去第i个结点仅当1≤i≤n时是合法的。则当i=n+1时,虽然被删结点不存在,但其前驱结点却存在,是终端结点。故判断条件之一是p->next!=NULL。显然此算法的时间复杂度也是O(n)。

算法描述

视频二维码(扫码观看)

(2)按值删除

删除单链表中值为key的第一个结点。

与按值查找相类似,首先要查找值为key的结点是否存在?若存在,则删除;否则返回NULL。

算法描述

算法的执行与形参key有关,平均时间复杂度为O(n)。

从上面的讨论可以看出,链表上实现插入和删除运算,无需移动结点,仅需修改指针。解决了顺序表的插入或删除操作需要移动大量元素的问题。

变形之一:

删除单链表中值为key的所有结点。

与按值查找相类似,但比前面的算法更简单。

基本思想:从单链表的第一个结点开始,对每个结点进行检查,若结点的值为key,则删除之,然后检查下一个结点,直到所有的结点都检查。

算法描述

变形之二:

删除单链表中所有值重复的结点,使得所有结点的值都不相同。

与按值查找相类似,但比前面的算法更复杂。

基本思想:从单链表的第一个结点开始,对每个结点进行检查:检查链表中该结点的所有后继结点,只要有值和该结点的值相同,则删除之;然后检查下一个结点,直到所有的结点都检查。

算法描述

5单链表的合并

设有两个有序的单链表,它们的头指针分别是La、Lb,将它们合并为以Lc为头指针的有序链表。合并前的示意图如图2-4所示。

图2-4 两个有序的单链表La、Lb的初始状态

合并了值为-7,-2的结点后示意图如图2-5所示。

图2-5 合并了值为-7,-2的结点后的状态

算法说明

算法中pa,pb分别是待考察的两个链表的当前结点,pc是合并过程中合并的链表的最后一个结点。

算法描述

算法分析:

若La,Lb两个链表的长度分别是m,n,则链表合并的时间复杂度为O(m+n)。

三、循环链表

1循环链表(Circular Linked List)

循环链表是一种头尾相接的链表。其特点是最后一个结点的指针域指向链表的头结点,整个链表的指针域链接成一个环。从循环链表的任意一个结点出发都可以找到链表中的其他结点,使得表处理更加方便灵活。如图2-6所示。

图2-6 带头结点的单循环链表示意图

2循环链表的操作

对于单循环链表,除链表的合并外,其他的操作和单线性链表基本上一致,仅仅需要在单线性链表操作算法基础上作以下简单修改:

(1)判断是否是空链表:head->next==head;

(2)判断是否是表尾结点:p->next==head。

四、双向链表

视频二维码(扫码观看)

双向链表(Double Linked List):指的是构成链表的每个结点中设立两个指针域:一个指向其直接前驱的指针域prior,一个指向其直接后继的指针域next。这样形成的链表中有两个方向不同的链,故称为双向链表。

和单链表类似,双向链表一般增加头指针也能使双链表上的某些运算变得方便。

将头结点和尾结点链接起来也能构成循环链表,并称之为双向循环链表。

双向链表是为了克服单链表的单向性的缺陷而引入的。

1双向链表的结点及其类型定义

双向链表的结点的类型定义如下。如图2-7所示。

图2-7 双向链表结构

双向链表结构具有对称性,设p指向双向链表中的某一结点,则其对称性可用下式描述:

(p->prior)->next=p=(p->next)->prior;

结点p的存储位置存放在其直接前驱结点p->prior的直接后继指针域中,同时也存放在其直接后继结点p->next的直接前驱指针域中。

2双向链表的基本操作

(1)双向链表的插入:将值为e的结点插入双向链表中。插入前后链表的变化如图2-8所示。

图2-8 双向链表的插入

插入时仅仅指出直接前驱结点,钩链时必须注意先后次序是:“先右后左”。部分语句组如下:

S=(DulNode *)malloc(sizeof(DulNode));
S->data=e;
S->next=p->next;p->next->prior=S;
p->next=S;S->prior=p;/*钩链次序非常重要*/

插入时同时指出直接前驱结点p和直接后继结点q,钩链时无须注意先后次序。部分语句组如下:

S=(DulNode *)malloc(sizeof(DulNode));
S->data=e;
p->next=S;
S->next=q;
S->prior=p;
q->prior=S;

(2)双向链表的结点删除

设要删除的结点为p,删除时可以不引入新的辅助指针变量,可以直接先断链,再释放结点。部分语句组如下:

p->prior->next=p->next;
p->next->prior=p->prior;
free(p);

注意:

与单链表的插入和删除操作不同的是,在双向链表中插入和删除必须同时修改两个方向上的指针域。