当前:LC27 · 移除元素:原地删除 · 快慢指针 · 首次出现于 Day 1 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC27 · Remove Element · 数组 / 双指针

移除元素:留下不等于 val 的一切

删除一个值,不需要真删——用读指针发现、用写指针安放,返回新长度 k。

维护写指针 write 指向有效前缀末尾,读指针 read 扫描全数组:只要 nums[read] ≠ val,就把这个值写到 nums[write] 并前移 write。结束时 write 就是 k。若允许乱序,还有一个更快的「交换到尾部」技巧。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

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

和 LC26 一样,这是「原地 + 返回长度」的契约题:前 k 个元素不包含 val 即可,后面的无所谓。

这篇文章先讲通用的双指针覆盖法,再讲一个「不要求相对顺序」时更快的交换法。

先明确问题

nums = [3,2,2,3],val = 3,返回 2,前 2 位可以是 [2,2]。

nums = [0,1,2,2,3,0,4,2],val = 2,返回 5。

注意:LC26 要求保留相对顺序,本题只要求「不等于 val 的元素都在前 k 位」——是否保留顺序没写死。这给第二种解法留了门。

读指针扫一遍,分拣两类值

核心思想与 LC26 完全一致:读指针 read 从前到后看每个元素,写指针 write 指向有效前缀的末尾。

遇到 nums[read] ≠ val:这是要保留的值,把它写到 nums[write],write 前移。

遇到 nums[read] == val:要移除,跳过,什么都不做。

由于 write ≤ read 恒成立,覆盖不会破坏还没读到的值。

同款这题的骨架和 LC26 一模一样,只是判断条件从「是否重复」换成了「是否等于 val」。
读指针分拣:不等于 val 的搬到前面,等于 val 的跳过
3
[0]
2
[1]
2
[2]
3
[3]
read
write
val = 3命中 val → 跳过

更快的一招:和尾部交换

如果题目不要求保留相对顺序(本题就是这样),可以用「命中就交换到尾部」的技巧。

维护一个右指针 right 指向当前数组的有效尾部。从左往右:

遇到 nums[i] == val,就把 nums[i] 和 nums[right] 交换,right 前移;交换过来的新值可能还是 val,所以要留在原地再比一次。

遇到 nums[i] ≠ val,i 前进。

这个写法里每个 val 只做一次交换,而覆盖法里每个 val 都要跳过、每个保留值都要写一次。交换法最坏 O(n),但省去了很多次写入。

命中 val 就与右端交换:每个 val 只被处理一次
3
[0]
2
[1]
2
[2]
3
[3]
right
val = 3命中 → 与 right 交换

两种写法怎么选

覆盖法:保留元素相对顺序,适用于要求稳定顺序的场景,最坏每个保留值写一次。

交换法:破坏相对顺序,但 val 出现得越多越省写操作;当 val 很少时两者差别不大。

本题两种都行,多数解法默认用覆盖法(更通用)。记住交换法这道「暗门」即可,LC283 移动零就是交换法的变体。

五行 Go:双指针覆盖

solution.goGo
func removeElement(nums []int, val int) int {
write := 0
for _, x := range nums {
if x != val { nums[write] = x; write++ }
}
return write
}

1write 指向下一个写入点。

2遍历每个值 x。

3x ≠ val 就写入并前移;等于 val 则自动被「挤掉」。

4返回 write 即新长度 k。

总结

读指针找出要留的,写指针安排它们的位置——和 LC26 同一条路。

  • 原地删除的本质是「把该留的搬到前 k 位」,不是物理删除。
  • 覆盖法保留顺序,交换法省写操作但破坏顺序。
  • write ≤ read 保证覆盖安全,这是双指针原地题的通用不变量。
同族题目
LC26删除有序数组中的重复项LC283移动零(交换法变体)LC80删除有序数组中的重复项 II