链表的基本操作
跟着图解手画每条指针的走向,掌握增删改查与所有边界细节
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
链表的基本操作
跟着图解手画每条指针的走向,掌握增删改查与所有边界细节
什么是链表
上节操作的那些节点,内部都长一个样:左边存数据,右边存'下一个在哪'。链表正是用这种'手拉手'的指针,把节点串成一条可自由伸缩的链。
车厢载客(数据)并保留通往下一节的车钩(指针);车厢可加挂可甩掉
节点的构成
从节点出发:下方分数据域与指针域;箭头指向"下一节点"或 NULL。
链表 vs 数组
都是线性结构,但底层逻辑截然不同
- 逐个遍历访问,时间 O(n)
- 散落内存各处,靠指针相连
- 插入删除快,只需改指针
- 按下标随机访问,时间 O(1)
- 连续内存块,地址相邻
- 插入删除慢,要搬移元素
空链表的初始化
上一节我们造好了节点,知道 head 指向第一个节点。那么问题来了:链表刚声明、还没塞任何节点时,head 该指向哪里?答案是 NULL——这是链表为空的唯一合法状态。
贴'空置'明示无人,对应显式置 NULL;不是乱指别家
创建链表的过程
创建链表本质是循环做四件事:申请内存、填数据、接指针、判断是否继续。
创建链表的代码实现
用 C 语言完整实现『创建一个单向链表』,含节点工厂函数与尾插法串联两个关键函数。
L15-L16 完成『数据填充+孤立即终结』;L22 创建头节点后,L26-L27 的『接链-移动指针』循环是构造链表的核心节拍。
循环链表的创建
图示含 4 个节点的循环链表:顺次串联后,尾节点的 next 指针折回头节点——这就是循环链表与普通链表唯一的结构差异。
链表的遍历
从 head 出发,每次判断当前指针是否为空,非空就访问数据再后移,直到 null 结束。
遍历的实现步骤
从头节点开始,沿 next 指针逐个访问,直到末尾。
按值查找
从头遍历逐节点比对,直到命中目标值或走完整条链表。
插入节点
在链表的第 i 个位置插入新节点,需要四步配合完成。
插入操作的代码
对比两种插入:头插两步完成、O(1);尾插要遍历到末尾、O(n)。
头插只动头指针两步搞定;尾插要先特判空表,再走完整条链表才能挂上去——这就是 O(1) 和 O(n) 的差距。
删除节点
删除节点的难点不在「删」而在「接」:必须先保住后续链表,才能安全释放目标节点。
删除操作的代码
用哨兵+双指针实现按值删除,统一处理「删头」边界。
哨兵把「删头」和「删中间」统一成「前驱.next = 当前.next」;命中即返回 dummy.next,可能是原 head 或其后继。
约瑟夫问题背景
之前我们建好了循环链表,遍历也跑通了——但链表真正的威力要靠"删除"来释放。现在用一个经典故事来看删除操作的极致用法:约瑟夫问题。
出列=删除节点,下个人从1开始=指针后移并清零计数
约瑟夫问题图解
循环链表模拟围坐与报数过程
约瑟夫算法步骤
约瑟夫问题的求解,就是不断报数、删人、接着报的循环。
约瑟夫问题代码
循环链表解法的完整C语言实现,n人报数到m的逐轮淘汰。
第19行把尾节点指针收回头节点形成环;第26行prev指向p的前驱便于删除;第28行用'p自己指向自己'判断只剩一人;第34行让前驱跳过p完成删除;第36行从p的下一个重新开始报数。
自测题
在单链表中,将新节点插入到已知节点 p 之后,正确的指针操作顺序是?
课后思考
先尝试独立思考,再对照参考答案,每道题都值得多想一会儿。
参考答案因为只需修改相邻节点的指针指向,不需要像数组那样移动后续元素。核心代价是失去了随机访问能力。
参考答案改为双向链表,每个节点额外存一个 prev 指针。反向遍历变 O(1),但每个节点多 8 字节,指针维护也变复杂。
参考答案因为缓存局部性:数组元素连续存储,CPU 预取命中率高;链表节点分散,频繁缓存未命中反而更慢。