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

难度:简单 | 主题:链表

题目

给定一个已排序的链表的头节点 head,删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。

示例

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

思路

先讲个故事:同一排座位挨着的双胞胎

教室座位按学号排好。双胞胎总是挨着坐(学号相同)。老师想只保留每个学号一人:看到学号相同 → 第二个人站起来出去,第一个不动。学号变了 → 前移到新人。

因为已排序,重复的一定相邻。


引导式推导:相邻比较

关键洞察:排序后重复必相邻。

 
[1, 1, 2, 3, 3]
 
curr=1,  curr.next=1  → 跳过:curr.next = curr.next.next
 
curr=1,  curr.next=2  → 不等,前进:curr = curr.next
 
curr=2,  curr.next=3  → 不等,前进:curr = curr.next
 
curr=3,  curr.next=3  → 跳过:curr.next = curr.next.next
 
curr=3,  curr.next=None → 结束
 

为什么相等时 curr 不前进?

[1, 1, 1]:第一次跳过得到 ① → ①,如果 curr 前进了,第二个 1 就漏了。不前进才能继续检查新的 curr.next


graph LR

    subgraph 相邻比较去重

        A["curr=①→①→②"] --> B{"curr.val == curr.next.val?"}

        B -->|是| C["跳过: curr.next = curr.next.next<br/>curr不动"]

        C --> B

        B -->|否| D["前进: curr = curr.next"]

    end

解法时间空间
单指针O(n)O(1)
双指针O(n)O(1)

代码

 
def deleteDuplicates(self, head):
 
    curr = head
 
    while curr and curr.next:
 
        if curr.val == curr.next.val:
 
            curr.next = curr.next.next
 
        else:
 
            curr = curr.next
 
    return head
 

复杂度

指标解释
时间O(n)一次遍历
空间O(1)一个指针

实战考量

频率分析

出现在:链表入门题,约 15% 的 AI Agent 会作为热身。通常不会单独考,而是作为更难题的子问题或对比。

延伸思考

Q:和 82 题(重复全删)的区别?

A:本题重复保留一个,curr 遇到重复时跳过多个但不动。82 题重复全删,prev 不动等新节点。

Q:如果链表没排序呢?

A:用哈希集合记录出现过的值。时间复杂度 O(n),空间 O(n)。

Q:相等时 curr 为什么不前进?

A:[1,1,1]——跳过第一个重复后变成 ① → ①,还需要继续检查新的 curr.next 是否还是 1。

Q:如果要求只删除第一个重复元素(保留第二个)呢?

A:反过来判断:curr.next.val == curr.val 时跳过 curr.next。逻辑反着写。

易错点

  • 相等时 curr 不前进——很多人习惯性写了 curr = curr.next

  • 忘记判断 curr.next 存在(while 条件 curr and curr.next

  • 链表已排序是前提,未排序不能用此法


生活类比

删除重复(保留一个)→ 双胞胎只留一个

学号相同的人挨着坐。看到学号一样?第二个人出去,我留在原地继续看下一个——直到看到不同学号才往前坐。每次只处理一个重复,不贪心。


相关题目

题目关系
82删除排序链表中的重复元素II进阶版:重复元素全部删除

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

速记卡(面试闪卡)

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

A:已排序链表用单指针遍历,相邻相等就跳过重复节点,每个值只保留一个。

Q2:双胞胎只留一个(adjacent duplicates) —— 怎么理解?

A:类比:座位按学号排好,双胞胎总挨着坐。看到学号相同,第二个站起来出去、第一个不动;学号变了才往前挪。因为已排序,重复必相邻,像揪出连号小抄一般顺手。(Sorted ⇒ adjacent)

Q3:跳过而非前进(linked list traversal) —— 怎么理解?

A:类比:curr 从头走,curr.val==curr.next.val 就 curr.next=curr.next.next 把重节点摘掉;否则 curr=curr.next 前进。注意相等时 curr 不前进——否则 [1,1,1] 会漏掉第二个 1。(Skip, don’t advance)

Q4:复杂度与边界(O(n) time, O(1) space) —— 怎么理解?

A:类比:从头到尾划一遍,时间 O(n);只用一个指针,空间 O(1)。边界是 while curr and curr.next,别让空指针踩了雷。像用一根手指划过名单,把重名轻轻划掉。(Single pointer)

Q5:对比 82 与未排序(hash set) —— 怎么理解?

A:类比:82 题是重复全删、prev 不动等新节点;本题重复留一个。若链表没排序,双指针失效,得用哈希集合(hash set)记出现过的値,以空间换 O(n) 的清爽解法。(Hash set fallback)

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

  • 题目:已排序链表,重复元素只保留一个

  • 思路:排序后重复必相邻,单指针相邻比较(sorted list)

  • 代码:相等跳 next,注意 curr 不前进

  • 复杂度:时间 O(n)、空间 O(1)

  • 实战:对比 82 全删;未排序要用 hash set

口诀

A:排序链表相连排,

重复相邻好拆开;

单指针走一遍过,

每值留一不乱来。

相关链接