数据结构与算法——第2章-单链表(链式存储结构)(2.3.1)

一 概述

1
2
本文详细介绍了链表的数据结构,包括节点的组成(数据域和指针域),
头节点和头指针的作用,以及如何使用C语言创建、插入、删除、查找和更新链表中的元素

二 链表

1
2
3
4
5
6
7
8
链表(链式存储结构、单链表)用于存储逻辑关系为“一对一”的数据。
与顺序表不同,链表不限制数据的物理存储状态,使用链表存储的数据元素,其物理存储位置是随机的。

例如,使用链表存储{1,2,3},数据的物理存储状态如下(图1):

上图根本无法体现出各数据之间的逻辑关系(图2)。
对此,链表的解决方案是:每个数据元素在存储时都配备一个指针,
用于指向自己的直接后继元素(数据元素随机存储,并通过指针表示数据之间逻辑关系)

图示

图1 图2

三 链表的节点

3.1 链表的组成

1
2
3
4
5
链表中每个数据的存储都由以下两部分组成(图1)
-数据元素本身,其所在的区域称为数据域
-指向直接后继元素的指针,所在的区域称为指针域

上图所示的结构在链表中称为节点。链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中(图2)

图示

图1 图2

3.2 示例

1
2
3
4
5
6
7
链表中每个节点的具体实现需使用 C 语言中的结构体
由于指针域中的指针指向的也是一个节点,因此要声明为 Link 类型(这里要写成 struct Link*的形式)

typedef struct Link{
char elem; // 代表数据域
struct Link * next; // 代表指针域, 指向直接后继元素
}link; // link 为节点名, 每个节点都是一个 link 结构体

四 头节点,头指针和首元节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
一个完整的链表需包括:
-头指针:一个普通的指针,永远指向链表第一个节点的位置
(用于指明链表的位置,便于后期找到链表并使用表中的数据)。

-节点:链表中的节点又细分为:
-头节点:
一个不存任何数据的空节点,通常作为链表的第一个节点
(头节点不是必须的,它的作用只是为了方便解决某些实际问 题)。

-首元节点:链表中第一个存有数据的节点(只是对链表中第一个存有数据节点的一个称谓,没有实际意义)。
-其他节点:链表中其他的节点。

一个存储{1,2,3}的完整链表结构如下

注意:链表中有头节点时,头指针指向头节点;若链表中没有头节点,则头指针指向首元节点

图示

五 链表的创建(初始化)

5.1 准备工作

1
2
3
创建一个链表需要做如下工作:
-声明一个头指针(如果有必要,可声明一个头节点)
-创建多个存储数据的节点,在创建的过程中,要随时与其前驱节点建立逻辑关系

5.2 示例

1-创建一个存储{1,2,3,4}且无头节点的链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
link * initLink(){
// 创建头指针
link * p=NULL;
// 创建首元节点
link * temp = (link*)malloc(sizeof(link));
// 首元节点先初始化
temp->elem = 1;
temp->next = NULL;
// 头指针指向首元节点
p = temp;
// 从第二个节点开始创建
for (int i=2; i<5; i++) {
// 创建一个新节点并初始化
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
// 将 temp 节点与新建立的 a 节点建立逻辑关系
temp->next=a;
// 指针 temp 每次都指向新链表的最后一个节点, 其实就
// 是 a 节点, 这里写 temp=a 也对
temp=temp->next;
}
// 返回建立的节点, 只返回头指针 p 即可, 通过头指针即可找
// 到整个链表
return p;
}

2-创建一个存储{1,2,3,4}且含头节点的链表(C 语言实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
link * initLink(){
// 创建一个头结点
link * p=(link*)malloc(sizeof(link));
// 声明一个指针指向头结点
link * temp=p;
// 生成链表
for (int i=1; i<5; i++) {
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
temp->next=a;
temp=temp->next;
}
return p;
}

3-只需在主函数中调用initLink函数,即可轻松创建一个存储{1,2,3,4}的链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <stdlib.h>

// 链表中节点的结构
typedef struct Link{
int elem;
struct Link *next;
}link;

// 初始化链表的函数
link * initLink();
// 用于输出链表的函数
void display(link *p);

int main() {
// 初始化链表(1,2,3,4)
printf("初始化链表为:\n");
link *p=initLink();
display(p);
return 0;
}

link * initLink(){
// 创建头指针
link * p=NULL;
// 创建首元节点
link * temp = (link*)malloc(sizeof(link));
// 首元节点先初始化
temp->elem = 1;
temp->next = NULL;
// 头指针指向首元节点
p = temp;
for (int i=2; i<5; i++) {
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
temp->next=a;
temp=temp->next;
}
return p;
}

void display(link *p){
// 将 temp 指针重新指向头结点
link* temp=p;
// 只要 temp 指针指向的结点的 next 不是 Null, 就执行输出语句
while (temp) {
printf("%d ",temp->elem);
temp=temp->next;
}
printf("\n");
}

/*
初始化链表为:
1 2 3 4
*/

4-如果使用带有头节点创建链表的方式,则输出链表的 display 函数需做适当修改

1
2
3
4
5
6
7
8
9
10
void display(link *p){
// 将 temp 指针重新指向头结点
link* temp=p;
// 只要 temp 指针指向的结点的 next 不是 Null, 就执行输出语句
while (temp->next) {
temp=temp->next;
printf("%d",temp->elem);
}
printf("\n");
}

六 单链表的基本操作

6.1 创建链表

以下对链表的操作实现均建立在已创建好链表的基础上,创建链表的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 声明节点结构
typedef struct Link{
// 存储整形元素
int elem;
// 指向直接后继元素的指针
struct Link *next;
}link;

// 创建链表的函数
link * initLink(){
// 创建一个头结点
link * p=(link*)malloc(sizeof(link));
// 声明一个指针指向头结点, 用于遍历链表
link * temp=p;
// 生成链表
for (int i=1; i<5; i++) {
// 创建节点并初始化
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
// 建立新节点与直接前驱节点的逻辑关系
temp->next=a;
temp=temp->next;
}
return p;
}

从实现代码中可以看到,该链表是一个具有头节点的链表。

头节点本身不用于存储数据(在实现对链表中数据的 "增删查改" 时要注意)

6.2 插入元素

一、概念

1
2
3
4
5
6
7
8
9
10
11
同顺序表一样,向链表中增添元素,根据添加位置不同可分为 3 种情况:

-插入到链表的头部(头节点之后),作为首元节点
-插入到链表中间的某个位置
-插入到链表的最末端,作为链表中最后一个数据元素

在链表插入元素只需做两步操作:
-将新结点的 next 指针指向插入位置后的结点
-将插入位置前结点的 next 指针指向插入结点

例如,在链表 {1,2,3,4} 的基础上分别实现在头部、中间部位、尾部插入新元素 5:

图示

二、示例:链表插入元素的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// p 为原链表, elem 表示新数据元素, add 表示新元素要插入的位置
link * insertElem(link * p, int elem, int add) {
// 创建临时结点 temp
link * temp = p;
// 首先找到要插入位置的上一个结点
for (int i = 1; i < add; i++) {
temp = temp->next;
if (temp == NULL) {
printf("插入位置无效\n");
return p;
}
}
// 创建插入结点 c
link * c = (link*)malloc(sizeof(link));
c->elem = elem;
// 向链表中插入结点
c->next = temp->next;
temp->next = c;
return p;
}

6.2 删除元素

一、概念

1
2
3
4
5
6
7
从链表中删除指定数据元素时,要对存储空间负责,对不再利用的存储空间要及时释放。
因此,从链表中删除数据元素需进行以下 2 步操作:
-将结点从链表中摘下来
-手动释放掉结点,回收被结点占用的存储空间

从链表上摘除某节点时,只需找到该节点的直接前驱节点 temp,并执行:temp->next=temp->next->next;
例如,从{1,2,3,4}链表中删除元素 3,则此代码的执行效果如下

图示

二、示例:链表删除元素(C 语言实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// p 为原链表, add 为要删除元素的值
link * delElem(link * p, int add) {
link * temp = p;
// 遍历到被删除结点的上一个结点
for (int i = 1; i < add; i++) {
temp = temp->next;
if (temp->next == NULL) {
printf("没有该结点\n");
return p;
}
}
// 单独设置一个指针指向被删除结点, 以防丢失
link * del = temp->next;
// 删除某个结点的方法就是更改前一个结点的指针域
temp->next = temp->next->next;
// 手动释放该结点, 防止内存泄漏
free(del);
return p;
}

6.3 查找元素

一、概念

1
2
3
在链表中查找指定数据元素最常用的方法:
从表头依次遍历表中节点,用被查找元素与各节点数据域中存储的数据元素进行比对,
直至比对成功或遍历至链表最末端的 NULL(比对失败的标志)。

二、示例:链表中查找特定数据元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// p 为原链表, elem 表示被查找元素
int selectElem(link * p,int elem){
// 新建一个指针 t, 初始化为头指针 p
link * t=p;
int i=1;
// 由于头节点的存在, 因此 while 中的判断为 t->next (直接越过
// 头节点对链表进行有效遍历)
while (t->next) {
t=t->next;
if (t->elem==elem) {
return i;
}
i++;
}
// 程序执行至此处, 表示查找失败
return -1;
}

注意:遍历有头节点的链表时,需避免头节点对测试数据的影响

6.4 更新元素

更新链表中的元素,只需通过遍历找到存储此元素的节点,对节点中的数据域做更改操作

1
2
3
4
5
6
7
8
9
10
11
12
13
// 更新函数, add 表示更改结点在链表中的位置, newElem 为新的数
// 据域的值
link *amendElem(link * p,int add,int newElem){
link * temp=p;
// 在遍历之前, temp 指向首元结点
temp=temp->next;
// 遍历到待更新结点
for (int i=1; i<add; i++) {
temp=temp->next;
}
temp->elem=newElem;
return p;
}

6.5 增删查改总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include <stdio.h>
#include <stdlib.h>

typedef struct Link {
int elem;
struct Link *next;
}link;

link * initLink();
// 链表插入的函数, p 是链表, elem 是插入的结点的数据
// 域, add 是插入的位置
link * insertElem(link * p, int elem, int add);
// 删除结点的函数, p 代表操作链表, add 代表删除节点的位置
link * delElem(link * p, int add);
// 查找结点的函数, elem 为目标结点的数据域的值
int selectElem(link * p, int elem);
// 更新结点的函数, newElem 为新的数据域的值
link *amendElem(link * p, int add, int newElem);
void display(link *p);

int main() {
// 初始化链表(1,2,3,4)
printf("初始化链表为:\n");
link *p = initLink();
display(p);
printf("在第4的位置插入元素5:\n");
p = insertElem(p, 5, 4);
display(p);
printf("删除元素3:\n");
p = delElem(p, 3);
display(p);
printf("查找元素2的位置为:\n");
int address = selectElem(p, 2);
if (address == -1) {
printf("没有该元素");
}
else {
printf("元素2的位置为:%d\n", address);
}
printf("更改第3的位置上的数据为7:\n");
p = amendElem(p, 3, 7);
display(p);
return 0;
}

link * initLink() {
// 创建一个头结点
link * p = (link*)malloc(sizeof(link));
// 声明一个指针指向头结点, 用于遍历链表
link * temp = p;
// 生成链表
for (int i = 1; i < 5; i++) {
link *a = (link*)malloc(sizeof(link));
a->elem = i;
a->next = NULL;
temp->next = a;
temp = temp->next;
}
return p;
}

link * insertElem(link * p, int elem, int add) {
// 创建临时结点temp
link * temp = p;
// 首先找到要插入位置的上一个结点
for (int i = 1; i < add; i++) {
temp = temp->next;
if (temp == NULL) {
printf("插入位置无效\n");
return p;
}
}
// 创建插入结点 c
link * c = (link*)malloc(sizeof(link));
c->elem = elem;
// 向链表中插入结点
c->next = temp->next;
temp->next = c;
return p;
}

link * delElem(link * p, int add) {
link * temp = p;
// 遍历到被删除结点的上一个结点
for (int i = 1; i < add; i++) {
temp = temp->next;
if (temp->next == NULL) {
printf("没有该结点\n");
return p;
}
}
// 单独设置一个指针指向被删除结点, 以防丢失
link * del = temp->next;
// 删除某个结点的方法就是更改前一个结点的指针域
temp->next = temp->next->next;
// 手动释放该结点, 防止内存泄漏
free(del);
return p;
}

int selectElem(link * p, int elem) {
link * t = p;
int i = 1;
while (t->next) {
t = t->next;
if (t->elem == elem) {
return i;
}
i++;
}
return -1;
}

link *amendElem(link * p, int add, int newElem) {
link * temp = p;
// temp 指向首元结点
temp = temp->next;
// temp 指向被删除结点
for (int i = 1; i < add; i++) {
temp = temp->next;
}
temp->elem = newElem;
return p;
}

void display(link *p) {
// 将 temp 指针重新指向头结点
link* temp = p;
// 只要 temp 指针指向的结点的 next 不是 Null, 就执行输出语句
while (temp->next) {
temp = temp->next;
printf("%d ", temp->elem);
}
printf("\n");
}

/*
初始化链表为:
1 2 3 4
在第4的位置插入元素5:
1 2 3 5 4
删除元素3:
1 2 5 4
查找元素2的位置为:
元素2的位置为:2
更改第3的位置上的数据为7:
1 2 7 4
*/

七 参考

  • CSDN—单链表(链式存储结构)