9.4 用指针处理链表
9.4.1 什么是链表
链表是一种常见且重要的数据结构。它是一种动态存储分配的结构,能够根据需要开辟内存单元,避免了数组固定长度的限制,从而更加高效地利用内存资源。
链表的基本结构
链表有一个“头指针”变量,图中以 head
表示,它存放一个地址,该地址指向一个元素。链表中每一个元素称为“结点”,每个结点都应包括两个部分:
- 用户需要用的实际数据
- 下一个结点的地址
可以看出,head
指向第1个元素,第1个元素又指向第2个元素……直到最后一个元素,该元素不再指向其他元素,它称为“表尾”,它的地址部分放一个“NULL”(表示“空地址”),链表到此结束。
链表示意图
head
|
v
+-----+------+
| A | next |---> +-----+------+
+-----+------+ | B | next |---> +-----+------+
+-----+------+ | C | next |---> +-----+------+
+-----+------+ | D | NULL |
+-----+------+
链表中各元素在内存中的地址可以是不连续的。要找某一元素,必须先找到上一个元素,根据它提供的下一元素地址才能找到下一个元素。如果不提供“头指针”(head
),则整个链表都无法访问。链表如同一条铁链一样,一环扣一环,中间是不能断开的。
为了理解链表,可以打一个通俗的比方:幼儿园的老师带领孩子出来散步,老师牵着第1个小孩的手,第1个小孩的另一只手牵着第2个孩子……这就是一个“链”。最后一个孩子有一只手空着,他是“链尾”。要找这个队伍,必须先找到老师,然后顺序找到每一个孩子。
显然,链表这种数据结构,必须利用指针变量才能实现,即一个结点中应包含一个指针变量,用它存放下一结点的地址。
定义链表结构体
前面介绍了结构体变量,用它去建立链表是最合适的。一个结构体变量包含若干成员,这些成员可以是数值类型、字符类型、数组类型,也可以是指针类型。用指针类型成员来存放下一个结点的地址。例如,可以设计这样一个结构体类型:
struct Student {
int num;
float score;
struct Student *next; // next是指针变量,指向结构体变量
};
其中,成员 num
和 score
用来存放结点中的有用数据(用户需要用到的数据),相当于图中的结点 A
、B
、C
、D
。next
是指针类型的成员,它指向 struct Student
类型数据(就是 next
所在的结构体类型)。一个指针类型的成员既可以指向其他类型的结构体数据,也可以指向自己所在的结构体类型的数据。现在,next
是 struct Student
类型中的一个成员,它又指向 struct Student
类型的数据。用这种方法就可以建立链表。
链表结点示意图
+-------+-------+ +-------+-------+ +-------+-------+
| num | score | next | num | score | next | num | score | NULL
+-------+-------+------>+-------+-------+------>+-------+-------+
| 10101 | 89.5 | | 10103 | 90.0 | | 10107 | 85.0 |
+-------+-------+ +-------+-------+ +-------+-------+
每一个结点都属于 struct Student
类型,它的成员 next
用来存放下一结点的地址,程序设计人员可以不必知道各结点的具体地址,只要保证将下一个结点的地址放到前一结点的成员 next
中即可。
注意:上面只是定义了一个 struct Student
类型,并未实际分配存储空间,只有定义了变量才分配存储单元。
9.4.2 建立简单的静态链表
下面通过一个例子来说明怎样建立和输出一个简单链表。
示例 9.8:建立一个简单链表并输出其数据
解题思路
- 声明一个结构体类型,其成员包括
num
(学号)、score
(成绩)和next
(指针变量)。 - 将第1个结点的起始地址赋给头指针
head
。 - 将第2个结点的起始地址赋给第1个结点的
next
成员。 - 将第3个结点的起始地址赋给第2个结点的
next
成员。 - 第3个结点的
next
成员赋予NULL
。这就形成了链表。
编写程序
#include <stdio.h>
// 声明结构体类型struct Student
struct Student {
int num;
float score;
struct Student *next;
};
int main() {
// 定义3个结构体变量a, b, c作为链表的结点
struct Student a, b, c;
struct Student *head, *p;
// 对结点a的num和score成员赋值
a.num = 10101;
a.score = 89.5;
// 对结点b的num和score成员赋值
b.num = 10103;
b.score = 90;
// 对结点c的num和score成员赋值
c.num = 10107;
c.score = 85;
// 将结点a的起始地址赋给头指针head
head = &a;
// 将结点b的起始地址赋给a结点的next成员
a.next = &b;
// 将结点c的起始地址赋给b结点的next成员
b.next = &c;
// c结点的next成员不存放其他结点地址
c.next = NULL;
// 使p指向a结点
p = head;
// 遍历链表并输出各结点的数据
do {
printf("%d %.1f\n", p->num, p->score);
// 使p指向下一结点
p = p->next;
} while (p != NULL); // 输出完c结点后p的值为NULL,循环终止
return 0;
}
运行结果
10101 89.5
10103 90.0
10107 85.0
程序分析
- 构建链表:为了建立链表,使
head
指向a
结点,a.next
指向b
结点,b.next
指向c
结点,这就构成了链表关系。c.next = NULL
的作用是使c.next
不指向任何有用的存储单元。 - 输出链表:在输出链表时要借助
p
,先使p
指向a
结点,然后输出a
结点中的数据。p = p->next
是为输出下一个结点作准备。p->next
的值是b
结点的地址,因此执行p = p->next
后p
就指向b
结点,所以在下一次循环时输出的是b
结点中的数据。
本例是比较简单的,所有结点都是在程序中定义的,不是临时开辟的,也不能用完后释放,这种链表称为“静态链表”。
9.4.3 建立动态链表
所谓建立动态链表是指在程序执行过程中从无到有地建立起一个链表,即一个一个地开辟结点和输入各结点数据,并建立起前后相链的关系。
示例 9.9:写一函数建立一个有3名学生数据的单向动态链表
解题思路
先考虑实现此要求的算法。在用程序处理时要用到第8章介绍的动态内存分配的知识和有关函数(malloc
,calloc
,realloc
和 free
函数)。定义3个指针变量:head
、p1
和 p2
,它们都是用来指向 struct Student
类型数据的。先用 malloc
函数开辟第1个结点,并使 p1
和 p2
指向它。然后从键盘读入一个学生的数据给 p1
所指的第1个结点。
如果输入的 p1->num
不等于0,则输入的是第1个结点数据(n=1),令 head=p1
,即把 p1
的值赋给 head
,也就是使 head
也指向新开辟的结点。p1
所指向的新开辟的结点就成为链表中第1个结点。然后再开辟另一个结点并使 p1
指向它,接着输入该结点的数据。以下是该算法的流程图。
编写程序
#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct Student)
struct Student {
long num;
float score;
struct Student *next;
};
int n; // n为全局变量,本文件模块中各函数均可使用它
struct Student* create(void) {
struct Student *head;
struct Student *p1, *p2;
n = 0;
p1 = p2 = (struct Student*) malloc(LEN); // 开辟一个新单元
scanf("%ld, %f", &p1->num, &p1->score); // 输入第1个学生的学号和成绩
head = NULL;
while(p1->num != 0) {
n = n + 1;
if (n == 1)
head = p1;
else
p2->next = p1;
p2 = p1;
p1 = (struct Student*) malloc(LEN); // 开辟动态存储区,把起始地址赋给p1
scanf("%ld, %f", &p1->num, &p1->score); // 输入其他学生的学号和成绩
}
p2->next = NULL;
return head;
}
int main() {
struct Student *pt;
pt = create(); // 函数返回链表第一个结点的地址
// 输出链表中的所有结点
while (pt != NULL) {
printf("num: %ld\nscore: %.1f\n", pt->num, pt->score);
pt = pt->next;
}
return 0;
}
运行结果
10101, 89.5
10103, 90
10107, 85
0, 0
num: 10101
score: 89.5
num: 10103
score: 90.0
num: 10107
score: 85.0
程序分析
- 调用
create
函数后,先后输入所有学生的数据,若输入“0,0”,表示结束。函数的返回值是所建立的链表的第1个结点的地址(请查看return
语句),在主函数中把它赋给指针变量pt
。为了验证各结点中的数据,在main
函数中输出了第1个结点中的信息。 - 宏定义
LEN
代表struct Student
类型数据的长度,sizeof
是“求字节数运算符”。 - 定义
create
函数,它是指针类型,即此函数带回一个指针值,它指向一个struct Student
类型数据。实际上此create
函数带回一个链表起始地址。 malloc(LEN)
的作用 是开辟一个长度为LEN
的内存区,LEN
已定义为sizeof(struct Student)
,即结构体struct Student
的长度。malloc
带回的是不指向任何类型数据的指针(void*
类型)。而p1
和p2
是指向struct Student
类型数据的指针变量,可以用强制类型转换的方法使指针的基类型改变为struct Student
类型,在malloc(LEN)
之前加了“(struct Student *)
”,它的作用是使malloc
返回的指针转换为struct Student
类型数据的指针。注意括号中的“*
”号不可省略,否则变成转换成struct Student
类型了,而不是指针类型了。 由于编译系统能实现隐式的类型转换,因此p1 = malloc(LEN)
也可以直接写为p1 = malloc(LEN)
。create
函数最后一行return
后面的参数是head
(head
已定义为指针变量,指向struct Student
类型数据)。因此函数返回的是head
的值,也就是链表中第1个结点的起始地址。n
是结点个数。- 这个算法的思路 是让
p1
指向新开辟的结点,p2
指向链表中最后一个结点,把p1
所指的结点连接在p2
所指的结点后面,用“p2->next = p1
”来实现。
以上对建立链表过程做了比较详细的介绍。如果对建立链表的过程比较清楚的话,对链表的其他操作过程(如链表的输出、结点的删除和结点的插入等)也就比较容易理解了。
9.4.4 输出链表
将链表中各结点的数据依次输出,这个问题比较容易处理。
示例 9.10:编写一个输出链表的函数 print
解题思路
从示例 9.8 中已经初步了解输出链表的方法。首先要知道链表第1个结点的地址,也就是要知道 head
的值。然后设一个指针变量 p
:
p = head
,使p
指向第1个结点。- 输出
p
所指的结点。 - 使
p
后移一个结点,再输出,直到链表的尾结点。
编写程序
#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct Student)
// 声明结构体类型struct Student
struct Student {
long num;
float score;
struct Student *next;
};
int n; // 全局变量n
// 定义print函数
void print(struct Student *head) {
struct Student *p; // 在函数中定义struct Student类型的变量p
printf("\nNow, These %d records are:\n", n);
p = head; // 使p指向第1个结点
if (head != NULL) { // 若不是空表
do {
printf("%ld %5.1f\n", p->num, p->score); // 输出一个结点中的学号与成绩
p = p->next; // p指向下一个结点
} while (p != NULL); // 当p不是“空地址”
}
}
int main() {
struct Student *head;
head = create(); // 调用create函数,返回第1个结点的起始地址
print(head); // 调用print函数
return 0;
}
运行结果
10101, 89.5
10103, 90
10107, 85
0, 0
Now, These 3 records are:
10101 89.5
10103 90.0
10107 85.0
程序分析
print
函数接收链表的头指针head
作为参数,指向链表的第1个结点。- 使用指针
p
遍历链表,从第1个结点开始,依次输出每个结点的数据。 - 当
p
指向NULL
时,遍历结束。
可以把示例 9.7 和示例 9.9 合起来加上一个主函数,组成一个完整的程序:
完整程序
#include <stdio.h>
#include <stdlib.h>
#define LEN sizeof(struct Student)
// 声明结构体类型struct Student
struct Student {
long num;
float score;
struct Student *next;
};
int n; // 全局变量n
// 建立链表的函数
struct Student* create(void) {
struct Student *head;
struct Student *p1, *p2;
n = 0;
p1 = p2 = (struct Student*) malloc(LEN);
scanf("%ld, %f", &p1->num, &p1->score);
head = NULL;
while (p1->num != 0) {
n = n + 1;
if (n == 1)
head = p1;
else
p2->next = p1;
p2 = p1;
p1 = (struct Student*) malloc(LEN);
scanf("%ld, %f", &p1->num, &p1->score);
}
p2->next = NULL;
return head;
}
// 输出链表的函数
void print(struct Student *head) {
struct Student *p; // 在函数中定义struct Student类型的变量p
printf("\nNow, These %d records are:\n", n);
p = head; // 使p指向第1个结点
if (head != NULL) { // 若不是空表
do {
printf("%ld %5.1f\n", p->num, p->score); // 输出一个结点中的学号与成绩
p = p->next; // p指向下一个结点
} while (p != NULL); // 当p不是“空地址”
}
}
int main() {
struct Student *head;
head = create(); // 调用create函数,返回第1个结点的起始地址
print(head); // 调用print函数
return 0;
}
运行结果
10101, 89.5
10103, 90
10107, 85
0, 0
Now, These 3 records are:
10101 89.5
10103 90.0
10107 85.0
说明
链表是一个比较深入的内容,初学者有一定难度,计算机专业人员是应该掌握的,非专业的初学者对此有一定了解即可,在以后需要用到时再进一步学习。
对链表中结点的删除和结点的插入等操作,在此不作详细介绍,如读者有需要或感兴趣,可以自己完成。如果想详细了解,可参考相关书籍或资料,其中会有详细的说明和例子。