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:排序链表相连排,
重复相邻好拆开;
单指针走一遍过,
每值留一不乱来。