一次吃透「移除元素」:双指针的最朴素、也最重要的形态

15 阅读3分钟

LeetCode 27|简单
核心思想:快慢指针(双指针)
关键词:原地修改、覆盖、不关心顺序


一、题目回顾

给你一个数组 nums 和一个值 val,你需要 原地移除所有等于 val 的元素,并返回移除后数组的新长度。

要求:

  • 不使用额外数组
  • 不关心新数组后面的内容
  • 返回的是“有效元素的长度”

示例:

nums = [3,2,2,3], val = 3
返回 2
nums 前 2 个元素为 [2,2]

二、这道题最容易踩的坑

很多人一开始会纠结:

  • “删元素后数组怎么变?”
  • “是不是要真的把数组缩短?”
  • “后面的值要不要清空?”

但题目其实已经偷偷告诉你答案了:

你只需要保证前 k 个元素是正确的,后面是什么不重要。

这一点非常关键。


三、核心思路:双指针在“做什么”?

我们用两个指针:

  • right:扫描整个数组(读)
  • left:指向下一个“应该被保留下来的位置”(写)

它们的分工非常明确:

right 负责看每一个元素,left 只在“遇到合法元素”时前进。


四、代码实现(Java)

class Solution {
    public int removeElement(int[] nums, int val) {
        int n = nums.length;
        int left = 0;

        for (int right = 0; right < n; right++) {
            if (nums[right] != val) {
                nums[left] = nums[right];
                left++;
            }
        }

        return left;
    }
}

五、逐行拆解这段代码的“真实含义”

1. left 指针的意义

int left = 0;

left 永远指向:

下一个可以放“有效元素”的位置

它不是在找 val,也不是在删除元素,而是在“维护结果数组的长度”。


2. right 指针在干嘛?

for (int right = 0; right < n; right++) {

right 是一个纯扫描指针

  • 从头到尾看一遍数组
  • 不回头、不跳跃
  • 保证每个元素只看一次

3. 为什么只在 != val 时才赋值?

if (nums[right] != val) {
    nums[left] = nums[right];
    left++;
}

这一步非常精髓:

  • 如果当前元素是 val

    • 直接跳过,相当于“删掉”
  • 如果不是 val

    • 把它覆盖写到 left 的位置
    • left 前进,表示有效长度 +1

注意:这是“覆盖”,不是交换。


六、用一个具体例子走一遍

输入:

nums = [0,1,2,2,3,0,4,2]
val = 2

执行过程:

rightnums[right]是否保留leftnums 前部
001[0]
112[0,1]
222[0,1]
322[0,1]
433[0,1,3]
504[0,1,3,0]
645[0,1,3,0,4]
725[0,1,3,0,4]

最终返回 left = 5


七、为什么这道题不需要交换?

有些双指针题会用“左右交换”,但这里不需要,原因是:

  • 题目不要求保留原有顺序之外的任何信息
  • 我们只关心“哪些值留下”
  • 覆盖写入比交换更简单、更稳定

这类写法也被称为:

慢指针构造结果,快指针遍历输入


八、时间与空间复杂度

  • 时间复杂度:O(n)

    • 每个元素只看一次
  • 空间复杂度:O(1)

    • 原地修改,没有额外数组

九、这道题在整个双指针体系里的位置

这是最基础、最干净的双指针模型:

  • 没有排序
  • 没有边界博弈
  • 没有复杂条件

但它是下面这些题的“地基”:

  • 移除重复元素
  • 移动零
  • 有效数组长度类问题
  • 原地过滤问题

一句话总结它的思想:

用一个指针遍历世界,用另一个指针构造答案。


十、最后的小总结

这道题的难点不在代码,而在观念转变:

  • 不要执着于“删除”
  • 把问题转化为“保留什么”
  • 用指针去描述“状态变化”

当你真正理解了这道题,
后面很多数组题都会突然变得顺眼。