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

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

给定一个升序链表,删除所有重复的元素,使每个元素只出现一次(重复的保留一个)。因为链表有序,重复的元素一定挨在一起,所以只需要比较相邻的两个结点:如果当前结点 cur 和它的下一个值相同,就把下一个结点摘掉(cur.next = cur.next.next);不同才让 cur 前进。这里有一个很容易写错的细节:摘掉之后 cur 不能前进,因为新的下一个结点可能还是重复的。本文用真实数据展示这个错误会错多少,给出五种语言的实现,并把逐个摘除的过程做成分步演示。

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

题意与示例

完整题目见官方页面(链接在文末)。链表在测试里用整数数组表示。基准解是数组去重(sorted(set(a))),与指针操作是两种独立的写法。头结点永远会被保留(它是第一个),所以不需要哑结点。一共 12 组用例,覆盖:官方两个示例、空链表、单个结点、全部相同、没有重复、连续三个或四个相同:

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

陷阱:摘掉重复结点之后,cur 不能前进

把"摘掉重复结点"和"cur 前进"写成两个独立的步骤(先判断并摘除,再无条件前进),看起来也顺理成章。但如果一个值出现了三次或更多,cur 摘掉第一个重复之后已经前进到了下一个结点,剩下的重复就被漏掉了。下面是这种写法与 solve 的真实结果:

真实实测 · 本机执行输出
# 摘掉重复结点之后 cur 也前进的写法
[1, 1, 2]: 该写法 -> [1, 2]    solve -> [1, 2]
[1, 1, 1]: 该写法 -> [1, 1]    solve -> [1]
[1, 1, 1, 2]: 该写法 -> [1, 1, 2]    solve -> [1, 2]
[1, 1, 2, 3, 3]: 该写法 -> [1, 2, 3]    solve -> [1, 2, 3]
随机 20000 个链表:该写法出错的有 8767 个(43.8%)

[1, 1, 2] 和 [1, 1, 2, 3, 3] 两种写法碰巧一致(每个值只重复了一次);但 [1, 1, 1] 得到 [1, 1],[1, 1, 1, 2] 得到 [1, 1, 2]:三个 1 只去掉了一个。在随机的 20000 个链表里(重复值很多),这种写法出错的有 8767 个,占 43.8%。所以只用官方示例(每个值最多重复一次)测不出这个错误。

相邻比较:五种语言的实现

cur 从头结点出发。只要 cur 和 cur.next 都存在:如果两者值相同,摘掉下一个结点:cur.next = cur.next.next,cur 不动;否则 cur = cur.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): cur = head while cur and cur.next: if cur.val == cur.next.val: cur.next = cur.next.next # 后一个结点重复:把它摘掉;cur 不动,因为新的后一个可能还是重复的 else: cur = cur.next # 后一个结点不同:cur 前进 return head
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[1,1,2]]  →  [1,2]
[[1,1,2,3,3]]  →  [1,2,3]
[[]]  →  []
[[1]]  →  [1]
[[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 cur = head; while (cur != null && cur.next != null) { if (cur.val == cur.next.val) cur.next = cur.next.next; // 后一个结点重复:把它摘掉;cur 不动 else cur = cur.next; // 后一个结点不同:cur 前进 } return head; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[1,1,2]]  →  [1,2]
[[1,1,2,3,3]]  →  [1,2,3]
[[]]  →  []
[[1]]  →  [1]
[[1,1]]  →  [1]
[[1,1,1]]  →  [1]
Go · 解法代码type ListNode struct { Val int Next *ListNode } func deleteDuplicates(head *ListNode) *ListNode { cur := head for cur != nil && cur.Next != nil { if cur.Val == cur.Next.Val { cur.Next = cur.Next.Next // 后一个结点重复:把它摘掉;cur 不动 } else { cur = cur.Next // 后一个结点不同:cur 前进 } } return head }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[1,1,2]]  →  [1,2]
[[1,1,2,3,3]]  →  [1,2,3]
[[]]  →  []
[[1]]  →  [1]
[[1,1]]  →  [1]
[[1,1,1]]  →  [1]
JavaScript · 解法代码class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function deleteDuplicates(head) { let cur = head; while (cur && cur.next) { if (cur.val === cur.next.val) cur.next = cur.next.next; // 后一个结点重复:把它摘掉;cur 不动 else cur = cur.next; // 后一个结点不同:cur 前进 } return head; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[1,1,2]]  →  [1,2]
[[1,1,2,3,3]]  →  [1,2,3]
[[]]  →  []
[[1]]  →  [1]
[[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* cur = head; while (cur && cur->next) { if (cur->val == cur->next->val) cur->next = cur->next->next; // 后一个结点重复:把它摘掉;cur 不动 else cur = cur->next; // 后一个结点不同:cur 前进 } return head; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[1,1,2]]  →  [1,2]
[[1,1,2,3,3]]  →  [1,2,3]
[[]]  →  []
[[1]]  →  [1]
[[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, 1, 1, 2, 3, 3]

下面的演示用 [1, 1, 1, 2, 3, 3]。cur 指向第一个 1:下一个也是 1,摘掉,cur 不动;下一个仍是 1,再摘掉,cur 仍不动;下一个是 2,不同,cur 前进;2 与 3 不同,前进;3 与后面的 3 相同,摘掉。结果 [1, 2, 3]。淡色的是被摘掉的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。

删除排序链表中的重复元素:[1, 1, 1, 2, 3, 3](每个值只保留一个)
cur被摘掉的重复结点

    参考与说明

    • 官方题目页:LeetCode 83. 删除排序链表中的重复元素。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:C++ 中被摘掉的结点没有释放内存(这里重点是指针逻辑);链表无序时需要哈希集合的版本。
    京ICP备2026067502号-1京公网安备11010502063487号