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
执行过程:
| right | nums[right] | 是否保留 | left | nums 前部 |
|---|---|---|---|---|
| 0 | 0 | 是 | 1 | [0] |
| 1 | 1 | 是 | 2 | [0,1] |
| 2 | 2 | 否 | 2 | [0,1] |
| 3 | 2 | 否 | 2 | [0,1] |
| 4 | 3 | 是 | 3 | [0,1,3] |
| 5 | 0 | 是 | 4 | [0,1,3,0] |
| 6 | 4 | 是 | 5 | [0,1,3,0,4] |
| 7 | 2 | 否 | 5 | [0,1,3,0,4] |
最终返回 left = 5。
七、为什么这道题不需要交换?
有些双指针题会用“左右交换”,但这里不需要,原因是:
- 题目不要求保留原有顺序之外的任何信息
- 我们只关心“哪些值留下”
- 覆盖写入比交换更简单、更稳定
这类写法也被称为:
慢指针构造结果,快指针遍历输入
八、时间与空间复杂度
-
时间复杂度:
O(n)- 每个元素只看一次
-
空间复杂度:
O(1)- 原地修改,没有额外数组
九、这道题在整个双指针体系里的位置
这是最基础、最干净的双指针模型:
- 没有排序
- 没有边界博弈
- 没有复杂条件
但它是下面这些题的“地基”:
- 移除重复元素
- 移动零
- 有效数组长度类问题
- 原地过滤问题
一句话总结它的思想:
用一个指针遍历世界,用另一个指针构造答案。
十、最后的小总结
这道题的难点不在代码,而在观念转变:
- 不要执着于“删除”
- 把问题转化为“保留什么”
- 用指针去描述“状态变化”
当你真正理解了这道题,
后面很多数组题都会突然变得顺眼。