(一)问题描述
leetcode问题地址https://leetcode.cn/problems/remove-element/
给你一个数组 nums
和一个值 val
,你需要原地移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。
示例1:
输入:nums=[3,2,2,3], val=3
输出:2,nums=[2,2,_,_]
解释:你的函数应该返回k=2,并且nums中的前两个元素均为2。你在返回的k个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例2:
输入:nums=[0,1,2,2,3,0,4,2], val=2
输出:5, nums=[0,1,4,0,3,_,_,_]
解释:你的函数应该返回k=5,并且原数组nums的前5个元素被修改为0,1,4,0,3。你在返回的k个元素之外留下了什么并不重要(因此它们并不计入评测)。
提示:
- 0<=nums.length<=100
- 0<=nums[i]<=50
- 0<=val<=10
(二)关键词提取
- 移除数组元素
- k个元素后不参与评测
考验对数组底层的实现的理解:数组的地址空间是连续的,删除一个元素,其他元素的地址也会移动。所以数组的元素是不能被删除的,只能被覆盖。因此即使移除了一个元素,实际数组占用的物理空间是不变的。只不过C++、Java、Python等编程语言会对数组做一个包装,有一个计数器,删除元素之后返回的数组长度会减1。
(三)解题思路
1. 双指针法(快慢指针)
定义快(fast)、慢(slow)两个指针。快指针的作用是找到新数组需要的元素。找到新数组需要的元素,需要赋值给新数组(或者说将新数组的元素值赋值到合适的位置),慢指针就指向新数组的元素应该被赋值的位置。
- 赋值发生的条件:快指针指向的值不等于val。不满足这个条件时,快指针将继续向前移动,直到满足赋值发生的条件。赋值结束后,快指针将继续移动。
- 赋值的位置:慢指针所指向的位置(慢指针就是新数组的下标)。经历一次赋值之后,慢指针的位置才会向前移动。如果快指针当前指向的位置不符合赋值发生的条件(即赋值没发生),那么慢指针将原地等待,直到快指针指向的值不等于val(赋值发生)。
快指针一定会先于慢指针遍历完数组,即先找到值,后找到位置。有k个新元素的值,就一定赋值了k次。所以不需要判断慢指每次指向的值是否等于val,所有新数组需要的元素一定都在移动完成时被赋值到合适的位置。
伪代码:
RemoveElement (nums[0,...,n],val)
//快慢指针法实现数组元素原地移除
//输入:数组nums,需要移除的值val
//输出:删除元素后的数组长度k
slow←0, fast←0
for fast←0; fast<nums.length; fast++
if nums[fast] != val
nums[slow]←nums[fast]
slow++
return slow
2. 相向双指针法
与快慢指针法类似,定义左(left)、右(right)两个指针,分别从数组的开端和结尾开始相向移动。右指针的作用是找到新数组需要的元素。找到新数组需要的元素,需要赋值给新数组(或者说将新数组的元素值赋值到合适的位置),左指针就指向新数组的元素应该被赋值的位置。
- 赋值发生的条件:左指针的值等于val,且右指针的值不等于val。如果不满足前者,说明当前位置不需要移除,左继续向前移动;如果不满足后者,当前右指针指向的元素不能用来赋值,右指针将继续向前移动直到找到新数组的值。
- 赋值发生的位置:左指针等于val时指向的位置。无论是否发生赋值,左指针都会向前移动。
伪代码:
RemoveElement (nums[0,...,n],val)
//相向双指针法实现数组元素原地移除
//输入:数组nums,需要移除的值val
//输出:删除元素后的数组长度k
left←0, right←nums.length-1
while right>=0 and nums[right]==val
right←right-1
while left<=right
if nums[left]==val
nums[left]=nums[right]
right←right-1
while right>=0 and nums[right]==val
right←right-1
left←left+1
return left
(四)易错点
1. 库函数的使用。C++、Java、Python等语言中都有删除数组元素的函数。这个问题不适合直接使用这些函数。如果只使用库函数就能解决这个问题,那这个问题最好不用库函数。
2. k个元素之后的值如何处理。我一开始的做法是将值等于val的元素与不等于val的元素进行交换,即新数组与原数组的元素完全一致,前k个是不等于val的元素,k个之后的全是等于val的元素。其实没有必要移动元素,只要对前k个元素进行赋值就可以了。
3.相向双指针法循环的执行条件和返回值的选择有两种情况:(1)left<right,循环结束后返回left+1; (2)left<=right,循环结束后返回left 。
我第一遍写的是(1),然后出错了。出现错误的用例是[1]。显然(1)会在nums中所有元素都等于val时出错。原因在于方式(1)相当于在统计最后结果时,要包括left所指向的元素;而方式(2)在返回最终结果时是不包括最后left所指向的元素的。当所有元素都等于val时不应当保留任何一个元素,所以(1)才会出错。
下次在写的时候最好单独考虑一下极端情况,这样在确定条件的时候不容易出错。
4. 相向双指针法当中,每次循环要通过while循环让right指向前进方向距离当前位置最近的非val元素。这个操作应该放在if判断之前还是之后。我第一遍的写法是放在之前,我的想法是,在判断和值覆盖开始之前,right的初始位置就应该在第一个非val元素上了,所以应该放在if之前。
出现问题还是在数组中所有元素都等于val时。如果按我第一遍的写法,大循环是执行的,在小循环里right直接移到-1,下面if判断完了又要交换值,会直接报错数组出界。正确的写法是在大循环外先将right调整到合适位置。从第一轮循环开始,每次都在判断完之后再调整right位置。这样下一轮循环开始时,left+1时right已经在合适的位置上了,也不会出现left先加+1再调整right,right已经小于left了循环还要执行一下的情况。
5.一个其他题解里都没写,但是我觉得也应该添加进来的小细节,就是给的那个提示,对数组长度、数组元素和val的取值范围都有规定,那应该提前判断一下,避免不必要的操作。
if(nums.length==0){return 0;}
if(val>50){return nums.length;}
int left=0, right=nums.length-1;