bigywater.tech
算法与数据结构 · LeetCode 82

LeetCode 82. 删除排序链表中的重复元素 II

给定一个升序链表,把所有出现过重复的数字的结点都删掉,只保留没有重复的数字。注意和下一题的区别:这一题的"重复"是整体删除([1, 2, 3, 3, 4, 4, 5] 变成 [1, 2, 5],两个 3 和两个 4 都没了),下一题是重复的只保留一个。因为头结点也可能被删掉,需要一个哨兵(哑)结点。做法是用 prev 记录最后一个确定保留的结点,head 往后扫描:如果 head 和它的下一个值相同,就整串跳过,让 prev.next 指向串后面的第一个结点(prev 不动,因为新的结点还可能也是重复串的开头);否则 head 是独一无二的,保留,prev 前进。本文用真实数据展示两种典型错误,给出五种语言的实现,并把整串删除的过程做成分步演示。

20,000
组随机对拍,与暴力解的结果全部一致
7 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。链表在测试里用整数数组表示。基准解是一句话:只保留在整个数组里出现次数恰好为 1 的元素(因为链表有序,重复的元素一定连续),与指针操作是两种独立的写法。一共 12 组用例,覆盖:官方两个示例、空链表、单个结点、全部重复、没有重复、开头重复、结尾重复:

真实实测 · 本机执行输出
输入 [[1,2,3,3,4,4,5]]
输出 [1,2,5]
输入 [[1,1,1,2,3]]
输出 [2,3]
输入 [[]]
输出 []
输入 [[1]]
输出 [1]
输入 [[1,1]]
输出 []
输入 [[1,1,1]]
输出 []

陷阱:只保留一个,或者没有哨兵结点

第一个常见错误是把这道题当成下一题:重复的保留一个而不是全部删除。第二个是没有哨兵结点:头结点本身重复时,没有"前一个结点"可以改指针,头结点就删不掉。下面是两种错误写法与 solve 的真实结果:

真实实测 · 本机执行输出
# 两种错误写法:只保留一个(那是第 83 题)/ 没有哑结点
[1, 2, 3, 3, 4, 4, 5]: 重复的保留一个 -> [1, 2, 3, 4, 5];没有哑结点 -> [1, 2, 5];solve -> [1, 2, 5]
[1, 1, 1, 2, 3]: 重复的保留一个 -> [1, 2, 3];没有哑结点 -> [1, 1, 1, 2, 3];solve -> [2, 3]
[1, 1, 2, 2]: 重复的保留一个 -> [1, 2];没有哑结点 -> [1, 1, 2, 2];solve -> []
[1, 2, 3]: 重复的保留一个 -> [1, 2, 3];没有哑结点 -> [1, 2, 3];solve -> [1, 2, 3]

[1, 2, 3, 3, 4, 4, 5] 应该是 [1, 2, 5],"保留一个"的写法得到 [1, 2, 3, 4, 5];没有哨兵结点时,[1, 2, 3, 3, 4, 4, 5] 碰巧是对的(头结点 1 不是重复的,不需要删除头结点),但 [1, 1, 1, 2, 3](头结点重复)得到 [1, 1, 1, 2, 3],什么都没删,[1, 1, 2, 2] 也是原样返回。[1, 2, 3] 没有重复,三种写法一致。

哑结点加 prev:五种语言的实现

在头结点前放一个哑结点 dummy,prev 指向它。循环 while head:如果 head.next 存在并且值与 head 相同,记下这个值 v,把 head 一直往后移,直到值不再是 v(整串都跳过了),然后让 prev.next = head,prev 保持不动;否则 head 是独一无二的,prev = head,head 往后移。返回 dummy.next。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def delete_duplicates(head): dummy = ListNode(0, head) # 哑结点:头结点本身也可能被删掉 prev = dummy # prev:最后一个确定要保留的结点 while head: if head.next and head.next.val == head.val: # head 开始有一串相同的值:整串都要删掉 v = head.val while head and head.val == v: head = head.next prev.next = head # 跳过整串重复的结点(prev 不动,因为后面的结点还可能也是重复的) else: prev = head # head 是独一无二的:保留,prev 前进 head = head.next return dummy.next
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[1,2,3,3,4,4,5]]  →  [1,2,5]
[[1,1,1,2,3]]  →  [2,3]
[[]]  →  []
[[1]]  →  [1]
[[1,1]]  →  []
[[1,1,1]]  →  []
Java · 解法代码class ListNode { int val; ListNode next; ListNode(int v) { val = v; } } class Solution { public ListNode deleteDuplicates(ListNode head) { ListNode dummy = new ListNode(0); // 哑结点:头结点本身也可能被删掉 dummy.next = head; ListNode prev = dummy; // prev:最后一个确定要保留的结点 while (head != null) { if (head.next != null && head.next.val == head.val) { // head 开始有一串相同的值:整串都要删掉 int v = head.val; while (head != null && head.val == v) head = head.next; prev.next = head; // 跳过整串重复的结点(prev 不动) } else { prev = head; // head 是独一无二的:保留,prev 前进 head = head.next; } } return dummy.next; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[1,2,3,3,4,4,5]]  →  [1,2,5]
[[1,1,1,2,3]]  →  [2,3]
[[]]  →  []
[[1]]  →  [1]
[[1,1]]  →  []
[[1,1,1]]  →  []
Go · 解法代码type ListNode struct { Val int Next *ListNode } func deleteDuplicates(head *ListNode) *ListNode { dummy := &ListNode{Next: head} // 哑结点:头结点本身也可能被删掉 prev := dummy // prev:最后一个确定要保留的结点 for head != nil { if head.Next != nil && head.Next.Val == head.Val { // head 开始有一串相同的值:整串都要删掉 v := head.Val for head != nil && head.Val == v { head = head.Next } prev.Next = head // 跳过整串重复的结点(prev 不动) } else { prev = head // head 是独一无二的:保留,prev 前进 head = head.Next } } return dummy.Next }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[1,2,3,3,4,4,5]]  →  [1,2,5]
[[1,1,1,2,3]]  →  [2,3]
[[]]  →  []
[[1]]  →  [1]
[[1,1]]  →  []
[[1,1,1]]  →  []
JavaScript · 解法代码class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function deleteDuplicates(head) { const dummy = new ListNode(0, head); // 哑结点:头结点本身也可能被删掉 let prev = dummy; // prev:最后一个确定要保留的结点 while (head) { if (head.next && head.next.val === head.val) { // head 开始有一串相同的值:整串都要删掉 const v = head.val; while (head && head.val === v) head = head.next; prev.next = head; // 跳过整串重复的结点(prev 不动) } else { prev = head; // head 是独一无二的:保留,prev 前进 head = head.next; } } return dummy.next; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[1,2,3,3,4,4,5]]  →  [1,2,5]
[[1,1,1,2,3]]  →  [2,3]
[[]]  →  []
[[1]]  →  [1]
[[1,1]]  →  []
[[1,1,1]]  →  []
C++ · 解法代码struct ListNode { int val; ListNode* next; ListNode(int v) : val(v), next(nullptr) {} }; class Solution { public: ListNode* deleteDuplicates(ListNode* head) { ListNode dummy(0); // 哑结点:头结点本身也可能被删掉 dummy.next = head; ListNode* prev = &dummy; // prev:最后一个确定要保留的结点 while (head) { if (head->next && head->next->val == head->val) { // head 开始有一串相同的值:整串都要删掉 int v = head->val; while (head && head->val == v) head = head->next; prev->next = head; // 跳过整串重复的结点(prev 不动) } else { prev = head; // head 是独一无二的:保留,prev 前进 head = head->next; } } return dummy.next; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[1,2,3,3,4,4,5]]  →  [1,2,5]
[[1,1,1,2,3]]  →  [2,3]
[[]]  →  []
[[1]]  →  [1]
[[1,1]]  →  []
[[1,1,1]]  →  []
真实实测 · 本机执行输出
共 12 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 12/12
  Java        通过 12/12
  Go          通过 12/12
  JavaScript  通过 12/12
  C++         通过 12/12

随机对拍

Python 版本另外和"只保留出现一次的元素"做了随机对拍:长度 0 到 12、取值 0 到 5(重复值很多)的有序链表:

真实实测 · 本机执行输出
随机对拍 20000 组(长度 0 到 12、取值 0 到 5 的有序链表(重复值很多)),solve 与 brute 的结果全部一致。

另外四种语言只在上面 12 组固定用例上验证过,没有做大规模随机对拍。这道题只扫描链表一遍,每个结点访问常数次,是线性的,所以没有做规模对比实验。

分步演示:[1, 2, 3, 3, 4, 4, 5]

下面的演示用 [1, 2, 3, 3, 4, 4, 5]。1 和 2 只出现一次,保留,prev 依次前进;两个 3 是一串重复,整串删掉(淡色),prev 不动;两个 4 同理;5 只出现一次,保留。结果 [1, 2, 5]。图中链表最前面的 dummy 是哑结点,所以下标从 1 开始算元素。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。

删除排序链表中的重复元素 II:[1, 2, 3, 3, 4, 4, 5](出现过重复的值要整个删掉)
head(正在看的结点)prev(最后一个确定保留的结点)被删除

    参考与说明

    • 官方题目页:LeetCode 82. 删除排序链表中的重复元素 II。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:递归写法;链表无序时需要哈希表统计出现次数的版本。
    京ICP备2026067502号-1京公网安备11010502063487号