三、移除元素
题目:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例1:
| 输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2] 解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。 |
示例2:
| 输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,3,0,4] 解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。 |
提示:
- 0 <= nums.length <= 100
- 0 <= nums[i] <= 50
- 0 <= val <= 100
数组基础理论
数组中的元素没办法删除,只能被覆盖;
加入数组中有5个元素,那么删除一个元素后,数组长度会-1,数组长度返回4,但实际上的空间中还是5(因为Java会提供数组相关的接口,里面有计数器,能够根据删除操作做减法,返回对应的长度,但实际的物理空间不会减小)
库函数的使用
做题时什么时候该用库函数,什么时候不该用?
- 如果题目用库函数直接就解决了,那么建议不要用,很明显是要锻炼解题方法和思路;
- 如果题目中使用库函数仅仅是解决问题的一小步,并且我们知道这个库函数里面的内部实现过程,以及时间复杂度,那么这时是可以使用库函数的。
解题思路分析:
假设有如下图的数组:要删除其中的元素3
谨记,元素不能被删除,只能被覆盖:
最简单的想法:
- 要删除一个元素,就要从后面向前,把每个元素做一个覆盖(这里就是4覆盖3,5覆盖4);
- 那么就需要一个for循环去遍历数组找到元素3,然后再用一个for循环把后面的元素一个一个向前覆盖;

- 这里用的就是两层for循环,属于暴力解法
双指针思路 :
快慢双指针(不改变元素相对位置):
利用双指针思路,去节省一层for循环要做的事情;
定义两个指针:
- 快指针:获取新数组中所需要的元素
- 新数组指的是删除了元素3以后的数组
- 慢指针:获取新数组的下标值(也就是数组中需要更新的元素的位置)
- 根据上图,我们要删掉3,那么快指针指向的就是元素4,慢指针指向的就是元素3(指向需要被覆盖的元素的位置)
代码讲解:
假设有如下图所示的数组:删除元素3
快慢指针一同从0开始移动并更新元素:

快指针第一次找到3:慢指针停在需要更新的位置,快指针走到下一个不是3的位置,完成覆盖,然后,慢指针向后移动一位
快指针第二次找到3:慢指针停在需要更新的位置,快指针走到下一个不是3的位置,完成覆盖,然后,慢指针向后移动一位
代码:
//要删除的目标元素值3
int val = 3;
//慢指针从0开始
int slow = 0;
//快指针也是从0开始,这个for循环完成了快指针的移动操作
for(int fast = 0; fast < nums.length; fast++) {
//快指针指向的元素不等于val的时候,是新数组所需要的元素(因为新数组是不包含目标元素的),此时就需要把快指针指向的元素值,赋给新数组所对应的下标位置(即慢指针所在的位置)
if(nums[fast] != val) {
//快指针用来获取新数组中的元素,慢指针用来获取新数组中需要更新的位置(只是实际操作是在同一个数组上)
nums[slow] = nums[fast];
//这里的元素更新以后,慢指针需要往后移动一个位置,获取新的需要更新的位置
slow++;
}
//当快指针找到目标值3以后,就不做更新操作了(因为新数组中不包含目标值),不做更新操作的话,慢指针就不需要往前移动了,就停在这个位置(按照图示,就停在了元素3所在的位置);
//此时快指针继续往前走,寻找新数组中需要的元素,那么就走到了元素4的位置,4不是要删除的目标值3,因此需要更新,所以此时就相当于把fast指向的4赋值给了slow指向的3所在的位置(完成元素的覆盖,即删除3)
//然后快指针向后移动寻找新元素,慢指针也继续移动到要更新的位置
}
//最终慢指针停留的位置就是新数组的大小
return slow;
总的来说:
快慢指针是一起开始向右移动的,快指针找到的不是3,那么就会直接覆盖慢指针所在位置,接着一同向后移动;
而快指针只要找到3,慢指针就会停下,等快指针找到下一个不是3的元素,然后覆盖到慢指针所在位置,接着继续一同向后移动。
最终慢指针停留的位置就是新数组的大小(因位下标从0开始)

除此之外,还有相向双指针法(一个指针从头开始移动,另一个指针从尾部开始移动),原理十分类似
相向双指针(改变元素相对位置):
定义两个指针:
- 左指针:获取目标值所在的位置
- 从0开始移动
- 右指针:获取新数组包含的元素所在的位置
- 从数组尾部(最后一个元素)开始移动
代码讲解:
假设有如下数组:删除元素3

左指针从0开始移动,寻找等于目标值3的元素;若未找到,则左指针向后移动继续找,右指针不动:


左指针找到了第一个3,将右指针所在位置的值赋给左指针位置,同时右指针向前移动一个位置:


接着左指针继续向后移动,直到找到了第二个3;或者左指针超过了右指针:
这里左指针找到了第二个3,完成一次赋值操作后,右指针向前移动一位,左指针超过了右指针,那么循环结束
这里其实是兼容了右指针指向的值与目标值相等的情况(右指针没有直接跳过目标值)

代码:
// 相向双指针法(版本二)
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 {
// 这里兼容了right指针指向的值与val相等的情况
left++;
}
}
return left;
}
}
此时,左指针停留的位置就是新数组的大小;
相向双指针法(版本一)其实就是右指针直接跳过了目标值的情况,不再赘述,可以自己理解一下,代码如下:
//相向双指针法
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0;
int right = nums.length - 1;
while(right >= 0 && nums[right] == val) right--; //将right移到从右数第一个值不为val的位置
while(left <= right) {
if(nums[left] == val) { //left位置的元素需要移除
//将right位置的元素移到left(覆盖),right位置移除
nums[left] = nums[right];
right--;
}
left++;
//注意,这里易错,容易忘记判断right目前是否等于val
while(right >= 0 && nums[right] == val) right--;
}
return left;
}
}
如果对此过程仍然有疑问,可以自己再多举几个具体的数组例子一步一步移动指针跟踪一下执行步骤
总结:
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
注意: 题目中的要求是原地,也就是要求不使用额外的辅助空间
快慢指针法:不改变元素的相对位置;相向指针法:改变了元素的相对位置
题解:
快慢指针法:

相向双指针法:版本一

相向双指针法:版本二

文章介绍了如何使用快慢指针的方法,在原地修改数组,移除所有数值等于给定值的元素,同时保持O(1)空间复杂度。通过实例和代码讲解了两种双指针策略:快慢指针和相向双指针,展示了它们在处理数组操作中的应用和优势。

887

被折叠的 条评论
为什么被折叠?



