给定一个升序链表,删除所有重复的元素,使每个元素只出现一次(重复的保留一个)。因为链表有序,重复的元素一定挨在一起,所以只需要比较相邻的两个结点:如果当前结点 cur 和它的下一个值相同,就把下一个结点摘掉(cur.next = cur.next.next);不同才让 cur 前进。这里有一个很容易写错的细节:摘掉之后 cur 不能前进,因为新的下一个结点可能还是重复的。本文用真实数据展示这个错误会错多少,给出五种语言的实现,并把逐个摘除的过程做成分步演示。
完整题目见官方页面(链接在文末)。链表在测试里用整数数组表示。基准解是数组去重(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 摘掉第一个重复之后已经前进到了下一个结点,剩下的重复就被漏掉了。下面是这种写法与 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
$ 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; } }
$ 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 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; }
$ 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; } };
$ 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]。cur 指向第一个 1:下一个也是 1,摘掉,cur 不动;下一个仍是 1,再摘掉,cur 仍不动;下一个是 2,不同,cur 前进;2 与 3 不同,前进;3 与后面的 3 相同,摘掉。结果 [1, 2, 3]。淡色的是被摘掉的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。