链表

算法

本文将兼述数据结构这门课中链表的侧重点以及算法实战中链表的侧重点。

什么是链表?

链表是一种用于存储数据的数据结构,通过像链条一般的指针连接元素(得名原因)。它是线性表的链式存储映像,称为线性表的链式存储结构

它的显著优势是删除和插入数据十分方便(数组要O(n),链表只需O(1)),但寻找和读取数据表现欠佳(数组只需O(1),链表需要O(n))。

链表的组成分类

链表由一系列节点(node)组成,每个节点(同结点)有两个部分:

  • 数据域:存储实际数据,(如int value)
  • 指针域: 存储下一个节点的地址(如 node *next)

链表分为,单链表,双链表,循环链表。

单链表

结点只有一个指针域,称为单链表或者线性链表。由表头唯一确定,所以可以用头指针的名字来命名。若头指针是L,则链表命名为L

image-20251110112047267

附上c++代码便于理解,但是后面会用到比较常见的typedef写法,所以这里的代码只是辅助理解,简洁为主。

注意:后文列举代码默认已定义Node结构体

1
2
3
4
5
struct Node {
int value;
Node *next;
};//比较好理解的写法,但是typedef更常用

双链表

有两个指针域(有左右之分)的链表,称为双链表

image-20251110112101610

1
2
3
4
5
struct Node {
int value;
Node *left;
Node *right;
};

循环链表

首尾相接的链表是循环列表。(尾节点的 next 指向头节点)由于这个特性,在插入数据时需要判断链表是否为空,为空则自身循环,不空则正常插入数据。(从循环链表中的任何一个节点都可以找到其他任何节点,而单链表不行)

image-20251110135816396

1
2
3
4
5
6
7
8
9
10
11
12
void insertNode(int i, Node *p) {
Node *node = new Node;//创建新节点
node->value = i;//设置节点值
node->next = NULL;//初始化next指针
if (p == NULL) {//链表为空
p = node;//p指向新节点
node->next = node;//自己指向自己
} else {
node->next = p->next;//新节点指向p的后继
p->next = node;//p指向新节点
}
}

链表常见运算符和关键字

初见代码你也许疑惑,new是什么?

它是c++中的关键字,用于动态分配内存。

例如

1
2
int *p=new int //分配一个int 大小的内存
int *p=new int (100)//初始值为100

->则是c/c++中的箭头运算符,等价于,先解引用指针,然后访问成员。

1
2
3
4
5
int main(){
Node* node=new Node;
node->value=5;//设置节点的value为5,等价于(*node).value=5
node->next=NULL;//设置Next指针为NULL,等价于 (*node).next=NULL
}

链表的一些概念

我们还提到了头结点的概念,其实与之对应的还有尾结点,头指针,首元结点。

  • 头指针:指向链表中第一个元素的指针
  • 首元结点:链表中存储第一个数据元素a1的结点
  • 头结点:是在链表的首元结点之前附设的一个结点,**数据域内可以为空,**只存放表长和空表标志等信息。但此结点不能计入链表长度值

按有无头结点划分

image-20251110142744415

有头结点时,当当头结点的指针域为空时表示空表。

为什么要设置头结点?

  • 便于首元结点的处理。

    **首元结点的地址保存在头结点的指针域中,**所以在链表的第一个位置上的操作和其他位置一致,无需进行特殊处理。

  • 便于空表和非空表的统一

    无论链表是否为空,头指针都是指向头结点的非空指针

链表的特点

  • 结点在存储器中的位置是任意的,逻辑相邻的数据元素在物理上不一定相邻

  • 访问时只能通过头指针进入链表,所以寻找第一个结点和最后一个结点的所需时间不等。

这种存取元素的方法称为顺序存取法

单向链表的操作

  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
21
22
#ifndef DULINKEDLIST_H
#define DULINKEDLIST_H

// 状态码定义
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2

// 状态类型定义
typedef int Status;
typedef int ElemType; // 可根据需要修改元素类型
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList//*LinkList为Lnode类型的指针
//指针变量p表示结点地址
//结点变量*p表示一个结点
//注意:struct LNode结构体类型有了一个别名 LNode。
//struct LNode *指针类型有了一个别名 LinkList。

取值

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
//获取链表中第i个元素的值
int GetElem(LinkList L,int i,int *e){
LNode *p=L->next; //p指向第一个结点(跳过头结点) 等价于LinkList p=L->next
int j=1;//计数器

//遍历链表直到第i个节点或链表结束,p不为NULL时继续循环
while(p && j<i){
p=p->next;
j++;
}

if(!p || j>i){
return 0;
}

*e=p->data;//通过指针参数返回元素值
return 1;
}

使用示例
LinkList list;//初始化的链表
int value;
if(GetElem(list,3,&value)){
printf("第三个元素是:%d\n",value);
}
else{
printf("第三个元素不存在\n");
}

查找

1
2
3
4
5
6
7
8
9
10
11
12
//在链表中查询值为e的节点
LNode *LocateElem(LinkList L,int e){
LNode *p = L->next;//从第一个节点开始

while(p!=NULL){
if(p->data==e){
return p;//找到,返回节点地址
}
p=p->next//没找到就下一个
}
return NULL//最终未找到
}

求表长

1
2
3
4
5
6
7
8
9
10
int ListLength_L(LinkList L){
LinkList p;
p=L->next;//p指向第一个结点
i=0;
while(p){
i++;
p=p->next;
}
return i;
}

判断是否为空

1
2
3
4
5
6
7
8
9
int ListEmpty(LinkList L){
//若L为空表,则返回1,否则返回0
if(L->next){//非空
return 0;
}
else{
return 1;
}
}

前插法建单链表

从一个空表开始重复读入数据

  • 生成新结点
  • 将读入数据放到新节点
  • 将该新节点插入到链表
1
2
3
4
5
6
7
8
9
10
void CreateList_F(LinkList &L,int n){
L=new LNode;
L->next=NULL;//先建立一个带头结点的空链表
for(int i=n;i>0;i--){
p=new LNode;//生成新节点
cin >>p->data;//输入元素值
p->next=L->next;
L->next=p;//插入到表头
}
}

image-20251111083610212

尾插法建单链表

1
2
3
4
5
6
7
8
9
10
11
12
void CreateList_L(LinkList &L,int n){
L=new LNode;
L->next=NULL;
r=L;//尾指针r指向头结点
for(int i=0;i<n;i++){
p=new LNode;
cin >>p->data;
p->next=NULL;//将新创建节点p的指针域设置为NULL,表示它是链表的最后一个节点。
r->next=p;//将新节点链接到链表尾部
r=p;//r指向新的尾结点
}
}

插入(指定位置插入新节点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//在第i个位置插入新元素e
int ListInsert(LinkList L,int i,int e){
LNode *p=L; //p指向头结点
int j=0;

//寻找着第i-1个节点
while(p && j<i-1){
p=p->next;
j++;
}
if(!p || j>i-1){
return 0;
}

//创建新节点
LNode *newNode = (LNode)malloc(sizeof(LNode));
newNode->data=e;

//插入操作
newNode->next=p->next;
p->next = newNode;

return 1;
}

删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//删除第i个节点,通过e返回其值
int ListDelete(LinkList L,int i,int *e){
LNode *p=L;
int j=0;

//寻找第i-1个节点,p的下一个节点存在时继续循环
while(p->next && j<i-1){
p=p->next;
j++;
}

if(!(p->next) || j>i-1){
return 0;
}

LNode *q=p->next;//q指向要删除的节点
*e=q->data;//保存被删除节点的值
p->next=q->next;//绕过要删除的节点
free(q);//释放内存
return 1;

}

[!NOTE]

关于p->next = q->next的绕过原理

假设一个链表 头节点-> A->B->C->NULL

p指向节点A,则p->next指向节点B,即q。

q->next指向c。

p->next=q->next就是直接让A指向C

单向循环链表的操作及约瑟夫环

初始化

1
2
3
4
5
6
7
8
9
10
11
// 通常在头文件中这样定义
#define OK 1
#define ERROR 0
#define OVERFLOW -1
typedef int Status; // 将Status定义为int类型
Status InitList_CL(LinkList &L){
L=new LNode;//生成头结点
if(!L) exit(OVERLOW);//内存分配失败
L->next=L;//头结点的next指向自己,形成空环
return OK;
}

判断是否为空

1
2
3
4
int ListEmpty_CL(LinkList L){
//判断头结点的next是否指向自己
return (L->next==L);
}

结点p之前插入新结点s

1
2
3
4
5
6
7
8
9
10
11
Status ListInsert_CL(LinkList &L,LNode *p,EleType e){
LNode *s=new LNode;
if(!s)return ERROR;
s->data=e;

s->next=p->next;//新结点s指向p的后继
p->next=s;//结点p指向新结点s

//如果p是尾结点,那么s就成为新的尾结点,其next头指向头结点L
return OK;
}

删除点p的后继节点

1
2
3
4
5
6
7
8
9
10
Status ListDelete_CL(LinkList &L,LNode *p,EleType &e){
if(p->next==L)return ERRER;
//p是尾结点,无后续可删

LNode *q=p->next;//q指向要删除的结点
e=q->data;//保存被删除的结点位置
p->next=q->next;//绕过要删除的结点
delete q;//释放内存
return OK;
}

合并链表

Ta是链表a的尾指针,Ta->next指向链表a的头结点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LinkList Connect(LinkList Ta,Linklist Tb){
//假设ta,tb都是非空单循环链表
p=Ta->next//p存表头结点
Ta->next = Tb ->next->next;//tb表头连接Ta表尾
delete Tb->next;//释放tb表头结点,现在链表a的尾直接指向了链表b的首元结点,链表b原来的头结点(Tb->next)已经不再被需要。
Tb->next=p;//修改指针
return Tb;
/*现在,整个链表的尾部是链表b原来的尾节点(即 Tb指向的节点)。
要让新链表成为循环链表,需要让尾节点的 next指针指向头结点。
p保存的正是链表a原来的头结点,现在它将成为合并后新链表的头结点。
这行代码让链表b的尾节点(Tb)的 next指针指向这个头结点,从而闭合整个链表,形成一个新的循环链表。
合并完成后,Tb指向的是新链表的尾节点。
函数返回 Tb,即返回新循环链表的尾指针。
*/
}

约瑟夫环

image-20251110212825259

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
void Josephus(int n, int m) {
// 创建包含n个结点的循环链表
LinkList L, current, prev;
InitList_CL(L); // 初始化循环链表

// 构建约瑟夫环
for(int i = 1; i <= n; i++) {
ListInsert_CL(L, i, i); // 第i个人的编号为i
}

current = L->next; // 从第一个人开始
prev = L;

while(n > 1) {
// 数到第m个人
for(int count = 1; count < m; count++) {
prev = current;
current = current->next;
// 如果遇到头结点,跳过
if(current == L) {
prev = current;
current = current->next;
}
}

// 删除当前结点(淘汰这个人)
printf("出列的人是:%d\n", current->data);
prev->next = current->next;
delete current;
current = prev->next;
n--;

// 如果遇到头结点,跳过
if(current == L) {
current = current->next;
}
}

printf("最后剩下的人是:%d\n", current->data);
}

双向链表

定义初始化

1
2
3
4
5
6
typedef struct DuLNode{
EleType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode,*DuLLinkList

插入

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
// 函数功能:在带头结点的双向链表L中,第i个位置之前插入新元素e
// 参数说明:L-双向链表头指针(引用传递),i-插入位置,e-待插入元素
// 返回值:操作状态(OK-成功,ERROR-失败)
Status ListInsert_DuL(DuLinkList &L, int i, ElemType e) {

// 步骤1:定位插入位置,获取第i个结点的指针p
// 注意:这里是在第i个结点"之前"插入,所以p指向的是目标位置的后继结点
DuLNode *p;
if(!(p = GetElemP_DuL(L, i))) // 调用查找函数获取第i个结点的地址
return ERROR; // 如果第i个结点不存在,返回错误

// 步骤2:创建新结点并赋值
DuLNode *s = new DuLNode; // 动态分配新结点内存
s->data = e; // 将数据e存入新结点的数据域

// 步骤3:核心操作 - 四步指针修改,将新结点s插入到p结点之前
// 这四步操作必须按顺序执行,确保链表不断链

s->prior = p->prior; // 操作①:新结点s的前驱指向p原来的前驱结点
p->prior->next = s; // 操作②:p原前驱结点的后继指向新结点s
s->next = p; // 操作③:新结点s的后继指向p结点
p->prior = s; // 操作④:p结点的前驱指向新结点s

return OK; // 插入成功,返回OK状态
}

删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 函数功能:删除带头结点的双向链表L中第i个位置的元素,并通过e返回被删除的值
// 参数说明:L-双向链表头指针(引用传递),i-删除位置,e-用于返回被删除元素值的引用
// 返回值:操作状态(OK-成功,ERROR-失败)
Status ListDelete_DuL(DuLinkList &L, int i, ElemType &e) {

// 步骤1:定位要删除的结点,获取第i个结点的指针p
DuLNode *p;
if(!(p = GetElemP_DuL(L, i))) // 调用查找函数获取第i个结点的地址
return ERROR; // 如果第i个结点不存在,返回错误

// 步骤2:保存被删除结点的数据值
e = p->data; // 将待删除结点p的数据域值保存到参数e中,以便返回给调用者

// 步骤3:核心操作 - 两步指针修改,将结点p从链表中"摘除"
// 关键思想:让p的前驱结点直接指向p的后继结点,绕过p本身

p->prior->next = p->next; // 操作①:p前驱结点的后继指针指向p的后继结点
p->next->prior = p->prior; // 操作②:p后继结点的前驱指针指向p的前驱结点

// 步骤4:释放被删除结点的内存空间
delete p; // 释放结点p占用的内存,防止内存泄漏

return OK; // 删除成功,返回OK状态
}

有序链表合并

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
// 函数功能:将两个非递减有序的带头结点的单链表La和Lb合并为一个新的非递减有序链表Lc
// 核心思想:通过指针操作,重用La和Lb的现有节点,不分配新的节点内存
// 参数说明:La, Lb-待合并的有序链表;Lc-合并后的新链表(引用传递)
// 返回值:无(通过Lc参数返回结果)
void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc) {
// 步骤1:初始化工作指针,跳过头结点,指向第一个数据节点
pa = La->next; // pa指向链表La的第一个数据节点(首元结点)
pb = Lb->next; // pb指向链表Lb的第一个数据节点

// 步骤2:设置合并后链表Lc的头结点,并初始化尾指针pc
pc = Lc = La; // 重用La的头结点作为Lc的头结点,pc作为Lc的当前尾指针

// 步骤3:核心循环 - 比较并合并两个链表的节点
// 循环条件:两个链表都还有未处理的节点(pa和pb都不为NULL)
while(pa && pb) {
// 比较当前两个节点的大小,选择较小的节点链接到Lc
if(pa->data <= pb->data) {
// La的当前节点值较小或相等,将其链接到Lc末尾
pc->next = pa; // 将pa节点挂接到pc之后
pc = pa; // pc指针移动到新的尾节点pa
pa = pa->next; // pa指针后移,准备处理La的下一个节点
}
else {
// Lb的当前节点值较小,将其链接到Lc末尾
pc->next = pb; // 将pb节点挂接到pc之后
pc = pb; // pc指针移动到新的尾节点pb
pb = pb->next; // pb指针后移,准备处理Lb的下一个节点
}
} // 结束循环时,pa和pb中至少有一个为NULL

// 步骤4:处理剩余节点 - 将未处理完的链表直接连接到Lc末尾
pc->next = pa ? pa : pb; // 如果pa不为空则连接La剩余部分,否则连接Lb剩余部分

// 步骤5:资源清理 - 释放Lb的头结点(数据节点已全部并入Lc)
delete Lb; // Lb的头结点已不再需要,释放其内存空间
}