LeetCode 27. 移除元素(易)

当我们遇到 \(nums[i] = val\) 时,我们可以将当前元素与最后一个元素进行交换,并释放最后一个元素,这实际上使数组的大小减少了 1,最后返回 \(nums\) 的长度就行了。

题目

给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:

1
2
3
4
5
6
7
8
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例

示例 1:

1
2
3
4
5
给定 nums = [3,2,2,3], val = 3,

函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2

你不需要考虑数组中超出新长度后面的元素。
示例 2:
1
2
3
4
5
6
7
给定 nums = [0,1,2,2,3,0,4,2], val = 2,

函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4

注意这五个元素可为任意顺序。

你不需要考虑数组中超出新长度后面的元素。

考察知识点

数组、双指针

核心思想

注意在双指针中,树立有效位指针的概念。

方法一、双指针
\(nums[j]\) 与给定的值相等时,递增 \(j\) 以跳过该元素。只要 $ nums[j] val $,我们就复制 \(nums[j]\)\(nums[i]\) 并同时递增两个索引。重复这一过程,直到 \(j\) 到达数组的末尾,该数组的新长度为 \(i\)

方法二、双指针改进
思路
现在考虑数组包含很少的要删除的元素的情况。例如,num=[1,2,3,5,4],Val=4。之前的算法会对前四个元素做不必要的复制操作。另一个例子是 num=[4,1,2,3,5],Val=4。似乎没有必要将 [1,2,3,5] 这几个元素左移一步,因为问题描述中提到元素的顺序可以更改。
算法
当我们遇到 \(nums[i] = val\) 时,我们可以将当前元素与最后一个元素进行交换,并释放最后一个元素,这实际上使数组的大小减少了 1,最后返回 \(nums\) 的长度就行了。 请注意,被交换的最后一个元素可能是您想要移除的值。但是不要担心,在下一次迭代中,我们仍然会检查这个元素。

LeetCode题解

Python版

一个失败的尝试

无法通过[1], 1样例。在这个写法中,一直想保留要删除的元素,其实直接覆盖就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def removeElement(self, nums: list, val: int) -> int:
if len(nums) == 0:
return 0

j = 0 # 指向等待替换的那个位置
i = 0 # 指向第一个可以替换他的那个位置
is_find = False
# for i in range(len(nums)): # 技术上我们不需要移除那个元素
while (i+1) != len(nums):
if nums[i] == val: # 该位置需要替换
is_find = True
pass # 继续寻找可以用于替换的位置
else:
if j < i: # 该位置可以用于替换且需要替换
nums[j] = nums[i]
nums[i] = val
j += 1 # 不需要替换或者已经替换了 j加1
i += 1

return j if is_find else len(nums)

双指针版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution:
def removeElement(self, nums: list, val: int) -> int:

i = 0 # 有效位指针 保证该指针所指位置及其前面的位置都必须是非删除元素
j = 0 # 遍历指针
# is_find = False
for j in range(len(nums)): # 技术上我们不需要移除那个元素,也不用保存,因为要保存也很麻烦,所以直接覆盖就行。
if nums[j] != val:
nums[i] = nums[j] # 如果是非删除元素元素,就保留原来的值;否则,i就停在这里,等待下一个非删除元素过来覆盖这个删除元素
i += 1 # 最终有n
return i

print("leet code accept!!!")
Input = [[1], [3, 2, 2, 3], [0, 1, 2, 2, 3, 0, 4, 2], [], [2]]
Input1 = [1, 3, 2, 0, 3]
Answer = [[], [2, 2], [0, 1, 3, 0, 4], [], [2]]

if __name__ == "__main__":
solution = Solution()
for i in range(len(Input)):
print("-"*50)
result = solution.removeElement(Input[i], Input1[i])
is_right = True
if result != len(Answer[i]):
is_right = False
for j in range(result):
if Input[i][j] != Answer[i][j]: # 你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
is_right = False
break
print(is_right)

时间复杂度:\(O(n)\)
空间复杂度:\(O(1)\)

双指针思路改进版本

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def removeElement(self, nums: list, val: int) -> int:
i = 0 # 有效位指针 保证该指针所指位置及其前面的位置都必须是有效的
n = len(nums)
while i < n:
if nums[i] == val:
nums[i] = nums[n - 1]
n -= 1
# 当前位置的元素是val,被移到了数组最后,且i没有变化,这意味着,下次还将检查i位置的元素,这样即使移过来的是val,也可以继续排除。
else:
i += 1 # 只有确保当前位置的元素不是val时i才加1

return n

这个改进主要是在要移除数总量比较少的时候体现,就不用遍历到结尾了。
时间复杂度:\(O(n)\)
空间复杂度:\(O(1)\)