今天是2020年8月4日,昨天我复习了《数据结构之线性表复习(上)》中的内容,今天撰写下部内容,主要复习线性表的链式结构中的单链表。
老规矩,先上思维导图:
直接开始复习,不废话了。
链式存储线性表时,不需要使用地址连续的存储单元,换句话说,就是允许逻辑上连续的存储单元在物理上不连续,链式存储下的线性表通过一根 “链条” 建立起数据元素之间的逻辑关系,因此插入和删除数据元素的时间复杂度为 ,但同时也失去了线性表在顺序存储时的一大特性——随机存取,使得访问数据元素的时间复杂度从顺序结构时的 到链式结构时的 。
单链表
单链表采用一组任意的存储单元对线性表中的数据元素进行存储。在单链表中,每个数据元素都需要增设一个后继指针域,使得前驱结点与后继结点之间的逻辑次序保持通畅。其形式如下图:
每个结点的 data
域存放数据元素,next
域存放指向后继结点的指针,因此可得其结点代码描述如下:
typedef struct LNoed{ ElemType data; struct LNode *next;}LNode,*LinkList;
此处代码同顺序表中的申明做出对比,显然,链式存储增设指针域,也就增加了空间开销,但是好在解决了顺序表中需要大量物理地址连续的存储单元的缺点,使得碎片化空间也得以利用。可惜,这玩意是非随机存取的,这意味着查找某个元素,必须从头结点开始依次往下访问,直到所需结点元素为止。
通常我们会在单链表的表头加一个头指针 L
,当头指针为 NULL
时,意味着单链表 L
为空表。接着会在 L
的后面加入一个不存放任何数据信息的,之后指向后继结点的头结点。当然也可以把头结点的数据域用于存放链表表长。带头结点的链表如下图所示:
引入头结点有俩好处:
一、由于第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在链表的任意位置都一致,不需要做特殊处理。二、无论链表是否为空,其头指针都会指向头结点,这样子空表和非空表的处理也就得到了统一。
头插法
从一个空表开始,每生成一个新结点,就把线性表中的数据元素存放到该结点的数据域中,然后把这个结点插入到当前表的表头,也就是头结点之后,如下图所示:
尾插法
从一个空表开始,总是把新结点插入在表尾位置,这样子能够保证链表和原线性表的数据元素次序一致,尾插法示意图如下:
由于尾插法需要增设一个尾指针指示表尾结点,因此其时间复杂度同头插法一致,为 。
查找元素不做赘述,头指针开始依次往下搜索即可。
插入操作
插入操作是将含有数据元素的新结点插入到单链表的第 i 个位置上。首先需要检查插入位点的合法性,然后找到带插入位点的前驱结点,然后在其后插入新结点,并修改新结点的指针域,指向第 i-1 个结点的后继结点。插入过程如图所示:
实现插入结点的关键代码如下:
p=GetElem(L,i-1);s->next=p->next;p->next=s;
删除操作
删除操作是将第 i 个结点删除。同样需要先检查删除位点的合法性,然后查找表中的第 i-1 个结点,也就是欲删除结点的前驱结点,将该前驱结点的指针域修改为指向删除结点的后继结点即可。删除过程如下图所示:
删除操作的代码片段:
p=GetElem(L,i-1);q=p->next;p->next=q->next;free(q);
其实不难发现,插入和删除操作的时间复杂度都是 ,但是由于需要遍历一遍单链表,寻找操作结点的前驱结点,故而总的时间复杂度在 。