3. 链表
3.1 基本概念
数组的局限性:
插入数值时,需要数字依次往后挪一位。操作耽误时间。
![](https://i-blog.csdnimg.cn/blog_migrate/07d81600176e9b7d615305269c404e6c.png)
链表的优势:
插入数值,时间快很多。
![](https://i-blog.csdnimg.cn/blog_migrate/29b8c0838ed0c510fb4eba7f9ca9bcdf.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8f2b8f4a5ec000ffe5c25db78f2ed42d.png)
3.2 链表的实现
如何实现链表呢?
在 C 语言中可以使用指针和动态分配内存函数 malloc 来实现。
3.2.1 回顾指针
指针的定义
int a;
int *p;
第一行是定义一个整型变量 a。
第二行定义了一个整型指针变量 p。
指针的作用
存储一个地址。确切地说是存储一个内存空间的地址。
严格地说这里的指针 p 只能存储“一个存放整数的内存空间”的地址。
整型指针 p 如何才能存储整型变量 a 的地址呢? 如下:
p = &a;
&叫取地址符。这样整型指针 p 就获得了(存储了)整型变量 a 的地址。
可以形象地理解整型指针 p 指向了整型变量 a。
p 指向了 a 之后,有什么用呢?
用处就是可以用指针 p 来操作变量 a 了。比如,可以通过操作指针 p 来输出变量 a 的值,如下:
#include <stdio.h>
int main() {
int a=10;
int *p;//定义个指针p
p = &a;//指针p获取变量a的地址
printf("%d", *p);//输出指针p所指向的内存中的值
getchar();getchar();
return 0;
}
其中, ∗ p *p ∗p 中的*号叫做间接运算符,作用是取得指针 p 所指向的内存中的值。
在 C 语言中*号有三个用途,分别是:
- 乘号,用做乘法运算,例如 5*6。
- 申明一个指针,在定义指针变量时使用,例如 int *p;。
- 间接运算符,取得指针所指向的内存中的值,例如 printf("%d",*p);。
在程序中存储一个整数10,除了使用int a;这种方式在内存中申请一块区域来存储,还有另外一种动态存储方法:
malloc(4);
malloc函数的作用就是从内存中申请分配指定字节大小的内存空间。
上面这行代码就申请了 4 个字节。如果不知道 int 类型是 4 个字节的,还可以使用 sizeof(int)获取 int 类型所占用的字节数,如下:
malloc(sizeof(int));
已经成功地从内存中申请了 4 个字节的空间来准备存放一个整数,可是如何来对这个空间进行操作呢?
这里就需要用一个指针来指向这个空间,即存储这个空间的首地址。
int *p;
(int *)malloc(sizeof(int));
需要注意,malloc 函数的返回类型是 void * 类型。void * 表示未确定类型的指针。在 C 和 C++中,void * 类型可以强制转换为任何其他类型的指针。
上面代码中我们将其强制转化为整型指针,以便告诉计算机这里的 4 个字节作为一个整体用来存放整数。
指针就是用来存储内存地址的,为什么要分不同类型的指针呢?
因为指针变量存储的是一个内存空间的首地址(第一个字节的地址),但是这个空间占用了多少个字节,用来存储什么类型的数,则是由指针的类型来标明的。这样系统才知道应该取多少个连续内存作为一个数据。
可以通过指针 p 对刚才申请的 4 个字节的空间进行操作了,例如向这个空间中存入整数 10,如下:
*p = 10;
完整代码如下:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p;//定义一个指针p
p = (int *) malloc(sizeof(int));//指针p获取动态分配的内存空间地址
*p=10;//向指针p所指向的内存空间中存入10
printf("%d", *p);//输出指针p所指向的内存中的值
getchar();getchar();
return 0;
}
为什么要用这么复杂的办法来存储数据呢?
因为之前的方法,必须预先准确地知道所需变量的个数,也就是说必须定义出所有的变量。
有了 malloc 函数,便可以在程序运行的过程中根据实际情况来申请空间。
3.2.2 链表的结点
每一个结点都由两个部分组成。左边的部分用来存放具体的数值,那么用一个整型变量就可以;
右边的部分需要存储下一个结点的地址,可以用指针来实现(也称为后继指针)。
这里定义一个结构体类型来存储这个结点,如下:
struct node {
int data;
struct node *next;
}
![](https://i-blog.csdnimg.cn/blog_migrate/acb7644f7fa8f8a572acdf5e5c14a010.png)
上面代码中,定义了一个叫做 node 的结构体类型,这个结构体类型有两个成员。
第一个成员是整型 data,用来存储具体的数值;
第二个成员是一个指针,用来存储下一个结点的地址。因为下一个结点的类型也是 struct node,所以这个指针的类型也必须是 struct node * 类型的指针。
如何建立链表呢?
首先需要一个头指针 head 指向链表的最开始。当链表还没有建立的时候头指针 head 为空(也可以理解为指向空结点)。
struct node *head;
head = NULL;//头指针初始为空
创建第一个结点,并用临时指针 p 指向这个结点。
struct node *p;
//动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
p = (struct node *)malloc(sizeof(struct node));
分别设置新创建的这个结点的左半部分和右半部分:
scanf("%d", &a);
p->data = a;
p->next = NULL;//设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空
![](https://i-blog.csdnimg.cn/blog_migrate/d1d793a33604f4b225c4baae9243e4e1.png)
其中,符号“->” 叫做结构体指针运算符,也是用来访问结构体内部成员的。
因为此处 p 是一个指针,所以不能使用.号访问内部成员,而要使用->。
下面来设置头指针并设置新创建结点的*next 指向空。头指针的作用是方便以后从头遍历整个链表:
if (head == NULL) {
head = p;//如果这是第一个创建的结点,则将头指针指向这个结点
} else {
q->next = p;//如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
}
如果这是第一个创建的结点,则将头指针指向这个结点。
![](https://i-blog.csdnimg.cn/blog_migrate/c0fee1a10875159029a8eee53538ba15.png)
如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点。
![](https://i-blog.csdnimg.cn/blog_migrate/91e3deb0524c339b8f0a92f97dc665db.png)
最后要将指针 q 也指向当前结点,因为待会儿临时指针 p 将会指向新创建的结点。
q=p;
完整代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//这里创建一个结构体用来表示链表的结点类型
struct node {
int data;
struct node *next;
};
int main() {
struct node *head, *p, *q, *t;
int i, n, a;
scanf("%d", &n);
head = NULL;//头指针初始为空
for (i=1; i<=n; i++) //循环读入n个数
{
scanf("%d", &a);
//动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
p = (struct node *)malloc(sizeof(struct node));
p->data = a;//将数据存储到当前结点的data域中
p->next = NULL;//设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空
if (head==NULL) {
head = p;//如果这是第一个创建的结点,则将头指针指向这个结点
} else {
q->next = p;//如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
}
q = p;//指针q也指向当前结点???
}
t=head;
while (t!=NULL) {
printf("%d\n", t->data);
t = t->next;//继续下一个结点
}
getchar();getchar();
return 0;
}
输入数据:
9
2 3 5 8 9 10 18 26 32
返回值:
2 3 5 8 9 10 18 26 32
接下来需要往链表中插入 6,操作如下:
![](https://i-blog.csdnimg.cn/blog_migrate/53a0574a9352c0da62e9b31ef1d0dece.png)
首先用一个临时指针 t 从链表的头部开始遍历。
t=head;//从链表头部开始遍历
等到指针 t 的下一个结点的值比 6 大的时候,将 6 插入到中间。即 t->next->data 大于 6 时进行插入,代码如下:
scanf("%d", &a);//读入待插入的数
while (t!=NULL) //当没有到达链表尾部的时候循环
{
if (t->next->data > a) //当没有到达链表尾部的时候循环
{
p = (struct node *)malloc(sizeof(struct node));//动态申请一个空间,用来存放新增结点
p->data = a;
p->next = t->next;//新增结点的后继指针指向当前结点的后继指针所指向的结点
t->next = p;//当前结点的后继指针指向新增结点
break;//插入完毕退出循环
}
t = t->next;
}
完整代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//这里创建一个结构体用来表示链表的结点类型
struct node {
int data;
struct node *next;
};
int main() {
struct node *head, *p, *q, *t;
int i, n, a;
scanf("%d", &n);
head = NULL;//头指针初始为空
for (i=1; i<=n; i++) //循环读入n个数
{
scanf("%d", &a);
//动态申请一个空间,用来存放一个结点,并用临时指针p指向这个结点
p = (struct node *)malloc(sizeof(struct node));
p->data = a;//将数据存储到当前结点的data域中
p->next = NULL;//设置当前结点的后继指针指向空,也就是当前结点的下一个结点为空
if (head==NULL) {
head = p;//如果这是第一个创建的结点,则将头指针指向这个结点
} else {
q->next = p;//如果不是第一个创建的结点,则将上一个结点的后继指针指向当前结点
}
q = p;//指针q也指向当前结点???
}
scanf("%d", &a);//读入待插入的数
t = head;//从链表头部开始遍历
while (t!=NULL) //当没有到达链表尾部的时候循环
{
if (t->next->data > a) //当没有到达链表尾部的时候循环
{
p = (struct node *)malloc(sizeof(struct node));//动态申请一个空间,用来存放新增结点
p->data = a;
p->next = t->next;//新增结点的后继指针指向当前结点的后继指针所指向的结点
t->next = p;//当前结点的后继指针指向新增结点
break;//插入完毕退出循环
}
t = t->next;//继续下一个结点
}
//输出链表中的所有数
t=head;
while (t!=NULL) {
printf("%d\n", t->data);
t = t->next;//继续下一个结点
}
getchar();getchar();
return 0;
}
输入数据:
9
2 3 5 8 9 10 18 26 32
6
返回值:
2 3 5 6 8 9 10 18 26 32
参考
《啊哈!算法》 —— 第2章 栈、队列、链表