题目链接
题目描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:
- 更改
nums数组,使nums的前k个元素包含不等于val的元素。 nums的其余元素和nums的大小并不重要。- 返回
k。
用户评测
评测机将使用以下代码测试您的解决方案:
java
int[] nums = [...]; // 输入数组
int val = ...; // 要移除的值
int[] expectedNums = [...]; // 长度正确的预期答案。
// 它以不等于 val 的值排序。
int k = removeElement(nums, val); // 调用你的实现
assert k == expectedNums.length;
sort(nums, 0, k); // 排序 nums 的前 k 个元素
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有的断言都通过,你的解决方案将会通过。
示例
示例 1
text
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,_,_]
解释:你的函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例 2
text
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,_,_,_]
解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。
注意这五个元素可以任意顺序返回。
你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
解题思路
这道题是数组操作中的经典题目,核心要求是 原地移除 指定元素。与第 26 题不同,这道题的数组 不一定有序,而且元素的顺序可以发生改变。这个条件给了我们更多的解题思路。
本文将介绍三种解法,从暴力解法到两种双指针解法,逐步优化。
方法一:暴力解法(不推荐)
思路
遍历数组,当遇到等于 val 的元素时,向后寻找一个不等于 val 的元素,将两者交换。这样可以保证前面的元素都不是 val,同时统计不等于 val 的元素个数。
Java 代码
java
class Solution {
public int removeElement(int[] nums, int val) {
if (nums.length == 0) {
return 0;
}
int k = 0;
for (int index = 0; index < nums.length; index++) {
if (nums[index] == val) {
// 向后寻找一个非val元素交换过来
for (int i = index + 1; i < nums.length; i++) {
if (nums[i] != val) {
nums[index] = nums[i];
nums[i] = val;
break;
}
}
}
if (nums[index] != val) {
k++;
}
}
return k;
}
}
复杂度分析
- 时间复杂度 :
O(n²),因为每遇到一个val元素,都需要向后遍历寻找非val元素。 - 空间复杂度 :
O(1),只使用了常数级别的额外空间。
缺点
虽然空间复杂度满足要求,但时间复杂度太高,在数据规模较大时会超时,不推荐使用。
方法二:同向双指针(快慢指针)
思路
这种解法和第 26 题的思路类似,使用两个指针 slow 和 fast:
fast指针用于遍历整个数组。slow指针指向下一个应该被覆盖的位置。- 当
nums[fast] != val时,说明这个元素需要保留,将其赋值给nums[slow],然后slow向后移动。 - 当
nums[fast] == val时,跳过这个元素,fast继续向前移动。 - 最终
slow的值就是不等于val的元素个数。
Java 代码
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,表示第一个需要保留的元素应该放在位置0。fast从0开始遍历数组。- 如果
nums[fast] != val,说明当前元素需要保留,将其赋值给nums[slow],然后slow++。 - 如果
nums[fast] == val,说明当前元素需要移除,直接跳过。 - 最终
slow的值就是保留的元素个数。
复杂度分析
- 时间复杂度 :
O(n),只需要遍历一次数组。 - 空间复杂度 :
O(1),只使用了常数级别的额外空间。
优点
- 简单直观,代码量少。
- 保持了元素的相对顺序。
- 适用于需要保持原顺序的场景。
方法三:双向双指针(首尾指针)
思路
题目明确说明 元素的顺序可能发生改变 ,这意味着我们可以用更巧妙的方法。使用两个指针 left 和 right:
left从数组开头向右移动。right从数组末尾向左移动。- 当
nums[left] == val时,将nums[right]的值赋给nums[left],然后right--。这样就把val元素"扔"到了数组末尾。 - 当
nums[left] != val时,left++。 - 当
left > right时,循环结束,left的值就是保留的元素个数。
Java 代码
java
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
if (nums[left] == val) {
nums[left] = nums[right];
right--;
} else {
left++;
}
}
return left;
}
}
Python 代码
python
class Solution(object):
def removeElement(self, nums, val):
"""
:type nums: List[int]
:type val: int
:rtype: int
"""
left = 0
right = len(nums) - 1
while left <= right:
if nums[left] == val:
nums[left] = nums[right]
right -= 1
else:
left += 1
return left
代码说明
left指向数组开头,right指向数组末尾。- 循环条件为
left <= right。 - 如果
nums[left] == val,说明当前元素需要移除,将nums[right]的值赋给nums[left],然后right--。注意此时left不移动,因为新的nums[left]可能仍然是val,需要继续判断。 - 如果
nums[left] != val,说明当前元素需要保留,left++。 - 最终
left的值就是保留的元素个数。
复杂度分析
- 时间复杂度 :
O(n),每个元素最多被访问一次。 - 空间复杂度 :
O(1),只使用了常数级别的额外空间。
优点
- 效率高,减少了不必要的赋值操作。
- 当数组中
val元素较少时,速度会比同向双指针更快。
方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 相对顺序 | 适用场景 |
|---|---|---|---|---|
| 暴力解法 | O(n²) | O(1) | 保持 | 数据规模小 |
| 同向双指针 | O(n) | O(1) | 保持 | 需要保持顺序 |
| 双向双指针 | O(n) | O(1) | 可能改变 | 顺序无关紧要 |
总结
这道题是双指针技巧的又一经典应用。通过对比三种解法,我们可以看到:
- 暴力解法虽然思路简单,但效率太低,不推荐使用。
- 同向双指针是最直观的解法,保持了元素的相对顺序,代码简洁易懂。
- 双向双指针利用了题目中"顺序可以改变"的条件,在某些场景下效率更高。
关键点回顾:
- 同向双指针 :
slow和fast都从0开始,遇到非val元素就赋值给slow位置。 - 双向双指针 :
left从开头,right从末尾,遇到val就用right元素覆盖。 - 两种双指针解法的时间复杂度都是
O(n),空间复杂度都是O(1)。
掌握这两种双指针技巧,对于解决类似的数组原地操作问题非常有帮助。建议读者多做几道相关题目,加深理解。