移除元素:留下不等于 val 的一切
删除一个值,不需要真删——用读指针发现、用写指针安放,返回新长度 k。
维护写指针 write 指向有效前缀末尾,读指针 read 扫描全数组:只要 nums[read] ≠ val,就把这个值写到 nums[write] 并前移 write。结束时 write 就是 k。若允许乱序,还有一个更快的「交换到尾部」技巧。
给你数组 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」。
更快的一招:和尾部交换
如果题目不要求保留相对顺序(本题就是这样),可以用「命中就交换到尾部」的技巧。
维护一个右指针 right 指向当前数组的有效尾部。从左往右:
遇到 nums[i] == val,就把 nums[i] 和 nums[right] 交换,right 前移;交换过来的新值可能还是 val,所以要留在原地再比一次。
遇到 nums[i] ≠ val,i 前进。
这个写法里每个 val 只做一次交换,而覆盖法里每个 val 都要跳过、每个保留值都要写一次。交换法最坏 O(n),但省去了很多次写入。
两种写法怎么选
覆盖法:保留元素相对顺序,适用于要求稳定顺序的场景,最坏每个保留值写一次。
交换法:破坏相对顺序,但 val 出现得越多越省写操作;当 val 很少时两者差别不大。
本题两种都行,多数解法默认用覆盖法(更通用)。记住交换法这道「暗门」即可,LC283 移动零就是交换法的变体。
五行 Go:双指针覆盖
func removeElement(nums []int, val int) int {write := 0for _, 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 保证覆盖安全,这是双指针原地题的通用不变量。