5. 线性表的链式存储结构
5.1 顺序存储结构不足的解决办法
顺序存储结构,最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。
要解决这个问题,就得考虑一下导致这个问题的原因。
发现原因就在于相邻两元素的存储位置也具有邻居关系。它们在内存中的位置也是挨着的,中间没有空隙,当然就无法快速介入,而删除后,当中就会留出空隙,自然需要弥补。
要让相邻元素间留有足够余地,那干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在哪里。
5.2 线性表链式存储结构定义
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。
这就意味着,这些数据元素可以存在内存未被占用的任意位置(如图3-6-1所示):
以前在顺序结构中,每个数据元素只需要存数据元素信息就可以了。现在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。
因此,为了表示每个数据元素 a i a_i ai与其直接后继数据元素 a i + 1 a_{i+1} ai+1之间的逻辑关系,对数据元素 a 1 a_1 a1来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。
-
数据域
把存储数据元素信息的域称为数据域 -
指针域
把存储直接后继位置的域称为指针域。 -
指针/链
指针域中存储的信息称做指针或链。 -
结点
这两部分信息组成数据元素 a 1 a_1 a1的存储映像,称为结点(Node)。 -
单链表
n个结点( a i a_i ai的存储映像)链结成一个链表,即为线性表( a 1 a_1 a1, a 2 a_2 a2, … , a n a_n an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
单链表正是通过每个结点的指针城将线性表的数据元素按其逻辑次序链接在一起,如图3-6-2所示:
- 头指针
把链表中第一个结点的存储位置叫做头指针。
线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示, 如图3-6-3所示):
- 头结点
有时,为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。
头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等附加信息。
头结点的指针域存储指向第一个结点的指针,如图3-6-4所示:
用更方便的存储示意图来表示单链表,如图3-6-7所示:
若带有头结点的单链表,则如图3-6-8所示:
空链表如图3-6-9所示:
单链表中,在C语言中可用结构指针来描述:
typedef struct Node {
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList;
结点由存放数据元素的数据域存放后继结点地址的指针域组成。
假设p是指向线性表第i个元素的指针,则该结点 a i a_i ai的数据域可以用p->data来表示,p->data的值是一个数据元素。
结点 a i a_i ai的指针域可以用p->next来表示,p->next的值是一个指针。p->next指向第i+1个元素,即指向 a i + 1 a_{i+1} ai+1的指针。
也就是说,如果p->data= a i a_i ai, 那么p->next->data= a i + 1 a_{i+1} ai+1(如图3-6-10所示):
6. 单链表的读取
获得链表第i个数据的算法思路:
-
声明一个结点p指向链表第一个结点,初始化j从1开始;
-
当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
-
若到链表末尾p为空,则说明第i个元素不存在;
-
否则查找成功,返回结点p的数据。
实现代码算法如下:
Status GetElem(LinkList L, int i, ElemType *e) {
int j;
LinkList p;
p = L->next;
j = 1;
while (p && j < i) {
p = p->next;
++j;
}
if (!p || j > i) {
return ERROR
}
*e = p->data;
return OK;
}
这个算法的时间复杂度取决于i的位置,最坏情况的时间复杂度是O(n)。
其主要核心思想就是“工作指针后移”。
7. 单链表的插入与删除
7.1 单链表的插入
假设存储元素e的结点为s,要实现结点p、p->next和s之间逻辑关系的变化,只需将结点s插入到结点p和p->next之间即可。
代码实现如下:
s->next = p->next; p->next = s;
解读这两句代码,也就是说让p的后继结点改成s的后继结点,再把结点s变成p的后继结点(如图3-8-2所示):
这两句是无论如何不能反的。
插入结点s后,链表如图3-8-3所示:
对于单链表的表头和表尾的特殊情况,操作是相同的,如图3-8-4所示:
单链表第i个数据插入结点的算法思路:
- 声明一结点p指向链表第一个结点,初始化j从1开始;
- 当j<i时, 就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,在系统中生成一个空结点s;
- 将数据元素e赋值给s->data;
- 单链表的插入标准语句s->next=p->next; p->next=s;
- 返回成功。
实现代码算法如下:
Status ListInsert(LinkList *L, int i, ElemType e) {
int j;
LinkList p, s;
p = *L;
j = 1;
while (p && j<i) {
p = p->next;
++j;
}
if (!p || j>i) {
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
7.2 单链表的删除
设存储元素a的结点为q,要实现将结点q删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可,如图3-8-5所示:
代码实现如下:
q = p->next; p->next = q->next;
解读这两句代码,也就是说让p的后继的后继结点改成p的后继结点。
单链表第i个数据删除结点的算法思路:
- 声明一结点p指向链表第一个结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,将欲删除的结点p->next赋值给q;
- 单链表的删除标准语句p->next=q->next;
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
- 返回成功。
实现代码算法如下:
Status ListDelete(LinkList *L, int i, ElemType *e) {
int j;
LinkList p, q;
p = *L;
j = 1;
while (p->next && j < i) {
p = p->next;
++j;
}
if (!p->next || j > i) {
return ERROR;
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
8. 单链表的整表创建
回顾一下,顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型和大小的数组并赋值的过程。
而单链表和顺序存储结构就不一样,它不像顺序存储结构这么集中,它可以很散,是一种动态结构。
对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
所以创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单链表整表创建的算法思路:
- 声明一结点p和计数器变量i;
- 初始化一空链表L;
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
- 循环:
生成一新结点赋值给p;
随机生成一数字赋值给p的数据域p->data;
将p插入到头结点与前一新结点之间。
实现代码如下:
void createListHead(LinkList *L, int n) {
LinkList p;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
for (i=0; i<n; i++) {
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;
p—>next = (*L)->next;
(*L)->next = p;
}
}
这段算法代码里,其实用的是插队的办法,就是始终让新结点在第一的位置。也可以把这种算法简称为头插法,如图3-9-1所示:
事实上,还可以把每次新结点都插在终端结点的后面,这种算法称之为尾插法。
实现代码算法如下:
void CreateListTail(LinkList *L, int n) {
LinkList p, r;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
r = *L
for (i=0;i<n;i++) {
p = (Node *)maclloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
r->next = NULL;
}
注意L与r的关系,L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表。
r->next=p;
的意思,是将刚才的表尾终端结点r的指针指向新结点p,如图3-9-2所示,当中①位置的连线就是表示这个意思。
r=p;
是什么意思? 正如图3-9-3,就是本来r是在
a
i
−
1
a_{i-1}
ai−1元素的结点,可现在它已经不是最后的结点了,现在最后的结点是
a
i
a_i
ai,所以应该要让将p结点这个最后的结点赋值给r。此时r又是最终的尾结点了。
循环结束后,应该让这个链表的指针域置空,因此有了r->next=NULL;
以便以后遍历时可以确认其是尾部。
9. 单链表的整表删除
当我们不打算使用这个单链表时,我们需要把它销毁,也就是在内存中将它释放掉,以便于留出空间给其他程序或软件使用。
单链表整表除的算法思路如下:
- 声明一结点p和q;
- 将第一个结点赋值给p;
- 循环:
将下一结点赋值给q;
释放p;
将q赋值给p。
实现代码算法如下:
Status ClearList(LinkList *L) {
LinkList p, q;
p = (*L)->next;
while (p) {
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
其中,变量q的作用,它使得下一个结点是谁得到了记录,以便于等当前结点释放后,把下一结点拿回来补充。
参考
《大话数据结构》 —— 3 线性表