目录
双指针的妙用(删除数组元素专题)
由于数组的元素是紧紧依靠在一起的,加入有空隙后面的元素就要整体向前移动。如果在中间位置插入元素,那么其后的元素都要整体向后移动。而很多算法题都需要多轮、大量移动元素,这就导致执行效率地下,解决该问题是数组算法的一个重要问题,其中一种非常好用的方式就是双指针思想。下面,我们将借用双指针来解决有关数组元素删除的专题。
题型一 移除所有数值等于val的元素
给定一个数组nums和一个值val,需要原地移除所有数值为val的元素,并返回移除后数组的新长度。但是要求:不使用额外的数组空间,必须仅用O(1)额外空间并原地修改输入数组。元素的顺序可以改变,不需要考虑数组中超出新长度后面的元素。
由于删除的时候,从删除位置开始的元素都要向前移动,所以如果有多值为val,则会导致反复移动,效率低下。故我们可以使用双指针方式,下面介绍三种方法。
方法一 快慢双指针
定义两个指针,分别为slow和fast,slow之前为数组有效部分,fast表示当前访问的元素。
这样遍历时,fast不断向后移动,遇到的情况有如下两种:
1)如果nums[fast]值不为val,则将其移动到nums[slow++]处,即赋值给nums[slow],slow指针右移
2)如果nums[fast]值为val,则fast继续向前移动,slow指针不动
实现代码如下:
public int removeElement(int[] nums, int val) {
// 快慢指针
int slow = 0;
for(int fast = 0; fast < nums.length; fast++){
// 若nums[fast]不为val,则将其赋给nums[slow],slow指针右移
if(nums[fast] != val){
nums[slow] = nums[fast];
slow++;
}
}
// 返回slow,即fast找到几个不为val的元素,即为新数组长度
return slow;
}
方法二 对撞双指针
该方法同样构建了两个指针,分别为最左边的left和最右边的right。其核心思想是从右侧找到不是val的值来顶替左侧是val的值,还是比较好理解的,我们看看下面的图就会很清楚了。
当lett == right的时候,left及其左侧就是删除掉2后的所有元素了。实现代码如下:
public int removeElement(int[] nums, int val) {
// 对撞指针
int left = 0;
int right = nums.length - 1;
for(left = 0; left <= right;){
// 当左侧为val,右侧不为val,则左右互换
if((nums[left] == val) && (nums[right] != val)){
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
}
// 若左侧不为val,left指针右移,右侧为val,right指针左移
if(nums[left] != val) left++;
if(nums[right]== val) right--;
}
return left;
}
方法三 对撞双指针+覆盖
基于上面的做法,我们做出如下改进。当nums[left]==val时,我们将nums[right]位置的元素覆盖nums[left], 继续循环,如果nums[left]还是等于val那就让nums[right]继续覆盖,否则才让left++,这样代码更加简洁优美,实现代码如下:
public int removeElement(int[] nums, int val) {
// 对撞指针和覆盖相结合
int right = nums.length - 1;
for(int left = 0; left <= right;){
// 只要nums[left]==val,就用nums[right]将其覆盖到不为val
if(nums[left] == val){
nums[left] = nums[right];
right--;
}else{
left++;
}
}
return right+1;
}
题型二 删除有序数组中的重复项
删除重复元素也是很多算法结构中经常出现的。在数组中其最基本的题便为,给定一个有序数组nums,让你原地删除重复出现的元素,使每个元素只出现一次,返回删除数组后数组的新长度。不要使用额外的数组空间,必须在原地修改,并在使用O(1)额外空间的条件下完成。
本题使用双指针最方便,思想其实都是一样的,一个指针负责数组遍历,一个指向有效数组的最后一个位置。为了减少不必要的操作,我们在此会做适当调整,例如令slow=1,因为第一关不需要删除,并且我们笔记的对象为nums[slow-1], 代码如下:
public int removeDuplicates(int[] nums) {
// 双指针
// sLow表示可以放新元素的位置,索引为0可不管
int slow = 1;
for(int fast = 0; fast < nums.length; fast++){
// fast 和 slow-1 对比, slow为调整的
// 若slow与fast所处值不同,则将fast的赋值给slow,因为fast所赋的一定不是重复的
if(nums[fast] != nums[slow - 1]){
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
那既然我们可以重复元素保留一个,那可不可以保留2个,3个或者k个?甚至一个都不要呢,其实也是可以的。其实思路是一模一样的,只是一个换成了k个,代码如下:
public int removeDuplicates(int[] nums) {
if(nums.length < 2){
return nums.length;
}
// 双指针
int slow = 2;
for(int fast = 2; fast < nums.length; fast++){
// 如果fast和slow-2一样,那么fast就一定和slow-1一样,那么就要换slow
// 当fast遍历到不等于slow-2时则替换掉slow,此时就只保留两个重复元素
if(nums[slow - 2] != nums[fast]){
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}