82. 删除排序链表中的重复元素 II(Remove Duplicates from Sorted List II)

难度:中等 | 主题:链表

题目

给定已排序的链表的头节点 head,删除原始链表中所有重复数字的节点,只保留原始链表中没有重复出现的数字。返回同样按升序排列的结果链表。

示例

 
输入:head = [1,2,3,3,4,4,5]
 
输出:[1,2,5]
 
输入:head = [1,1,1,2,3]
 
输出:[2,3]
 

思路

先讲个故事:火车站检票

在火车站排队检票,你不确定前面的人是不是”有问题”(重复)。你手里有一张”确认没问题”的最后一张票(prev)。你朋友在前面一个一个检查。

如果发现前后两张票号码一样(重复团伙)→ 跳过整个团伙。如果票不一样 → 这张票安全,你往前站一格。

关键:跳过重复团伙后,你不往前走——因为新接上的可能也有问题。


引导式推导:从保留一个到全删除

跟 83 题(保留一个重复)的区别:

83:遇到重复,跳过后面的,curr 继续往前

82:遇到重复,全部跳过,一个不留

 
83题处理 [1,1,2,2]:1→2
 
82题处理 [1,1,2,2]:[](全删)
 

过程模拟:

 
[1,1,2,2,3]
 
dummy → ① → ① → ② → ② → ③
 
prev=dummy, cur=①
 
①==① → 重复!记下 duplicate=1,跳过所有 1
 
cur=②,prev.next=②
 
②==② → 又重复!记下 duplicate=2,跳过所有 2
 
cur=③,prev.next=③
 
③.next=None → 退出
 
结果:③
 

graph TD

    subgraph 跳过重复

        A["prev=dummy<br/>cur=head"] --> B{"cur.val == cur.next.val?"}

        B -->|是| C["记下重复值<br/>跳过所有相同值"]

        C --> D["prev.next = cur<br/>prev不动"]

        D --> B

        B -->|否| E["prev = cur<br/>cur = cur.next"]

        E --> B

    end

    style C fill:#fdd

解法时间空间
虚拟头+单指针扫描O(n)O(1)

代码

 
class Solution:
 
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
 
        dummy = ListNode(0)
 
        dummy.next = head
 
        prev = dummy
 
        cur = head
 
        while cur and cur.next:
 
            if cur.val == cur.next.val:
 
                duplicate_val = cur.val
 
                while cur and cur.val == duplicate_val:
 
                    cur = cur.next
 
                prev.next = cur
 
            else:
 
                prev = prev.next
 
                cur = cur.next
 
        return dummy.next
 

复杂度

指标解释
时间O(n)每个节点最多访问一次
空间O(1)几个指针

实战考量

频率分析

出现在:腾讯/美团约 20% 的 AI Agent 常会出这题对比 83 题。重点考察对”重复”边界的精确控制

延伸思考

Q:和 83 题的核心区别?

A:83 题遇到重复保留一个,curr 会前进。本题遇到重复全删,prev 不前进——因为新接上的可能也是重复。

Q:为什么发现重复时 prev 不能前进?

A:[1,1,2,2] 为例——跳过第一个 1 团伙后,prev.next = cur(cur=2)。如果 prev 前进了,下一对 2,2 就无法被跳过。

Q:如果链表没排序还能做吗?

A:不能。排序后重复必相邻,才能用相邻比较。未排序需要用哈希表统计频率。

Q:不用 dummy 能做吗?

A:头节点可能被删(如 [1,1,2]),不用 dummy 需要单独处理头节点删除的情况,代码更复杂。

易错点

  • 发现重复后 prev 跟着前进(应该不动)

  • 内层用 if 而不是 while,只能跳过一个重复节点

  • 忘记用 dummy,头节点被删除时返回错误结果


生活类比

删除所有重复 → 火车站检票跳过问题团伙

你手里拿着最后一张确认有效的票。发现前后两人票号一样?整个团伙(所有人)全部请出去。你站在原地不动——因为新排上来的人可能也是团伙成员。


相关题目

题目关系
83删除排序链表中的重复元素保留一个 vs 全删

→ 返回题单:LeetCode学习路线图 > 四、链表

速记卡(面试闪卡)

Q1:一句话讲清「82. 删除排序链表中的重复元素 II(Remove Duplicates from Sorted List II)」到底是什么?

A:给一条已排序链表,把所有出现重复的节点整个删掉,只留下从未重复过的数字。

Q2:题目怎么理解? —— 怎么理解?

A:像火车站检票,同一票号的”团伙”全部请出车站、一个不留——和83题”保留一个”正好相反。这是排序链表去重(sorted list deduplication,本题要求全删)。

Q3:为什么 prev 不能前进? —— 怎么理解?

A:跳过1团伙后你站在原地不动,因为新排上来的可能也是团伙成员(如 [1,1,2,2])。prev 一前进就漏掉下一对重复。靠的是虚拟头指针(dummy node)托住头节点可能被删的情况。

Q4:内层为什么用 while? —— 怎么理解?

A:重复往往是一串([2,2,2]),if 只能踢一个,while 才能把整伙清走:cur 一直后移直到值变。这是 while 跳过连续重复(skip-whole-cluster loop)。

Q5:复杂度和实战怎么理解? —— 怎么理解?

A:腾讯/美团约20%会拿它和83题对比,考你对”重复边界”的精确控制。时间 O(n) 一遍扫,空间 O(1)(几个指针)。

Q6:核心速记主线有哪些?

  • 题目:删光所有重复节点,只留唯一数字

  • 核心:dummy 虚拟头 + prev/cur 双指针

  • 关键:发现重复 prev 不动,用 while 跳整伙

  • 实战:腾讯/美团常考,对比 83 题边界控制

口诀

A:排序链表去重复,

遇见团伙全踢走;

prev站稳别乱动,

while扫净才罢手。

相关链接