移除数组里的指定元素,你学会了吗?
大家好,今天我们来聊聊 LeetCode 上的一道基础题------第 27 题:移除元素。
题目看起来很简单:给你一个数组 nums 和一个值 val,你需要原地 移除所有数值等于 val 的元素,然后返回新数组的长度。
而且不能使用额外的数组空间,只能使用 O(1) 的额外空间,也就是只能在原数组上动手脚。
另外还有两个小条件:
- 元素的顺序可以改变
- 新长度后面的元素是什么都无所谓,不用管
举个例子:
text
ini
输入:nums = [3,2,2,3], val = 3
输出:新长度 2,并且 nums 的前两个元素应该是 2, 2
另一个例子:
text
ini
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:新长度 5,nums 的前五个元素为 0,1,3,0,4(顺序无所谓,但必须是这几个数)
是不是感觉很简单?但真正写代码的时候,很多新手会掉进坑里。
先想想为什么要"原地"操作
如果你刚学编程,可能第一反应是:新建一个数组,把不等于 val 的元素放进去,然后复制回来。
但题目明确说了不能用额外数组,因为面试官想考察的是你对数组内存结构的理解。
数组在内存中是连续的一段空间,你没法像链表那样随意删除一个节点。所谓的"删除",本质上是覆盖------把后面的元素往前挪,盖掉要删的那个。
暴力解法:两层循环
最容易想到的办法就是:遍历数组,遇到等于 val 的元素,就把它后面的所有元素整体往前移动一位,覆盖掉它。
这个过程看起来就像这样:
text
ini
初始:[3, 2, 2, 3],val = 3
i=0,发现 nums[0]=3,于是把下标 1~3 的元素依次往前挪:
-> [2, 2, 3, 3],然后 i-- 回退一位,size 减 1
i=0,现在 nums[0]=2,不动
i=1,nums[1]=2,不动
i=2,nums[2]=3,再次触发删除,把下标 3 往前挪:
-> [2, 2, 3, 3](但此时 size 已经是 3,实际只看前三个)
结束,新长度 = 2
代码写出来也很直接:
javascript
ini
var removeElement = function(nums, val) {
let size = nums.length;
for (let i = 0; i < size; i++) {
if (nums[i] === val) {
for (let j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为后面的元素往前挪了,所以 i 也要回退
size--; // 数组有效长度减 1
}
}
return size;
};
这个解法的时间复杂度是 O(n²),空间复杂度 O(1)。虽然 LeetCode 上也能过,但效率不高,尤其是当数组很长、要删的元素很多时,嵌套循环会非常慢。
双指针法(快慢指针):优雅又高效
其实我们完全可以用一个循环完成两件事:
- 一个指针
fast负责遍历原数组,找出"不该删"的元素 - 另一个指针
slow负责指向新数组的末尾,每找到一个"不该删"的元素,就把它放到slow的位置,然后slow++
这样就避免了嵌套循环,时间复杂度直接降到 O(n)。
还是拿例子来看:nums = [0,1,2,2,3,0,4,2], val = 2
- fast=0,nums[0]=0(不等于2) → nums[slow]=0,slow=1
- fast=1,nums[1]=1 → nums[slow]=1,slow=2
- fast=2,nums[2]=2(等于2) → 跳过,slow 不动
- fast=3,nums[3]=2 → 跳过
- fast=4,nums[4]=3 → nums[slow]=3,slow=3
- fast=5,nums[5]=0 → nums[slow]=0,slow=4
- fast=6,nums[6]=4 → nums[slow]=4,slow=5
- fast=7,nums[7]=2 → 跳过
结束。新长度 = slow = 5,数组前五个元素是 [0,1,3,0,4]。
代码非常简洁:
javascript
ini
var removeElement = function(nums, val) {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
};
是不是比暴力解法清爽多了?而且因为只遍历一次,时间复杂度 O(n),空间复杂度 O(1)。
双指针的另一种写法:左右对撞
题目说了元素的顺序可以改变,那其实还有一个更高效的办法,尤其适用于要删的元素很少的情况。
我们可以用两个指针,一个从左边找等于 val 的元素,一个从右边找不等于 val 的元素,然后交换(或者说覆盖)。这样能减少移动的次数。
思路大概是:
-
左指针
left指向开头,右指针right指向末尾 -
当
left <= right时循环:- 如果
nums[left] == val,就把nums[right]的值赋给nums[left],然后right-- - 否则
left++
- 如果
这样最终 left 就是新数组的长度。
代码:
javascript
sql
var removeElement = function(nums, val) {
let left = 0, right = nums.length - 1;
while (left <= right) {
if (nums[left] === val) {
nums[left] = nums[right];
right--;
} else {
left++;
}
}
return left;
};
这种方法的好处是,当要删除的元素很少时,它不会像快慢指针那样把所有保留元素都复制一遍,而是直接把右边的幸存元素挪过来覆盖。不过代价是顺序会被打乱。
总结
这道题虽然简单,但很能体现对数组操作的理解。暴力法容易想到但效率低,双指针法才是面试官想看到的。
我建议大家先自己写一遍暴力法,再改成快慢指针,最后再看对撞指针的写法。这样对"原地修改数组"这件事的理解会更深刻。
如果你正在刷 LeetCode,可以把这道题当作双指针的入门练习,后面碰到类似的三数之和、移除重复元素之类的题目,思路其实是相通的。
好了,今天的分享就到这里。如果觉得有用,可以点个赞或者收藏一下,也欢迎在评论区讨论你的解法~