力扣刷题#3:LeetCode26-27-283双指针三连

上回写了 LeetCode 26 的踩坑记录,今天继续。这次是一次"顿悟"的体验——当你突然发现,原来三道题是同一个妈生的。


起因

刷完 26 题(删除有序数组中的重复项)之后,我信心满满地打开 27 题(移除元素)。看了一眼题,感觉差不多,但写出来的代码却乱七八糟:

// 我写的 27 题第一版(又臭又长)
int removeElement(vector<int>& nums, int val) {
    int len = nums.size();
    int k = 1;
    for(int i = 0; i < len; i++){
        if(nums[i] == val){
            k = i;
            for(; i < len; i++){
                if(nums[i] != val){
                    swap(nums[k], nums[i]);
                    i = k;
                    break;
                }
            }
        }
    }
    return k;
}

双重循环、k 同时充当三个角色(初始值、val 位置、返回值)、空数组时返回 1……

说实话,写这段的时候我大脑是过载的。明明感觉应该不难,但就是写不顺。


被点醒:模板早就有了

问题在于:我把 26 题的方法和 27 题的方法当成了两套不同的东西。

其实我从 26 题就已经掌握了正确的模板——只是我自己没意识到。

// 26 题的模板
int k = 1;
for (int i = 1; i < n; i++) {
    if (nums[i] != nums[k - 1]) {   // 发现新元素
        nums[k] = nums[i];
        k++;
    }
}

27 题只是换了一个"保留条件"而已:

// 27 题的正确写法——直接把 26 的模板改一个条件
int k = 0;  // 注意这里从 0 开始
for (int i = 0; i < n; i++) {
    if (nums[i] != val) {           // 发现要保留的元素
        nums[k] = nums[i];          // 或者 swap(nums[k], nums[i])
        k++;
    }
}

把 26 题的 nums[i] != nums[k-1] 换成 nums[i] != val就这一处改动


顿悟时刻

发现这个秘密之后,我又打开了 283 题(移动零),这次只用了 2 分钟:

void moveZeroes(vector<int>& nums) {
    int k = 0;
    for (int i = 0; i < nums.size(); i++) {
        if (nums[i] != 0) {          // 发现非零元素
            swap(nums[i], nums[k]);  // 用 swap 直接把零甩到后面
            k++;
        }
    }
}

通过了。

三道题,同一个模板。区别仅仅在于"保留条件":

题号 保留条件 说明
26 删除重复项 nums[i] != nums[k-1] 和上一个保留的比,不同才要
27 移除元素 nums[i] != val 不等于目标值就要
283 移动零 nums[i] != 0 非零就要

其他部分一字不差。


通用模板

int k = 开始位置;
for (int i = 开始位置; i < 数组长度; i++) {
    if (满足保留条件) {
        // 方式 A:直接赋值
        nums[k] = nums[i];
        // 方式 B:swap(会把不要的甩到后面)
        // swap(nums[k], nums[i]);
        k++;
    }
    // 不满足 → 跳过,什么都不做
}
return k;

慢指针 k:指向"下一个要写入的位置",只在写入时前进。
快指针 i:全数组扫描,每次循环都前进。


深入理解 k:不变量(Invariant)

知道模板还不够,必须理解 k 到底在干什么。不然换个马甲又认不出来了。

k 是一条分界线

nums[0] ... nums[k-1] | nums[k] ... nums[i] ... nums[n-1]
         ↑                              ↑
   已确认保留的部分                   还没处理
   ✅ 全部合规

这条分界线在算法运行期间始终成立,这叫不变量(invariant)——不管 i 走到哪,nums[0..k-1] 永远是已经确认要保留的正确数据。

为什么 k 只能在 k++ 这一个地方改变?

我的第一版代码犯了一个致命错误:给 k 赋值了 k = i

我的第一版:
  初始: k = 1     ← 跟"已保留 1 个"对不上,数组还是空的
  中间: k = i     ← 不变量被破坏!k 不再代表"已保留个数"
  返回: k         ← 空数组时 k=1,返回 1,错了

正确的 k 只能通过 k++ 来增加——每发现一个要保留的元素,就把它放到 nums[k],然后 k++。没有 k = xxx 这种赋值。

用洗澡的比喻

浴室地板 = 数组,水龙头一滴一滴地滴水

快指针 i = 你的眼睛,检查每一滴水
慢指针 k = 干净区域的分界线

规则:
  检查第 i 滴水
  → 是干净的(要保留)→ 放到 k 的位置,k+1
  → 是脏的(重复/val/0)→ 跳过,k 不动

最终 nums[0..k-1] 全是干净的——这就是答案。

k 的初始值怎么定?

要保留什么? k 代表什么 k 初始值 为什么?
26 不重复的元素 已确认的唯一元素个数 1 有序数组,nums[0] 不用比,直接算第一个保留
27 不等于 val 的元素 已保留的非 val 个数 0 还没检查,一个都没确认
283 非零元素 已保留的非零元素个数 0 还没检查,一个都没确认

26 题为什么 k=1 不是 k=0? 因为数组有序且非空,第一个元素 nums[0] 肯定是唯一的,可以直接算作"已保留",所以 k 从 1 开始,i 也从 1 开始扫描。

面试官问怎么解释 k

"我用慢指针 k 维护一个不变量:nums[0] 到 nums[k-1] 永远是已经确认要保留的元素。快指针 i 遍历数组,每次遇到要保留的元素就写到 nums[k] 的位置,然后 k+1。循环结束时 k 就是保留元素的个数,前 k 个位置就是答案。"

能说出 "不变量" 这三个字,说明不是在背模板,是真的理解了。


踩坑总结

1. 同一个变量不要做三件事

我的 k 在第一版里同时做三件事:

  • 初始值 = 1(空数组时返回 1,错了)
  • 记录 val 的位置(被双重循环覆盖)
  • 最后的返回值(空数组时返回 1)

三个角色塞在一个变量里,不 bug 才怪。

2. 写完 26 题后没有总结模板

我做完 26 题就觉得自己"会了",但没把核心思想抽象出来。结果 27 题又重新发明了一次轮子(还把轮子做成了方的)。

如果刷完一道题能花 2 分钟总结一下"这题的核心模式是什么",下一题会快很多。

3. 三连通的快乐

从 26 题折腾半天,到 27 题绕弯路,再到 283 题两分钟通——最大的感受是:

与其每道题都新想一套解法,不如先看看之前做过的题有没有一样的模式。

这个道理听起来简单,但我自己真的经历了"先写废再醒悟"的过程后,才真正记住了。


写于 2026-06-02。从第一道折腾两小时到第三道两分钟,有时候离顿悟就差一层窗户纸。
以上内容由AI辅助生成

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