力扣刷题#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;
}

为什么我觉得它能行?

我的逻辑是:

  1. ji+1 开始往后扫描
  2. 相同就把 nums[j] 涂成 101——"删除"它
  3. 不同就把 nums[j] 的值覆盖到 nums[i+1],然后把原来的位置涂成 101
  4. j 每次循环后 ++,跳过了已经被涂成 101 的位置
  5. 最后前几个位置应该就是不重复的值,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辅助生成元素,作者提供问题和思路,综合获得以上内容

posted @ 2026-06-02 14:26  STA_running  阅读(8)  评论(0)    收藏  举报