在链表操作类算法题中,删除重复元素 是一类经典问题。而 LeetCode 第 82 题《Remove Duplicates from Sorted List II》(有序链表去重 II)因其"只要重复,全部删掉"的严格要求,常被作为考察指针操作与边界处理能力的重要题目。
本文将深入剖析该问题的解题思路、关键细节,并提供清晰易懂的 Python 实现。
一、题目回顾
题目描述 :
给定一个已排序 的链表,删除所有存在重复数字的节点 ,只保留原始链表中没有重复出现的数字。返回修改后的链表。
示例:
输入: 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5
输出: 1 -> 2 -> 5
输入: 1 -> 1 -> 1 -> 2 -> 3
输出: 2 -> 3
注意:与第 83 题(保留一个重复元素)不同,本题要求完全移除所有重复值的节点。
二、核心难点
- 头节点可能被删除 → 需要使用虚拟头节点(dummy node)。
- 连续多个相同值 → 不能仅比较相邻两个节点,需跳过整段重复区域。
- 边界情况多:空链表、全重复、无重复等。
三、解题思路:双指针 + 虚拟头节点
我们采用以下策略:
- 创建一个 dummy 节点 ,其
next指向原链表头,用于统一处理头节点被删除的情况。 - 使用指针
prev(前驱)指向确定保留的最后一个节点。 - 对于当前
prev.next节点,检查其后是否有重复:- 若
prev.next.val == prev.next.next.val,说明有重复。 - 则用另一个指针
curr向后遍历,跳过所有相同值的节点。 - 将
prev.next直接指向第一个不同值的节点(即删除整段重复)。 - 注意:此时不移动
prev,因为新prev.next可能仍是重复段开头。 - 若无重复,则
prev = prev.next,继续前进。
- 若
四、Python 代码实现
# 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: ListNode) -> ListNode:
# 创建虚拟头节点
dummy = ListNode(0)
dummy.next = head
prev = dummy # prev 指向已确认不重复的最后一个节点
while prev.next and prev.next.next:
# 如果发现重复
if prev.next.val == prev.next.next.val:
# 记录重复值
duplicate_val = prev.next.val
# 跳过所有重复节点
curr = prev.next
while curr and curr.val == duplicate_val:
curr = curr.next
# 删除整段重复
prev.next = curr
else:
# 无重复,prev 前进
prev = prev.next
return dummy.next
五、关键点解析
-
为什么用
dummy?因为原头节点可能被删除(如
[1,1,2]),若直接操作head,难以返回新头。dummy保证始终有稳定入口。 -
为何不立即移动
prev?例如链表
1->2->2->2->3,当prev在1时,发现2重复,删除后prev.next变为3。但若链表是1->2->2->2,删除后prev.next为None,无需再判断。更重要的是,如果删除后的新prev.next本身又是重复段(如1->2->2->3->3->4),我们必须再次检查,因此不能盲目前进。 -
时间复杂度:O(n),每个节点最多访问两次。
-
空间复杂度:O(1),仅用常数额外空间。
六、测试用例验证
| 输入 | 输出 |
|---|---|
[] |
[] |
[1] |
[1] |
[1,1] |
[] |
[1,2,3,3,4,4,5] |
[1,2,5] |
[1,1,2,2,3,3] |
[] |
均可正确处理。
七、总结
LeetCode 82 题考验的是对链表指针的精细控制和对边界条件的全面考虑。通过引入虚拟头节点 和谨慎移动前驱指针,我们可以优雅地解决"彻底删除重复节点"的问题。
掌握此类题型,不仅有助于面试,更能提升对链表这一基础数据结构的理解。记住:当需要删除头节点或处理复杂连接时,dummy node 是你的最佳伙伴。
编程如织网,指针似针线------唯有细致穿引,方得清晰结构。