移除元素 图解题解
快慢双指针,把要留的直接写到前面,O(n) 一遍原地移除所有目标值。
就像整理一排柜子,挑出要留下的物品:快指针逐个检查每格,凡是不该扔的,就直接放到慢指针指着的空位,慢指针再前进一格——不是先找出要删的再费力后移,而是把要保留的往前抄,扫一遍就整理好了前缀。
这道题到底在问什么
- 输入
- nums = [3,2,2,3], val = 3
- 输出
- 2 ,前两个位置是 [2,2]
最优解:一步一步想明白
- 3一边扫一边删 val,每删一个,后面的元素都要整体往前挪一格,最坏要挪 n 的平方次;而且删完下标容易跳过下一个元素。换个思路:别删,把要留的写到前面。
- 4fast 从左到右扫每个元素;只要 nums[fast] 不等于 val,就把它写到 nums[slow],然后 slow 前进一格。不变量:nums[0:slow] 永远是已确认要保留的元素,长度就是 slow。
- 5slow = 0,下一个保留元素写到 0 号位slow 等于 0,表示前面干净的前缀现在是空的。fast 还没开始扫,下一个要保留的元素该写到 0 号位。
- 6等于 val → 跳过,slow 不动第一个数就是要移除的 3。它不能进前缀,所以 slow 不动,仍然守着 0 号位,等下一个该保留的元素。
- 7nums[1]=2 不等于 valfast 走到 1 号位,值是 2,不等于 val,要保留。它该被写到 slow 指着的 0 号位。
- 8nums[0] = 2把 2 写到 0 号位,数组变成 [2,2,2,3],前缀第一格定下来了。注意此刻 slow 还没动,下一帧才进位。
- 9写入了 → slow → 1因为放进了一个保留元素,slow 才前进到 1。颜色约定一句话:绿色=这一格刚被写入,变暗=已确认的前缀。现在 nums[0:1] 是有效前缀 [2],下一个该写到 1 号位。
- 10nums[2]=2 不等于 valfast 走到 2 号位,值还是 2,要保留。它该写到 slow 现在指着的 1 号位。
- 11nums[1]=2,slow → 2把第二个 2 写到 1 号位(值刚好相同),slow 再进位到 2。现在前缀 nums[0:2] 是 [2,2],已经攒到两个保留元素。
- 12等于 val → 跳过,slow 不动最后一个数又是 3,要移除。它不写入前缀,slow 保持 2 不动。
- 13return slow = 2fast 扫完整个数组,返回 slow 等于 2。判题只看前两个位置是不是不等于 3 的元素;后面的 [2,3] 是无效尾部,留什么都行。
- 16只要题目说「原地保留一部分元素」,就先问两件事:前缀里该放什么、slow 什么时候才前进。本题的「合格」就是「不等于 val」。
- 18nums=[3,3,3], val=3 → 返回 0极端例子:全是要删的 3。fast 扫完三个都跳过,从没写入,slow 一直停在 0,返回 0。如果你写成「slow 每轮都加」,这里就会错误返回 3。
- 22把这题练到能复述后,去同类题里迁移:先复用 slow 写指针的定义,再只改「合格」条件。想不通时随时问小欧,或到通关训练里实战一遍。
⚠️ 容易写错的地方
✗ 错:循环里直接 pop / del 掉 val
✓ 对:用覆盖写维护有效前缀
删除会让后面元素整体左移,下标容易跳过下一个元素,最坏还退化成 O(n 平方)
✗ 错:slow 每轮都 +1
✓ 对:只有写入时(不等于 val)才 slow += 1
slow 是「下一个该写的位置」,遇到 val 没写入就不能动,否则前缀里会混进脏数据
✗ 错:返回整个 nums 或新数组
✓ 对:返回有效长度 k,也就是 slow
判题只用返回值确定要检查前几个位置,k 之后的内容不看
完整代码(Python / C++ / Java)
Python
class Solution:
def removeElement(self, nums, val):
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slowC++
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0;
for (int fast = 0; fast < (int)nums.size(); fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
};Java
class Solution {
public int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
}套路模板 · 快慢指针「读写分离」(背骨架)
# 「原地保留符合条件的元素」通用骨架
slow = 0 # 写指针:下一个该写的位置
for fast in range(len(nums)): # 读指针:扫每个元素
if keep(nums[fast]): # 合格条件(本题:!= val)
nums[slow] = nums[fast]
slow += 1
return slow # slow 左边即结果,长度 = slow// keep(x) 改成本题条件 x != val 即可
int slow = 0;
for (int fast = 0; fast < (int)nums.size(); fast++) {
if (keep(nums[fast])) { // 合格才写
nums[slow++] = nums[fast];
}
}
return slow; // 长度 = slow// keep(x) 改成本题条件 x != val 即可
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (keep(nums[fast])) { // 合格才写
nums[slow++] = nums[fast];
}
}
return slow; // 长度 = slow复杂度
时间复杂度
O(n)
fast 从左到右扫一遍,slow 只增不减,指针总共只走一遍
空间复杂度
O(1)
只用 slow、fast 两个指针,原地覆盖写,不开新数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 移除元素 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
覆盖写会不会把还没扫到的元素弄丢?+
不会。slow 永远小于等于 fast,写入的位置一定在 fast 当前位置或它左边,那里的值要么已经被 fast 读过、要么是它自己,不会覆盖到未读的数据。
如果要删的元素很少,有没有更省「写次数」的写法?+
有。可以用首尾双指针:碰到等于 val 的就把末尾元素搬过来填,这样不打乱也不要求保序时,移动次数更少(但本题不要求保序,两种都接受)。
这题和 LC26 删除有序数组重复项有什么关系?+
骨架完全一样,都是 slow 当写指针。区别只在「合格」条件:本题是「不等于 val」,LC26 是「不等于上一个保留值」。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。