本文详解 leetcode 83 题「删除排序链表中的重复元素」,聚焦于原地修改链表时因指针残留连接导致的尾部重复问题,并给出可鲁棒处理所有边界情况(包括全相同元素)的修正方案。
本文详解 leetcode 83 题「删除排序链表中的重复元素」,聚焦于原地修改链表时因指针残留连接导致的尾部重复问题,并给出可鲁棒处理所有边界情况(包括全相同元素)的修正方案。
在解决「删除排序链表中重复元素」问题时,一个常见且看似直观的思路是:维护一个新链表头 temp 和当前尾节点 curr,遍历原链表 head,仅当 head.val != curr.val 时才将该节点接续到 curr.next 并前移 curr。然而,这种写法存在结构性隐患——它未主动切断 curr 与后续冗余节点的引用关系,导致链表末尾可能意外保留重复节点。
以输入 [1,1,2,3,3] 为例,原始代码执行过程如下:
- 初始:temp → None, curr 指向 temp
- 第一个 1:curr.val=0(默认值),满足 1 ≠ 0 → curr.next = head(1),curr 移至该节点
- 第二个 1:curr.val=1 == head.val=1 → 跳过,仅 head = head.next
- 2:满足不等 → 接入,curr 移至 2
- 第一个 3:接入,curr 移至 3
- 第二个 3:跳过,head 变为 None,循环结束
此时 curr(即最后一个 3 节点)的 next 仍指向原链表中已被跳过的第二个 3(即 curr.next 未被置空),因此最终链表为 1→2→3→3,而非预期的 1→2→3。
关键问题在于:curr.next 始终继承自原链表的原始连接,未被显式断开。简单在循环末尾加 curr.next = None(如 temp.next 返回前)看似可行,但在全相同链表(如 [0,0,0])中会失效——因为 curr 始终未移动(从未进入 if 分支),最终 curr 仍指向 temp,curr.next = None 将直接清空整个结果。
✅ 正确解法是在每次跳过重复节点时,同步切断 curr.next 的旧连接:
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:
return None
temp = ListNode(0) # 使用明确初始值,避免默认 val=0 与输入冲突
curr = temp
while head:
if head.val != curr.val:
curr.next = head
curr = curr.next
head = head.next
curr.next = None # ✅ 关键:立即断开 curr 后续引用
else:
head = head.next
return temp.next
? 为什么 curr.next = None 必须放在 if 分支内?
因为只有当 curr 实际向前移动(即新节点被接入)后,才需要清理其 next 字段。若放在循环外,curr 可能从未更新(如全重复链表),导致误删有效连接;若放在 else 中,则对非重复节点无效。本位置确保每次 curr 成为新尾节点时,其 next 都被安全归零。
此外,初始化 ListNode(0) 比 ListNode() 更稳妥,避免 curr.val 默认为 0 与输入首值冲突(如 head = [0,0,1])。该方案时间复杂度 O(n),空间复杂度 O(1),适用于所有边界用例:空链表、单节点、全重复、无重复及任意混合情况。
总结:链表原地操作的核心原则是——每一次节点复用,都必须显式管理其 next 指针。依赖原链表结构“自然终止”是危险的,主动切断冗余连接才是鲁棒性的基石。










