力扣刷题#2:删除有序数组中的重复项_从标记删除到搬运保留
LeetCode 26 删除有序数组中的重复项 — 一个初学者的踩坑与复盘
这是我刷力扣的一道心路历程。本来以为很简单,结果绕了一个大弯子。记录一下,帮自己记住,也帮看到的人少走弯路。
题目回顾
给你一个升序排列的数组 nums,请你原地删除重复元素,使每个元素只出现一次,返回新的长度 k。
判题系统会这样检查:
// 检查前 k 个元素是否等于预期的有序不重复序列
for (int i = 0; i < k; i++) {
assert(nums[i] == expected[i]);
}
// k 之后的值——不检查
输入输出示例:
输入: nums = [0,0,1,1,1,2,2,3]
输出: k = 4, nums 前 4 个元素为 [0,1,2,3]
后面的元素无所谓
我的错误思路:标记值法
看到题目说"忽略后面的项",我想:既然后面的不管,那把重复的改成 101 不就行了吗?
当时我的代码长这样:
int removeDuplicates(vector<int>& nums) {
int len = nums.size();
int i = 0;
int j = i + 1;
for (; i < len; i++) {
for (; j < len; j++) {
if (nums[i] == nums[j]) {
nums[j] = 101; // 标记为"已删除"
} else {
nums[i + 1] = nums[j]; // 把不重复的挪到前面
nums[j] = 101;
break;
}
}
j++;
}
return i;
}
为什么我觉得它能行?
我的逻辑是:
j从i+1开始往后扫描- 相同就把
nums[j]涂成 101——"删除"它 - 不同就把
nums[j]的值覆盖到nums[i+1],然后把原来的位置涂成 101 j每次循环后++,跳过了已经被涂成 101 的位置- 最后前几个位置应该就是不重复的值,101 在后面的位置,"忽略"掉就行了
看起来好像没什么问题?但提交一次就给我上了一课。
踩坑记录
坑 1:返回值错了
我返回的是 i,但外层 for (; i < len; i++) 结束时,i = len。不管数组里有几个不重复元素,我都返回了原始数组长度 7 或 8。
判题系统: 你说前 k=8 个元素是有序不重复的?我检查一下…… [0, 101, 1, 101, 101, 2, 101, 3] 这哪里不重复了??❌
教训: 必须用一个单独的变量记录"唯一元素的个数",不能用循环变量充数。
坑 2:标记值反过来污染了数据
这是最致命的问题。i 迟早会扫到我亲手涂的 101:
原始: [0, 0, 1, 1, 1, 2, 2, 3]
↑
第一次处理完: [0, 1, 101, ...] 但 i=1 时,nums[1]=101
i=1: nums[1]=101 != nums[2]=2 → 找到一个"新"元素 → k++
101 本来是我用来表示"已删除"的标记,结果我自己不认识它,把它当成合法数据给处理了。
教训: 标记值会污染后续的遍历逻辑。原地算法应该搬运"要保留的",而不是标记"要删除的"。
坑 3:自覆盖
[1, 2] 这种无重复的 case:
// i=0, j=1
nums[0] != nums[1]
nums[0+1] = nums[1]; // nums[1] = 2
nums[1] = 101; // 刚赋完值,立刻改成 101
nums[i+1] 和 nums[j] 指向同一个位置时,先赋值给自己再改成 101,等于把自己抹掉了。
教训: 写入位置和目标位置可能是同一个,要确保这种时候不会出 bug。
正确的解法:快慢指针
经过一番折磨后,我换了一个思路——不标记删除,而是搬运保留。
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if (nums.empty()) return 0;
int k = 1; // 慢指针:指向下一个唯一元素该放的位置
for (int i = 1; i < nums.size(); i++) { // 快指针:扫描
if (nums[i] != nums[k - 1]) {
nums[k] = nums[i]; // 新元素,搬到前面
k++; // 唯一元素数 +1
}
// 相同 → 跳过
}
return k;
}
};
它和我的思路有什么不同?
| 我的错误思路 | 标准快慢指针 | |
|---|---|---|
| 做什么 | 标记"哪些是重复的" | 搬运"哪些是唯一的" |
| 额外变量 | 往数组写 101(污染) | 只写原数组已有的值(无污染) |
| 返回值 | 循环变量 i(不对) | 独立的 k(精确计数) |
| 通用性 | 只对 -100~100 有效 | 任意类型都行 |
| 代码量 | 两重循环 | 一重循环 |
模拟跑一遍
nums = [0, 0, 1, 1, 1, 2, 2, 3]
初始: k=1(指向 index 1,第一个唯一元素是 nums[0]=0)
i=1: nums[1]=0 == nums[0]=0 → 相同,跳过
i=2: nums[2]=1 != nums[0]=0 → nums[1]=1, k=2
i=3: nums[3]=1 == nums[1]=1 → 相同,跳过
i=4: nums[4]=1 == nums[1]=1 → 相同,跳过
i=5: nums[5]=2 != nums[1]=1 → nums[2]=2, k=3
i=6: nums[6]=2 == nums[2]=2 → 相同,跳过
i=7: nums[7]=3 != nums[2]=2 → nums[3]=3, k=4
结果: 前 k=4 个元素 [0, 1, 2, 3],返回 4 ✅
学到的知识点
1. 快慢指针模板
慢指针 k:指向"下一个写入位置",只在有写入时前进
快指针 i:扫描全数组,每次循环都前进
这个模板可以通杀好几道题:
| 题号 | 思路变化 |
|---|---|
| 27. 移除元素 | 把指定值 val 覆盖掉,慢指针写入"非 val" |
| 283. 移动零 | 慢指针写入"非零",最后补零 |
| 80. 删除有序数组中的重复项 II | 比较 nums[k-2](最多保留两个) |
2. "原地"不是万能的
原地 ≠ 可以随便往数组里写东西。改造数组时,如果你写入了原数组不存在的值,这些值迟早会变成你的逻辑负担。
3. 写代码前先手工跑一遍
这个教训最贵——如果我在写代码前先拿 [1,2]、[1,1,1] 这种边界 case 手工推一遍,早就能发现 bug,不用反复提交失败。
总结
这题本身不难,但我花了三次提交才彻底搞明白问题出在哪。核心就一句话:
不要执着于"标记删除",而是要聚焦于"搬运保留"。
快慢指针不仅解决了 LeetCode 26,它背后的思想——两个指针各司其职、不同步前进——在很多数组题里都能复用。接下来我会按这个模板把 27、283、80 一起刷掉,一道通三道。
本文档有ai辅助生成元素,作者提供问题和思路,综合获得以上内容

浙公网安备 33010602011771号