链表的基本操作

官方信息技术老师·21 页·深入(追求细节与边界)·0 次浏览·2 天前
链表指针数据结构边界处理

链表的基本操作

跟着图解手画每条指针的走向,掌握增删改查与所有边界细节

按 空格/→ 演示下一步

1 / 21 页

全部页面点击任意一页,跳回舞台从这页播放

链表指针数据结构边界处理

链表的基本操作

跟着图解手画每条指针的走向,掌握增删改查与所有边界细节

1第 1 页 · 链表的基本操作

什么是链表

上节操作的那些节点,内部都长一个样:左边存数据,右边存'下一个在哪'。链表正是用这种'手拉手'的指针,把节点串成一条可自由伸缩的链。

节点结构
每个节点包含数据域和至少一个指针域,指针域存放下一节点的内存地址
链式连接
前节点的指针指向后节点,靠逻辑关系而非物理相邻串联
内存非连续
节点可分散在内存各处,物理位置不必相邻,全靠指针逻辑连接
动态伸缩
增删节点无需挪动其他元素,长度可在运行时任意调整
头节点为入口
整条链从 head 出发顺序遍历,尾部节点 next 指针为 null
火车站的列车对应 →链表

车厢载客(数据)并保留通往下一节的车钩(指针);车厢可加挂可甩掉

headn1n2nullhead\to n_1\to n_2\to\cdots\to null
2第 2 页 · 什么是链表

节点的构成

从节点出发:下方分数据域与指针域;箭头指向"下一节点"或 NULL。

图解渲染中…
b1数据域:存放本节点的值,如数字、字符b2指针域:存的是"地址",不是下一个节点本身c1非末尾节点:next 指向下一节点的地址c2末尾节点:next 为 NULL,表示链表结束
3第 3 页 · 节点的构成

链表 vs 数组

都是线性结构,但底层逻辑截然不同

链表
  • 逐个遍历访问,时间 O(n)
  • 散落内存各处,靠指针相连
  • 插入删除快,只需改指针
数组
  • 按下标随机访问,时间 O(1)
  • 连续内存块,地址相邻
  • 插入删除慢,要搬移元素
频繁查找选数组,频繁增删且大小未知选链表
4第 4 页 · 链表 vs 数组

空链表的初始化

上一节我们造好了节点,知道 head 指向第一个节点。那么问题来了:链表刚声明、还没塞任何节点时,head 该指向哪里?答案是 NULL——这是链表为空的唯一合法状态。

head = NULL
空链表的标准初始化语句,声明链表存在但无节点
NULL 是约定哨兵
不是数值 0,是标记'此处无节点'的专用值
区分空链表与野指针
未初始化的 head 是野指针,访问即崩溃
遍历的终止锚点
while(head != NULL) 依赖 NULL 作为停止信号
空置的店面门口对应 →head 指针置 NULL

贴'空置'明示无人,对应显式置 NULL;不是乱指别家

5第 5 页 · 空链表的初始化

创建链表的过程

创建链表本质是循环做四件事:申请内存、填数据、接指针、判断是否继续。

1
分配节点
用malloc或new向系统申请一块节点大小的内存
2
填充数据
把要保存的值写入节点的data字段
3
链接指针
新节点next指向后续节点,头节点由head持有
4
循环或终止
还有数据回到st1,否则末节点next置null收尾
6第 6 页 · 创建链表的过程

创建链表的代码实现

c

用 C 语言完整实现『创建一个单向链表』,含节点工厂函数与尾插法串联两个关键函数。

代码高亮加载中…

L15-L16 完成『数据填充+孤立即终结』;L22 创建头节点后,L26-L27 的『接链-移动指针』循环是构造链表的核心节拍。

7第 7 页 · 创建链表的代码实现

循环链表的创建

图示含 4 个节点的循环链表:顺次串联后,尾节点的 next 指针折回头节点——这就是循环链表与普通链表唯一的结构差异。

图解渲染中…
h1头节点:循环入口,无前驱指针t1尾节点:next 指向头节点形成闭环a2普通中间节点:next 指向下一个节点
8第 8 页 · 循环链表的创建

链表的遍历

从 head 出发,每次判断当前指针是否为空,非空就访问数据再后移,直到 null 结束。

图解渲染中…
b1循环判断点:cur == null 时退出m1指针后移:cur = cur.nexte1所有节点访问结束
9第 9 页 · 链表的遍历

遍历的实现步骤

从头节点开始,沿 next 指针逐个访问,直到末尾。

1
指针指向头
p = head,让 p 从第一个节点出发
2
判断不为空
while(p != NULL) 才进入循环体
3
处理当前节点
访问、修改、统计等业务在这里
4
指针后移
p = p->next,为下一轮迭代准备
10第 10 页 · 遍历的实现步骤

按值查找

从头遍历逐节点比对,直到命中目标值或走完整条链表。

1
从头节点开始
指针 p 指向 head,位置计数器 i 初始化为 0
2
取值比对
取出 p 指向节点的 data,与目标值做相等比较
3
相等则返回
比对成功,立即返回当前位置 i,查找终止
4
不等则后移
p 移向下一个节点,i 加 1,继续下一轮
5
末尾未找到
p 到达 null 时遍历结束,返回查无此节点
11第 11 页 · 按值查找

插入节点

在链表的第 i 个位置插入新节点,需要四步配合完成。

1
申请新节点
动态分配内存,初始化数据域和指针域
2
定位前驱
从头遍历找到第 i-1 个节点作为前驱
3
新节点接后继
新节点的 next 先指向原位置节点,防止后继丢失
4
前驱接新节点
前驱的 next 再指向新节点,完成插入
12第 12 页 · 插入节点

插入操作的代码

python

对比两种插入:头插两步完成、O(1);尾插要遍历到末尾、O(n)。

代码高亮加载中…

头插只动头指针两步搞定;尾插要先特判空表,再走完整条链表才能挂上去——这就是 O(1) 和 O(n) 的差距。

13第 13 页 · 插入操作的代码

删除节点

删除节点的难点不在「删」而在「接」:必须先保住后续链表,才能安全释放目标节点。

1
定位前驱节点
从头遍历找到目标节点的前一个,因为只有它能改 next
2
保存后继节点
用临时指针记下目标节点的下一个,防止释放后丢失链表后段
3
断链重连
让前驱节点的 next 直接指向后继,把目标节点从链中绕开
4
释放目标内存
调用 free/delete 归还堆内存,否则会留下泄漏
14第 14 页 · 删除节点

删除操作的代码

python

用哨兵+双指针实现按值删除,统一处理「删头」边界。

代码高亮加载中…

哨兵把「删头」和「删中间」统一成「前驱.next = 当前.next」;命中即返回 dummy.next,可能是原 head 或其后继。

15第 15 页 · 删除操作的代码

约瑟夫问题背景

之前我们建好了循环链表,遍历也跑通了——但链表真正的威力要靠"删除"来释放。现在用一个经典故事来看删除操作的极致用法:约瑟夫问题。

n人围圈
n个人首尾相连站成环,循环链表的天然舞台
从k开始报
从第k个人起报数1,后面依次喊2、3、4…
报到m出列
数到m者出圈,从其下一人重新从1开始报
求幸存者
反复淘汰,直到圈中只剩最后一人,问其原始编号
围圈报数游戏对应 →约瑟夫问题

出列=删除节点,下个人从1开始=指针后移并清零计数

16第 16 页 · 约瑟夫问题背景

约瑟夫问题图解

循环链表模拟围坐与报数过程

约瑟夫问题图解
循环链表模拟围坐与报数过程
17第 17 页 · 约瑟夫问题图解

约瑟夫算法步骤

约瑟夫问题的求解,就是不断报数、删人、接着报的循环。

1
构造环形队列
n个节点首尾相连,形成循环链表作为出列顺序
2
定位起点报数
从第k个节点起,按1、2、3…顺序报数
3
报到m者出列
报数到m的节点被删除,前后节点重新相接
4
下一人重新报
从被删节点的下一个开始,计数重置为1
5
直至剩最后一人
反复执行,直到链表中只剩一个节点即为胜者
18第 18 页 · 约瑟夫算法步骤

约瑟夫问题代码

c

循环链表解法的完整C语言实现,n人报数到m的逐轮淘汰。

代码高亮加载中…

第19行把尾节点指针收回头节点形成环;第26行prev指向p的前驱便于删除;第28行用'p自己指向自己'判断只剩一人;第34行让前驱跳过p完成删除;第36行从p的下一个重新开始报数。

19第 19 页 · 约瑟夫问题代码

自测题

点击作答

在单链表中,将新节点插入到已知节点 p 之后,正确的指针操作顺序是?

20第 20 页 · 自测题

课后思考

先尝试独立思考,再对照参考答案,每道题都值得多想一会儿。

1为什么在已知位置的链表插入和删除操作能达到 O(1) 时间复杂度?这与数组有什么本质区别?

参考答案因为只需修改相邻节点的指针指向,不需要像数组那样移动后续元素。核心代价是失去了随机访问能力。

2如果要求链表能高效地从后往前遍历,你会如何改造它?需要付出什么代价?

参考答案改为双向链表,每个节点额外存一个 prev 指针。反向遍历变 O(1),但每个节点多 8 字节,指针维护也变复杂。

3实际工程中很多场景明明链表插入更方便,却仍然选择数组,为什么?

参考答案因为缓存局部性:数组元素连续存储,CPU 预取命中率高;链表节点分散,频繁缓存未命中反而更慢。

21第 21 页 · 课后思考