力扣刷题#16:LeetCode0083-删除排序链表中的重复元素
题目
给定一个已排序的链表的头 head,删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。
输入:head = [1, 1, 2]
输出:[1, 2]
输入:head = [1, 1, 2, 3, 3]
输出:[1, 2, 3]
初始思路
看到"已排序 + 删除重复",我立刻想到了 LeetCode 26(删除有序数组中的重复项)——那是用快慢指针做的。链表版的逻辑应该是一样的:cur 停留在不重复的位置,next 往前探,探到不同值就接上去。
思路是清晰的,但在写链表代码时踩了一串坑。
踩坑记录
坑 1:用 . 访问指针成员
while(next.val != cur.val ...) // ❌ next 是指针
tmp.next // ❌
cur.next // ❌
ListNode* 是指针类型,访问成员要用 ->,不是 .。
while(next->val != cur->val ...) // ✅
tmp->next // ✅
cur->next // ✅
数组值直接 .(比如 cur.val 在结构体变量上),指针走 ->。第一阶段的数组题没有指针,这是切换到链表后遇到的第一个语法问题。
知识点总结:. 和 -> 的区别
这是 C++ 里的一个基础语法,通俗地讲:
| 写法 | 用在什么上 | 原因 |
|---|---|---|
变量.成员 |
普通变量(栈上的对象) | 直接访问 |
指针->成员 |
指针变量 | -> 等价于 (*指针).成员,先解引用再访问 |
生活化类比:
.就像你直接走进自己家里拿东西——你就在家里->就像你手里拿着一张写着地址的纸条,先按纸条找到那栋房子(解引用),再进去拿东西
在链表题里怎么看?
ListNode node(5); // node 是普通变量,用 .
node.val = 5;
node.next = nullptr;
ListNode* p = &node; // p 是指针变量,用 ->
p->val = 5; // 等价于 (*p).val = 5
p->next = nullptr;
力扣给的 ListNode 为什么总是用 ->? 因为力扣传给你的链表是 ListNode* head——它是一个指针,不是变量本身。所以所有成员访问都是 head->val、head->next。
一句话记住:看类型就能判断——类型带
*就用->,不带就用.。
坑 2:循环条件写反了
while(next->val != cur->val && next != nullptr)
[1, 1, 2] 跑一遍,cur=1,next=1:
next->val(1) != cur->val(1)→false- 循环直接跳过,什么都没处理,返回了原链表
[1,1,2]
条件写反了。 我要的是值相同时进去处理重复,不是值不同才进去。
坑 3:先取值再判空
next->val ... && next != nullptr // ❌ next 是 null 的时候先崩了
next != nullptr && next->val ... // ✅ 短路求值,先判空
任何时候指针取值之前,先把 != nullptr 的判断放在前面——C++ 短路求值会先检查这个条件,失败就不会执行后面的取值,避免访问非法内存。
坑 4:cur 跳早了
cur = next; // ← 把 cur 移到了下一个节点
cur 本来指着 1₀,应该用 cur->next = 下一个不同的节点 来跳过重复。但我先让 cur 移到了 1₁,再改链接就没意义了——cur 已经不在正确的位置上了。
cur 只有一个任务:停在当前不重复的位置上,只有删完重复之后才往前走一步。
只处理了值相同的情况,值不同的情况什么都没做——cur 和 next 都原地不动,while 循环永远跳不出去。
值不同时,cur 和 next 都要往前走一步。
最终 AC 代码
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (head == nullptr || head->next == nullptr)
return head;
ListNode* cur = head; // 慢指针:指向保留的最后一个节点
ListNode* next = head->next; // 快指针:向前探
while (next != nullptr) {
if (next->val == cur->val) {
// 值相同:跳过所有重复
ListNode* tmp = cur;
while (next != nullptr && next->val == tmp->val) {
tmp = next;
next = tmp->next;
}
// next 现在指向第一个不同的节点
cur->next = next; // 接上后面的链表
} else {
// 值不同:两个指针都往前走
cur = next;
next = cur->next;
}
}
return head;
}
};
代码逻辑
| 情况 | cur 做什么 | next 做什么 |
|---|---|---|
next->val == cur->val |
不动,等 next 找到下一个不同值后 cur->next = next |
一直往前走,直到找到不同值 |
next->val != cur->val |
往前走一步 (cur = next) |
往前走一步 (next = cur->next) |
关键理解:这道题和 LeetCode 26 是同一道题
| LeetCode 26(数组) | LeetCode 83(链表) | |
|---|---|---|
| 核心逻辑 | cur 慢指针 + 快指针扫描 | 完全一样 |
| 保留条件 | nums[i] != nums[k-1] |
next->val != cur->val |
| 跳过重复 | 快指针自然跳过 | while 循环跳过 |
| 改链接 | nums[k] = nums[i] |
cur->next = next |
| 收尾 | return k | return head |
数组版用下标操作,链表版用指针操作——本质是同一种快慢指针模式,只是语言不同。
链表 vs 数组判断空值
| 数组 | 链表 |
|---|---|
v.empty() |
p == nullptr |
取值前保证 i < v.size() |
取值前保证 p != nullptr |
链表没有 empty() 和 size(),所有判断靠 == nullptr 和 != nullptr。
复杂度分析
| 维度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 每个节点访问一次 |
| 空间复杂度 | O(1) | 只用了几个指针 |
本文档由 AI 辅助生成,作者提供问题,思路和代码,AI仅负责文本修饰,综合获得以上内容。

浙公网安备 33010602011771号