第4章|链表

Unit 01 · LeetCode #203 移除链表元素

← Unit 00 基础
仅本次页面有效
U01
Remove Linked List ElementsDummy · Predecessor · Bypass

删除节点,不是“把它藏起来”,
而是让前驱绕过它。

本单元用 dummy 节点把“删除头节点”和“删除中间节点”统一成同一个动作:检查 current.next,命中时改写前驱的 next,保留时才移动 current。

已恢复上次学习位置阶段、动画、答题、代码和滚动位置已恢复。
知识桥梁|第3章 → 第4章
上一章:容器保存“未完成状态”

栈、队列和堆通过保留候选,等待某个事件到达后再结算答案。

结构重心改变 →
本章:指针决定“哪些节点仍可达”

链表操作不移动数组元素,而是改写 next 边。删除的本质是让入口路径绕过目标节点。

贯穿本章的审查问题当前变量指向哪个节点?哪一条 next 边将被断开?哪一条新边将被建立?从 head 出发还能到达谁?
LeetCode #203 题目模型删除所有值等于 val 的节点,并返回新的头节点

题目要求

给定链表头节点 head 和整数 val,删除链表中所有满足 Node.val == val 的节点,并返回删除后的头节点。

class Solution: def removeElements( self, head: Optional[ListNode], val: int ) -> Optional[ListNode]: pass

约束

  • 节点数为 [0, 10⁴];
  • 1 ≤ Node.val ≤ 50;
  • 0 ≤ val ≤ 50。

示例

head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
head = [], val = 1
输出:[]
head = [7,7,7,7], val = 7
输出:[]

复杂度目标

单次扫描每个节点,时间复杂度 O(n);只使用固定数量指针,额外空间复杂度 O(1)。

Dummy Node

放在原 head 前的虚拟节点,为原头节点提供稳定前驱,使删除头节点无需特殊分支。

Predecessor

要删除某节点,必须修改它的前驱节点的 next;本题 current 始终扮演前驱。

Bypass

current.next = current.next.next 让前驱绕过目标节点,改变可达链。

Loop Invariant

dummy 到 current 的区域已处理完毕;current.next 是下一个尚未判断的节点。

认知任务 1
先判断哪些节点应该从“可达链”中消失
点击所有值为 6 的节点,再检查。注意:删除的是节点对象,不是把节点值改成空白。
链表删除实验台人工选择
从 dummy / head 可达
已绕过节点
本步断开的边无
本步新建的边无
检查 next
判断值
绕过 / 保留
移动前驱
验证可达
请点击两个值为 6 的节点,将它们标记为待删除。

错误 1:删除后立刻移动 current

在 [1,6,6,2] 中,第一次绕过 6 后,新的 current.next 仍然是 6。若 current 同时前移,就会跳过它。

正确:current 留在 1,再检查第二个 6
错误:current 移到第二个 6,下一轮检查 2

错误 2:移动变量不等于删除节点

current = current.next 只改变变量指向;原前驱仍然通过 next 指向目标节点,因此结构没有变化。

删除动作必须是:
current.next = current.next.next

错误 3:没有 dummy 时头节点要特判

若 head 本身等于 val,需要反复更新 head。dummy 为 head 提供稳定前驱,把所有删除统一成同一条语句。

dummy → 7 → 7 → 1
current 始终从 dummy 开始

错误 4:返回 dummy 而不是 dummy.next

dummy 是算法辅助节点,不属于原题答案。完成后应返回真正结果入口 dummy.next。

return dummy.next
代码补全填写 4 个关键表达式
from typing import Optional class Solution: def removeElements(self, head, val): dummy = ListNode(0, ) current = while current.next: if current.next.val == val: current.next = else: current = return dummy.next
提示 1|先统一入口 dummy 应连接原 head;current 从 dummy 开始,始终充当待检查节点的前驱。 提示 2|删除分支 命中目标后,需要让 current.next 直接指向“目标节点的后继”。删除后 current 暂时不移动,因为新接上的节点仍未检查。 提示 3|保留分支 只有确认 current.next 不删除时,current 才前移到 current.next。最终返回 dummy.next,而不是 dummy。
代码—节点—边逐行联动状态 1 / 1
当前代码行1
current—
current.next—
可达链—
本步判断进入函数
进入函数。
完成代码联动后进入迁移练习代码补全正确或播放完整轨迹后解锁;查看提示与填入骨架不会解锁。
迁移原题 · LeetCode #83

删除排序链表中的重复元素

先阅读官方原题,再判断 #203 中哪些结构可以复用、哪些条件必须改变。不要从代码答案倒推题意。

打开 LeetCode 原题 ↗

官方题意

给定一个已按升序排列的链表 head,删除重复节点,使每个元素只出现一次,并返回仍按升序排列的链表。

class Solution: def deleteDuplicates( self, head: Optional[ListNode] ) -> Optional[ListNode]: pass
输入:head = [1,1,2]
输出:[1,2]
输入:head = [1,1,2,3,3]
输出:[1,2,3]
  • 节点数范围:[0, 300]
  • -100 ≤ Node.val ≤ 100
  • 链表保证按升序排列
阶段 1|先确认输出契约排序条件让所有相同值连续出现,但题目要求“每个值保留一个”,不是删除所有重复值。
0 / 2

① 输入 [1,1,2,3,3] 应返回什么?

每个重复值只保留一个节点。

② “链表已排序”提供了哪项关键保证?

排序并不会提供下标,只会把重复值聚集在一起。
两道读题检查完成后,才能进入结构迁移。
阶段 2|#203 与 #83 的结构对照不要只比较代码行;要比较目标节点、前驱角色、是否需要 dummy,以及 current 的移动条件。
问题输入#203

任意链表 + 外部目标值 val

问题输入#83

已排序链表,无外部目标值

删除触发#203

current.next.val == val

删除触发#83

current.val == current.next.val

头节点处理#203

头节点可能被全部删除,需要 dummy 稳定前驱

头节点处理#83

每组至少保留第一个节点,可直接从 head 开始

保持不变局部改边

命中时执行 current.next = current.next.next

保持不变移动规则

删除后不前移;保留后才前移

迁移判断

为什么 #83 通常不需要 dummy?

关注“删除当前节点”还是“删除当前节点之后的重复节点”。
阶段 3|把差异映射为四个代码语义填写的是结构意义,不是孤立语法。全部正确后才生成可提交代码。
0 / 4
删除分支仍然不移动 current;else 分支才执行 current = current.next。
from typing import Optional

class Solution:
    def deleteDuplicates(
        self,
        head: Optional[ListNode]
    ) -> Optional[ListNode]:
        current = head

        while current and current.next:
            if current.val == current.next.val:
                current.next = current.next.next
            else:
                current = current.next

        return head
阶段 4|运行官方样例 [1,1,2,3,3]每一步都观察 current、current.next、被断开的边和当前可达链。
1 / 6
currentvalue 1
current.nextvalue 1
动作准备比较
可达链[1,1,2,3,3]
从第一个值 1 开始,检查相邻后继。

独立重写|不自动覆盖你的答案

检查器会验证循环安全、相邻值比较、绕过重复后继、else 前移和返回 head。
检查清单:
1. 空链表不会访问 current.next;
2. 比较 current.val 与 current.next.val;
3. 重复时绕过 current.next;
4. 不重复时 current 前移;
5. 返回 head。

Unit 01 Skill:通过前驱指针删除节点

当头节点可能被删除时,引入 dummy 获得稳定前驱。循环中只检查 current.next:命中目标就改写 current.next 并保持 current 不动;保留节点时才令 current 前移。

触发信号头节点也可能被删除
核心表示dummy + current 前驱
关键不变量dummy 到 current 已处理
删除动作current.next = current.next.next
常见错误删除后 current 仍前移
复杂度O(n) 时间,O(1) 空间
本单元已完成,页面会停留在这里

不会自动跳转。可以回到任意阶段复习,刷新后仍会恢复当前位置。

手动进入 Unit 02 →
阶段 1 / 6 · 草稿仅本次页面有效,可随时返回