数据结构第3章 链表.docx
《数据结构第3章 链表.docx》由会员分享,可在线阅读,更多相关《数据结构第3章 链表.docx(39页珍藏版)》请在冰豆网上搜索。
数据结构第3章链表
第3章链表
上一章介绍的顺序表是线性表的顺序存储结构,采用顺序存储结构,内存的存储密度高;当结点等长时,可以随机地存取表中的结点。
但是,在顺序表中进行插入和删除结点的运算时,往往会造成大量结点的移动,效率较低;顺序表的存储空间常采用静态分配,在程序运行前就必须明确规定它的存储规模,如果线性表的长度n变化较大,则存储规模很难预先确定。
估计过大将导致空间的浪费,估计小了,随着结点的不断插入,所需的存储空间超出了预先分配的存储空间,就会发生空间溢出。
为了有效地克服顺序存储的不足,可以采用链接存储的方式。
链接存储适合于结点插入或删除频繁,存储空间需求不能预先确定的情形。
链接存储是最常用的存储方式之一,它不仅可以用来存储线性表,而且也可以用来存储各种非线性结构。
在后续章节将要讨论的各种复杂的数据结构(如树形结构、图结构等),都可以采用链表来进行存储。
本章仅介绍几种存储线性表的链接存储方式。
首先讨论单链表,以及基于单链表的栈与队列的表示方法,然后讨论单循环链表与双链表。
最后给出结点类与链表类的描述。
3.1单链表
单链表(singlylinkedlist)是一种最简单的链表,又称为线性链表。
它是最基本的链表结构,也是学习其他链表的基础。
3.1.1单链表的概念
用单链表来表示线性表时,每个数据元素占用一个结点(node)。
每个结点均由两个域(字段)组成:
一个域存放数据元素(data);另一个域存放指向结点后继的指针(next)。
终端结点没有后继,它next域为空(NULL),在图示中用Λ表示。
另外还需要一个表头指针head指向表的第一个结点。
单链表中的结点形式为:
一个线性表(a0,a1,…,an-1)的单链表结构如图3-1所示。
图3-1(a)是非空链表,它所表示的线性表为
L=(a0,a1,…,an-1)
图3-1(b)是空链表,是链表一种特殊情况。
此时它所表示的的线性表为空表:
L=()
这种链表中的每个结点只有一个指针域,所以称之为单链表(或线性链表)。
3.1.2单链表的存储描述
假设data字段均为相同的数据类型。
用C语言描述的的单链表如下:
typedefintdatatype;//假设结点的数据域的类型为整型
typedefstructnode{//结点类型定义
datatypedata;//结点的数据域
structnode*next;//结点的指针域
}ListNode,*LinkList;
ListNode*p,*q,*r;
LinkListhead;
datatypex;
这里需要注意以下几点:
⑴在上面的类型定义和变量说明中,ListNode*和LinkList是不同名字的同一个指针类型,不同的命名使得含义更加明确。
例如,ListNode*类型的指针变量p表示它是指向某一结点的指针,而LinkList类型的指针变量head则表示它是单链表的头指针。
⑵要严格区分指针变量与结点变量这两个概念。
指针变量是一种特殊的变量,它的值是所指结点(在说明部分即类型定义和变量说明中已明确)的地址;而结点变量就是通常意义下的变量。
例如:
上面的定义的变量p是类型为ListNode*的指针变量,若p的值非空(p!
=NULL),则它的值是类型为ListNode的某一个结点的地址。
而结点变量的类型则是ListNode。
通常p所指的结点变量并非在变量说明部分明显的定义,而是在程序执行的过程中,当需要时才通过申请空间而产生,因此把这种变量称为动态变量。
⑶存储空间的动态申请(分配)与释放(归还):
C语言提供了两个标准函数malloc()和free()来完成这两项工作。
申请时使用malloc()函数,例如:
p=(ListNode*)malloc(sizeof(ListNode));
表示分配了一个类型为ListNode的结点变量的空间,并把其首地址放入指针变量p中。
再例如:
int*s;
s=(int*)malloc(sizeof(int));
表示分配了一个类型为int的结点变量的空间,并把其首地址放入指针变量s中。
当指针变量所指的结点变量空间不再需要,可以通过标准函数free()来释放(即把使用后的存储空间归还给系统)。
对于上面的两个申请,释放时可写成:
free(p);
free(s);
C++提供了更为简洁的方式:
使用两个运算符new和delete来完成空间的申请(分配)与释放(归还)。
上面的例子用C++等价地描述为
申请空间:
p=newListNode;
s=newint;
释放空间:
deletep;
deletes;
由于C++提供的方式使用简单且功能又强,因此,我们使用new和delete来实现空间的申请与释放。
⑷由于这种结点变量是动态申请的,因此无法利用预先定义的标识符去访问它,而只能通过指针变量来访问它。
如前面的说明中有:
ListNode*p;
这里指针变量为p,结点变量为*p,即用*p作为该结点变量的名字来访问。
由于结点类型ListNode是结构类型,因而*p是结构名,还需用成员选择符“.”来访问该结构的两个分量(*p).data和(*p).next。
这种表示形式比较繁琐,可选用另一种成员选择符“->”来访问指针所指结构的成员更为方便,即p->data和p->next。
3.1.3在单链表上实现的基本运算
下面我们讨论用单链表作为存储结构时,如何实现线性表的一些基本运算。
1.访问单链表中的第i个结点
在顺序存储时,我们根据下标(索引)值,可以按公式:
LOC(ai)=LOC(a0)+i*c,直接计算求得第i个结点的地址,而时间与i的大小无关。
在链接存储时,需要从指针变量head所指的头结点开始沿着next字段组成的链,一个一个结点地向后搜索,直到第i个结点为止。
因此,查找ai所需的时间代价与i的大小成正比。
说明:
在后面的单链表的算法中,假定结点都为ListNode类型。
进入算法前,指针head已经指向单链表的首结点。
变量p和q是两个指针(变量)。
这里,0≤i≤n-1,算法结束时,p中存放着要找的第i个结点的地址。
当单链表中结点数小于i或i<0时,函数返回值为NULL。
算法3.1
查找链表第i个结点地址
Locate(head,i)
1.若i<0
则returnNULL
p←head
2.循环i次,执行
若p=NULL
则print("notfound");
returnNULL
否则p←p->next
3.returnp;
4.[算法结束]▍
算法的执行时间主要花费在循环语句上,它显然与i的大小有关。
在等概率的情况下,查找的平均时间复杂度为:
AMN(或Mavg)=
请读者自行与顺序存储结构进行对照比较。
2.单链表的插入
在链表中,结点间的关系是通过指针的链接实现的,而与结点在存储器的位置无关。
只须改变相应结点的next字段的值就行了。
当需要一个新结点时,通过执行
q=newListNode;
就可以能得到一个新结点,它的地址存放在指针变量q中,空间的大小与q所指结点类型ListNode的大小相同。
插入运算是将值为x的新结点插入到表的第i个结点的位置上,即插入在ai-1和ai之间。
因此首先需要找到ai-1的存储地址p,然后生成一个值为x的新结点*q,并通过调整指针来完成结点的插入工作。
插入过程见图3-3。
具体算法如下:
算法3.2单链表的插入
Insert(head,i,x)
1.q=newListNode;
q->data←x
2.若i=0
则q->next←head;head←q
否则⑴p←Locate(head,i-1);
⑵若p=NULL
则print("error");
算法结束
⑶q->next←p->next;
p->next←q
3.[算法结束]▍
设单链表的长度为n,合法的插入位置是0≤i≤n。
算法所花费的时间主要分为两部分:
①该算法调用了查找第i-1个结点地址的过程:
Locate(head,i-1,p),在前面的算法分析中我们已知道它的时间代价为O(n);
②当确定了插入位置(根据返回的指针p)后,接下来的工作就是通过调整指针将生成的值为x的新结点插入,这时,无论是插入在表的什么位置,都是由两条赋值语句就可完成。
因此,就插入动作来说,它的时间代价仅为O
(1)。
综合以上的分析,虽然对整个算法来说时间代价为O(n),但是它的主要的执行时间是花费在查找上,而真正进行插入的时间仅为常量级。
所以,一般地说,插入操作的时间代价为O
(1)。
3.单链表的删除
删除运算是将表的第i个结点删去。
由于在单链表中结点ai的存储地址是在前驱结点ai-1的指针域next中,因此需要先找到结点ai-1的存储位置并用指针p指向它。
然后让p->next指向ai的后继结点,即把ai从链上摘掉。
最后释放结点ai的存储空间,可以使用C++的delete操作符来完成,把删除结点的地址放于q中,执行deleteq;就把此结点的存储空间释放掉了,即归还给了可利用空间表(listofavailablespace),也叫作存储池(storagepool)。
删除过程见图3-4。
具体算法如下:
算法3.3单链表的删除
Delete(head,i)
1.若i=0
则q←head;head←q->next
否则⑴p←Locate(head,i-1);
⑵若p=NULL
则print("error");
算法结束
⑶若p->next=NULL
则print("noithnode");
算法结束
⑷q←p->next;
p->next←q->next
2.deleteq
3.[算法结束]▍
设单链表的长度为n,合法的删除位置是0≤i≤n-1。
与插入算法的分析类似,该算法的时间代价为O(n),它主要的执行时间也是花费在查找定位上,而用在删除操作上的时间代价仍为O
(1)。
3.1.4带表头结点的单链表
为了运算的方便,在实际应用中,可以在链表的表头指针和开始结点之间附加一个称作表头结点的特殊结点,如图3-5所示。
表头结点的data域并不存放线性表的数据元素,它常为空,有时也可以存放一些辅助信息(如表中的结点个数等)。
增加表头结点的好处是使得运算简单、处理方便。
具体来说,有以下两条:
⑴由于开始结点的地址被存放在表头结点的指针域中,所以在链表的开始结点处的插入或删除可以不作为特殊情况来专门处理,而与链表的其他位置上的操作一致。
⑵无论链表是否为空,其表头指针总是指向表头结点的一个非空指针(空表时表头结点的指针域为空),这样空表和非空表的处理也就统一起来了。
请与前面的单链表结点的插入和删除的图示进行对照比较。
在带表头结点的单链表中,存在表头指针head,表头结点及用与开始结点,请分析它们三者有何区别?
并说明表头指针和表头结点各自的作用。
读者还可以思考下列问题:
⑴从时间与空间的角度看,将一般的单链表改造成带表头结点的单链表体现了哪种思想或策略?
⑵一般的单链表与带表头结点的单链表表空的判断条件是什么?
⑶在解决实际问题时,一般的单链表与带表头结点的单链表各适合于何种情况?
3.2栈和队列的链接存储表示
上一章讨论了栈和队列的顺序存储,当使用单个栈和队列时是经常采用的,但是同时使用多个栈和队列时,为了共享一个存储区域,以节省空间,更有效的办法是采用链接存储,把栈和队列组织成的单链表形式。
3.2.1链栈
用链接存储方式表示的栈称为链栈。
它的表示形式就是前面讲的单链表,只是插入和删除等运算仅限定在表首进行。
表头指针就是栈顶指针,用指针变量top来标记。
图3-6给出了链栈的图示。
链栈的类型和变量说明如下:
typedefstructnode{//结点类型定义
datatypedata;//结点的数据域
structnode*next;//结点的指针域
}StackNode,*LinkStack;
StackNode*p,*q;
LinkStacktop;
datatypex;
下面给出在链栈上实现的基本运算。
由于链栈中的结点是动态分配的,可以不考虑“上溢”问题,因此无需定义StackFull运算。
具体算法如下:
算法3.7链栈的推入C/C++程序:
push(top,x)voidpush(LinkStack&top,datatypex){
1.p←newStackNode;StackNode*p;
p->data←xp=newStackNode;
2.p->next←top;top←pp->data=x;
3.[算法结束]▍p->next=top;top=p;
}
算法3.8链栈的弹出C/C++程序:
pop(top,x)voidpop(LinkStack&top,datatype&x){
1.若top=NULLif(top==NULL)
则print("underflow")cout<<"underflow";
否则p←top;x←top->data;else{StackNode*p=top;x=top->data;
top←top->next;top=top->next;
deletepdeletep;
2.[算法结束]▍}
}
算法3.9读链栈的栈顶元素C/C++程序:
GetTop(top,x)voidGetTop(LinkStacktop,datatype&x){
1.若top=NULLif(top==NULL)
则print("error")cout<<"error";
否则x←top->dataelse
2.[算法结束]▍x=top->data;
}
算法3.10置空链栈C/C++程序:
ClearStack(top)voidClearStack(LinkStack&top){
1.top←NULLtop=NULL;
2.[算法结束]▍}
算法3.11
判断链栈空否C/C++程序:
StackEmpty(top)intStackEmpty(LinkStack&top){
1.若top=NULL
if(top==NULL)
则returnTRUEreturn1;
否则returnFALSEelse
2.[算法结束]▍return0;
}
3.2.2链队列
用链接存储方式表示的队列称为链队列。
它是限制在表头删除和表尾插入的单链表。
由于操作在表头和表尾进行,所以需设置两个指针变量:
队头指针front和队尾指针rear来指向链表的第一个结点和最后一个结点。
为了便于管理和控制,把front和rear这两个指针变量封装在一起,将链队列的类型LinkQueue定义成一个结构类型。
图3-7给出了链队列的图示。
链队列的类型和变量说明如下:
typedefstructnode{
datatypedata;//结点的数据域
structnode*next;//结点的指针域
}QueueNode;
typedefstruct{
QueueNode*front;//队头指针
QueueNode*rear;//队尾指针
}LinkQueue;
QueueNode*p,*q;
LinkQueueQU;
datatypex;
具体算法如下:
算法3.12链队列的插入(入队)C/C++程序:
EnQueue(QU,x)voidEnQueue(LinkQueue&QU,datatypex){
1.p←newQueueNode;QueueNode*p=newQueueNode;
p->data←x;p->next←NULL;p->data=x;p->next=NULL;
2.若QU.front=NULLif(QU.front==NULL)//空队列
则QU.front←p;QU.front=QU.rear=p;
QU.rear←pelse{QU.rear->next=p;//非空队列
否则QU.rear->next←p;QU.rear=p;
QU.rear←p}
3.[算法结束]▍}
算法3.13链队列的删除(出队)C/C++程序:
DeQueue(QU,x)voidDeQueue(LinkQueue&QU,datatype&x){
1.若QU.front=NULLif(QU.front==NULL)
则print("underflow")cout<<"underflow";
否则q←QU.front;else{QueueNode*q=QU.front;
x←q->data;x=q->data;
QU.front←q->next;QU.front=q->next;
deleteqdeleteq;
2.[算法结束]▍}
}
算法3.14读链队列栈的队头元素C/C++程序:
GetFront(QU,x)voidGetFront(LinkQueueQU,datatype&x){
1.若QU.front=NULLif(QU.front==NULL)
则print("error")cout<<"error";
否则x←QU.front->dataelse
2.[算法结束]▍x=QU.front->data;
}
算法3.15置空链队列C/C++程序:
ClearQueue(QU)voidClearQueue(LinkQueue&QU){
1.QU.front←NULL;QU.front=QU.rear=NULL;
QU.rear←NULL}
2.[算法结束]▍
算法3.16
判断链队列空否C/C++程序:
QueueEmpty(QU)intQueueEmpty(LinkQueue&QU){
1.若QU.front=NULL
if(QU.front==NULL)
则returnTRUEreturn1;
否则returnFALSEelse
2.[算法结束]▍return0;
}
3.3循环链表
循环链表(circularlinkedlist)在本节是指单循环链表,它是单链表的另外一种形式。
它的结点结构与单链表相同,与单链表的主要差别是:
链表中的最后一个结点的指针域不再为空,而是指向链表的开始结点。
这样整个链表形成了一个环,只要知道表的任何一个结点的地址,就能找到表中其他的所有结点。
图3-8是单循环链表的图示。
实现循环链表的运算与单链表类似,只是控制条件有所差别:
在单循环链表中检查指针p是否达到链表的链尾时,不是判断p->next=NULL,而是判断p->next=head。
在实际处理中,常常用到表的首结点和尾结点,在图3-8所示的循环链表中,首结点可通过head直接找到,而尾结点则要搜索n次(n为表长)才能找到。
所以,可将指向表头的指针改为指向表尾。
这样首结点和尾结点都可直接找到,这给一些运算带来了很大的方便。
设置了表尾指针的单循环链表如图3-9所示。
与单链表一样,循环链表也可以带有表头结点,这样能够便于链表的操作,统一空表与非空表的运算。
图3-10给出了带有表头结点的空表与非空表的情形。
例3.1合并运算
编写一个算法,将图3-11(a)给出的两个单循环链表合并为一个如图3-11(b)所示的单循环链表。
请读者自行练习。
3.4双链表
用单链表表示的线性表,由于每个结点只有一个指向后继结点的指针,因此,对线性表中的任一个结点,都能通过next字段找到它的后继,执行时间为O
(1);而要找它的前驱,则需要从表头指针出发进行查找(对循环单链表也需要循环找一圈),执行时间为O(n)。
其中n为表中结点个数(表长)。
为了克服单链表这种单向性的缺点,对于找前驱结点较为频繁的运算,我们可以组织双(向)链表(doublelinkedlist)。
3.4.1双链表的概念
双链表即在单链表的每个结点中增加一个指向前驱结点的指针prior。
这样每个结点就有了两个指针域,一个指向前驱,一个指向后继。
结点形式为
双链表及结点的类型与变量说明如下:
typedefstructDnode{
datatypedata;
structDnode*prior,*next;
}DLnode;
typedefstruct{
DLnode*front,*rear;
}DoubleLinkList;
DoubleLinkListDL;
DLnode*p,*q;
双链表如图3-12所示。
3.4.2带表头结点的双循环链表
与单链表的情形类似,双链表常采用带表头结点的循环链表的方式,这样的双链表称为带表头结点的双循环链表。
它有一个表头结点,由链表的表头指针head指示,它的data域不放数据,或者存放辅助信息(如表长);它的prior指向双链表的最后一个结点,它的next指向双链表的最前端的第一个结点。
链表的第一个结点的左指针prior和最后一个结点的右指针next都指向表头结点。
它的存储说明如下,图示参见图3-13。
typedefDLnode*DLinkList;
DLinkListhead;
DLnodep,q;
d