前面研究了顺序表示下的线性表, 接下来一起来看一下链式存储结构下的线性表--链表
链表
链表是线性表的一种链式存储结构, 与顺序存储不同, 链表是 用一组任意的存储单元进行数据元素的存储, 这些存储单元可以是连续的, 也可以是不连续的。为了表示每个数据元素与其直接后继数据元素的逻辑关系, 链表除了存储本身的信息之外, 还需要存储其直接后继的存储位置信息。这两部分信息组成一个 "结点"。

如上图, 存储数据的部分叫做 数据域 , 存储下一个结点地址的部分称为 指针域 , 这一系列的结点通过 指针域 所存储的地址信息进行逻辑关系相连, 这就组成了我们今天要研究的 链表 。关于链表将其分为 单向链表 和 双向链表 两部分来研究, 下面先来看一下 单向链表。
单向链表
特点

根据上面的单向链表的结构图可以总结出以下特点:
- 方向是单向的, 对链表的访问需要从头部开始按顺序读取
- 物理结构不相邻, 需要借助 指针域的信息才能访问 后继结点
- 因为是单向的, 只能访问后继结点, 不能反向访问
- 在进行 插入/删除 操作时, 需要修改涉及到的结点的前驱和后继
- 每次插入操作时动态申请内存, 不需要考虑存储上限
- 删除操作时需要释放结点内存, 防止内存泄漏
关于头结点
头结点是我们在链表的第一个元素结点(首元结点) 之前设置的一个结点, 他的 数据域可以不存储任何信息, 指针域指向链表的首元结点。添加头结点的目的是为了减少在对链表进行 添加/删除 时遇到的特殊情况的处理, 降低程序的复杂性。(有了头结点以后, 在 删除/添加 时就不用再去考虑首元结点与首指针之间的关系了)
注意: 添加了头结点的链表在进行销毁时需要对头结点进行销毁操作。
设计
结构设计
单向链表的结构与顺序表的区别在于, 除去 数据域 之外单向链表还需要引入 指针域 , 所以其结构设计如下:
/* ElemType类型根据实际情况而定,这里假设为int */
typedef int ElementType;
typedef struct Node{
ElementType data;
struct Node *next;
}Node;
typedef struct Node * LinkList;
初始化
int initLinkList(LinkList *L) {
// 头结点分配空间, 并让首指针指向头结点
*L = (LinkList)malloc(sizeof(Node));
// 存储空间分配失败, 抛出异常
if (*L == NULL) exit(0);
// 头结点的指针域置空
(*L)->next = NULL;
return OK;
}
插入
插入需要考虑两种情况, 一种是需要插入特定位置时, 另一种是不需要插入特定位置, 那么就需要制定一个插入规则, 常用的规则主要有 前插法 和 后插法。
插入特定位置

算法步骤:
- 遍历链表, 找到所要插入位置的前一个位置的结点, 定义一个局部变量接收一下
- 判断找到的结点存不存在
- 创建新节点, 并将需要插入的值赋值给新节点的数据域
- 将找到的结点的后继赋值给新节点的后继
- 将新节点赋值给找到的结点的后继
注意: 这里的顺序是不能改变的, 为了防止链表断裂, 新的结点C必须先于结点A的后继结点B建立联系, 因为A结点是我们已经找到的, 所以之后再将A与C建立联系。
int insertLinkList(LinkList *L, int i, ElementType e) {
int j = 1;
LinkList temp, addNode;
temp = *L;
// 遍历寻找 i 的上一个结点
while (temp && j < i) {
temp = temp->next;
j++;
}
// 判断结点是否存在
if (!temp || j > i) return ERROR;
// 创建用来插入的新结点
addNode = (LinkList)malloc(sizeof(Node));
// 把想要插入的值给新的结点
addNode->data = e;
// 新的结点的后继指向 temp 的后继
addNode->next = temp->next;
// temp 的后继指向新节点
temp->next = addNode;
return OK;
}
插入非特定位置
- 前插法

如上图, 前插法就像其名字一样, 每次在首元结点之前做插入, 这样只需要拿到头结点, 然后在头结点的后面做插入就可以了
算法如下:
- 生成新的结点, 将需要插入的值赋给 data
- 将新节点的 next 指向头结点的 next
- 将头结点的 next 指向新的结点
int headInsertLinkList(LinkList *L, ElementType e) {
// 创建新的结点
LinkList p = (LinkList)malloc(sizeof(Node));
p->data = e;
// 结点p 指向 头结点的后继 (也就是首元结点)
p->next = (*L)->next;
// 头结点 指向 结点p (取代之前的首元结点)
(*L)->next = p;
return OK;
}
- 后插法

后插法就是在链表的尾部做插入操作, 这种操作就需要找到链表的尾结点, 然后再把新的结点放到尾结点的后面就可以了
算法如下:
- 创建新的结点, 将新节点的
next
指向NULL
, 因为尾结点是没有后继的 - 通过一个
while
循环找到尾结点 - 将尾结点的
next
指向 新的结点
int tailInsertLinkList(LinkList *L, ElementType e) {
// 创建新的结点
LinkList p = (LinkList)malloc(sizeof(Node));
p->data = e;
p->next = NULL;
// 建立一个尾结点 tailNode, 通过while循环遍历到尾部找到尾结点
LinkList tailNode = (*L)->next;
while (tailNode->next) {
tailNode = tailNode->next;
}
// 将新的结点放到尾结点之后
tailNode->next = p;
return OK;
}
取值
由于链表在内存中不是连续的, 所以不能通过下标直接获取结点, 需要通过当前结点的指针域找到下一个结点。这里在取值时我们顶一个一个临时结点, 用来保存每一步循环之后的结点信息, 然后循环到想要取值的位置时, 直接读取这个临时结点的值就可以了。
代码如下:
int getElem(LinkList *L, int i, ElementType *e) {
// 创建临时结点 p
LinkList p;
// 让 p 指向首元结点
p = (*L)->next;
// 循环到要取结点的位置 i
int j = 1;
while (j < i && p) {
p = p->next;
++j;
}
// 判断如果 p 不存在直接返回error
// 如果 j > i, 说明 传入的 i是小于1的值, 不合法直接返回error
if (!p || j > i) return ERROR;
// 赋值操作
*e = p->data;
return OK;
}
// 打印
int printLinkList(LinkList L) {
LinkList p;
p = L->next;
while (p) {
printf("%d\t", p->data);
p = p->next;
}
printf("\n");
return OK;
}
删除

删除结点如上图, 我们要把 A, B, C 三个结点的 B 结点移除出去, 首先需要找到 A 结点, 也就是 B 的前驱, 然后定义一个 临时结点来指向 B (注意在将B移除以后还需要对这块内存进行释放 ), 然后就是将 A 的后继指向 临时结点的后继就可以了, 最后就是需要把 B 的空间进行释放。
算法实现:
- 判断传入的删除位置是否合法
- 定义一个临时结点p, 用来保存当前遍历到的结点, 然后进行循环遍历, 找到要删结点的前驱结点
- 遍历结束以后需要看一下结点 p 的后继是否存在, 因为我们要删除的就是 p 的后继
- 定义 temp 结点指向要删除的结点 (也就是 p->next), 让 p 结点的后继指向 要删结点的后继, 这样 需要删除的结点就被分离出去了
- 最后需要把 temp 结点释放掉
int deletElem(LinkList *L, int i, ElementType *e) {
// 删除位置必须从1开始
if (i < 1) return ERROR;
// 定义两个临时结点
LinkList p, temp;
p = *L;
// 找到要删结点 i 位置的前驱, (注意这里是 j < i)
for (int j = 1; j < i && p->next; j++) {
p = p->next;
}
// 判断 p 结点的后继结点(也就是要删除的结点) 存不存在, 不存在的话就没有删除的必要了
if (!p->next) return ERROR;
// 用我们的临时结点 temp 去接收 要删的结点
// 将要删结点的前驱 p 的后继指向 要删结点的后继
temp = p->next;
p->next = temp->next;
*e = temp->data;
// 释放内存
free(temp);
return OK;
}
调用
int main(int argc, const char * argv[]) {
// insert code here...
printf("初始化\n");
// 创建
LinkList p;
initLinkList(&p);
printLinkList(p);
printf("插入\n");
// 插入
insertLinkList(&p, 1, 10);
insertLinkList(&p, 0, 20);
printLinkList(p);
insertLinkList(&p, 1, 30);
insertLinkList(&p, 44, 25);
printLinkList(p);
printf("前插法\n");
// 前插法
headInsertLinkList(&p, 99);
headInsertLinkList(&p, 22);
printLinkList(p);
printf("后插法\n");
// 后插法
tailInsertLinkList(&p, 88);
tailInsertLinkList(&p, 55);
printLinkList(p);
printf("取值\n");
// 取值
ElementType a;
getElem(&p, 2, &a);
printf("a = %d\n", a);
ElementType b;
getElem(&p, 99, &b);
printf("b = %d\n", b);
printf("删除\n");
// 删除
ElementType c;
deletElem(&p, 99, &c);
printf("c = %d\n", c);
ElementType d;
deletElem(&p, 4, &d);
printf("d = %d\n", d);
printLinkList(p);
return 0;
}
// 输出结果
初始化
插入
10
30 10
前插法
22 99 30 10
后插法
22 99 30 10 88 55
取值
a = 99
b = 0
删除
c = 0
d = 10
22 99 30 88 55
Program ended with exit code: 0
单向循环链表

如上图, 单向循环链表与单向链表的不同点在于, 单向循环链表的最后一个节点的指针指向链表头部, 而不是指向NULL, 即单向循环链表的尾结点是指向头结点的 (空表的时候就是头结点指向本身) 。
设计
注: 因为大部分的算法步骤跟单向链表是相似的, 所以这里不再描述具体步骤, 只在需要注意的地方添加说明。
结构设计
/* ElemType类型根据实际情况而定,这里假设为int */
typedef int ElementType;
typedef struct Node{
ElementType data;
struct Node *next;
}Node;
typedef struct Node * LinkList;
只是尾结点的指向不同, 所以在结构设计上单向链表和单向循环链表是一致的。
初始化
// 初始化
int initLinkList(LinkList *L) {
// 创建头结点
*L = (LinkList)malloc(sizeof(Node));
// 判断是否创建成功
if (*L == NULL) exit(0);
// 给数据域随便赋值, 将头结点的 next 指向本身
(*L)->data = -1;
(*L)->next = (*L);
return OK;
}
初始化时跟单向链表的不同点在于, 在最后需要将头结点的 next 指向其自己
插入
插入特定位置
// 插入
int insertLinkList(LinkList *L, int i, ElementType e) {
// 创建遍历记录结点和待插入的新节点
LinkList p, temp;
temp = (*L);
// 创建结点
p = (LinkList)malloc(sizeof(Node));
p->data = e;
// 通过 while 循环找到待插入位置的上一个结点
// (temp->next != (*L) 用来判断 temp 是否是尾结点
int j = 1;
while (j < i && (temp->next != (*L))) {
temp = temp->next;
++j;
}
// 对 i 的合理性判断, 如果 j > i, i应该是 <1 的值, 不满足
// 如果 i > j+1, 说明 i-1 > j, 即 j 不是 i 的前一个结点 (当 i 的值超出链表的长度 +1 时出现)
if (j > i || (i > j+1)) return ERROR;
// 插入操作, 将新节点的 next 指向 temp 的next
// 将 temp 的 next 指向 新节点p
p->next = temp->next;
temp->next = p;
return OK;
}
插入时需要注意的点在于 循环的判断条件, 因为循环链表是收尾相连的, 所以判断条件应该是 当前结点的 next 指向头结点
插入非特定位置
- 前插法
// 前插法
int headInsertLinkList(LinkList *L, ElementType e) {
LinkList p;
// 创建结点 | 判断是否创建成功
p = (LinkList)malloc(sizeof(Node));
if (!p) return ERROR;
p->data = e;
// 新节点的 next 指向 头结点的后继(非空表时是首元结点, 空表时是头结点)
p->next = (*L)->next;
// 头结点的 next 指向 新节点
(*L)->next = p;
return OK;
}
- 后插法
// 后插法
int tailInsertLinkList(LinkList *L, ElementType e) {
LinkList p, temp;
// 创建结点 | 判断是否创建成功
p = (LinkList)malloc(sizeof(Node));
p->data = e;
if (!p) return ERROR;
// 循环找到尾结点
temp = *L;
do {
temp = temp->next;
} while (temp->next != *L);
// 插入操作
p->next = temp->next;
temp->next = p;
return OK;
}
删除
// 删除
int deletElem(LinkList *L, int i, ElementType *e) {
// 空表
if ((*L)->next == (*L)) return ERROR;
//
LinkList p, temp;
p = (*L);
// 循环找到要删除结点的上一个结点
int j = 1;
while (j < i && (p->next != *L)) {
p = p->next;
j++;
}
// 判断
// j != i, 循环提前因为 p->next == *L 跳出, 给的 i 值超过链表长度; 或者 i 值 小于1 造成
// p->next == *L, 要删除结点的位置刚好在 尾结点的后面时产生, 如果不加这个判断就会把头结点干掉了
if (j != i || (p->next == *L)) return ERROR;
// 用定义的 temp接管要删除的结点, 然后将其从链表中断开
temp = p->next;
p->next = temp->next;
// 将删除加点的内容给e, 然后释放掉
*e = temp->data;
free(temp);
return OK;
}
删除时基本与单链表类似, 不同点在于对于要删除位置的判断, 然后在删除过后对结点进行释放。
查找
// 查找
int getElem(LinkList *L, int i, ElementType *e) {
LinkList p;
p = (*L);
// 循环找到查找的结点
int j = 1;
while (j <= i && (p->next != *L)) {
p = p->next;
j++;
};
// 对 i 值的合理性判断
if (j-1 != i) return ERROR;
// 赋值
*e = p->data;
return OK;
}
// 打印
int printLinkList(LinkList L) {
LinkList p;
p = L;
do {
p = p->next;
printf("%d\t", p->data);
} while (p->next != L);
printf("\n");
return OK;
}
调用
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
printf("初始化---------------\n");
LinkList L;
initLinkList(&L);
printf("插入---------------\n");
insertLinkList(&L, 1, 10);
printLinkList(L);
insertLinkList(&L, 2, 20);
printLinkList(L);
insertLinkList(&L, 1, 30);
printLinkList(L);
insertLinkList(&L, 4, 40);
printLinkList(L);
printf("头插法---------------\n");
headInsertLinkList(&L, 12);
printLinkList(L);
headInsertLinkList(&L, 13);
printLinkList(L);
printf("尾插法---------------\n");
tailInsertLinkList(&L, 22);
printLinkList(L);
tailInsertLinkList(&L, 23);
printLinkList(L);
printf("删除---------------\n");
ElementType a;
deletElem(&L, 1, &a);
printLinkList(L);
printf("删除了 %d\n", a);
ElementType b;
deletElem(&L, 5, &b);
printLinkList(L);
printf("删除了 %d\n", b);
printf("查找---------------\n");
ElementType c;
getElem(&L, 1, &c);
printf("找到了 %d\n", c);
ElementType d;
getElem(&L, 4, &d);
printf("找到了 %d\n", d);
return 0;
}
// 打印结果
Hello, World!
初始化---------------
插入---------------
10
10 20
30 10 20
30 10 20 40
头插法---------------
12 30 10 20 40
13 12 30 10 20 40
尾插法---------------
13 12 30 10 20 40 22
13 12 30 10 20 40 22 23
删除---------------
12 30 10 20 40 22 23
删除了 13
12 30 10 20 22 23
删除了 40
查找---------------
找到了 12
找到了 20
Program ended with exit code: 0
总结
本次内容主要涉及了 链表 的相关概念, 以及 单向链表
和 单向循环链表
的结构和基本操作的算法设计。如果有不足的地方或者不正确的地方可以在回复中留言, 本人看到以后一定积极改正。
和谐学习, 不急不躁~