双指针的妙用
1、双指针思想
所谓的双指针其实就是两个变量,不一定真的是指针。双指针思想在处理数组、字符串等场景下很常见。
主要用到的双指针有两种,一种是快慢型,即两个指针以不同的速度移动,一个在前,一个在后;另一种是对撞型,即两个指针从数组的两端向中间走,这种也称为相向指针。
下面结合实际的算法题来看双指针思想的运用。
2、删除元素
2.1 原地移除所有数值等于val的元素
LeetCode27. 给你一个数组nums和val,你需要原地移除所有数值等于val的元素,并返回移除后数组的新长度。
要求:不要使用额外的数组空间,你必须仅使用O(1)额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例1:
输入:nums = [3, 2, 2, 3],val = 3
输出:2,nums = [2, 2]
示例2:
输入:nums = [0, 1, 2, 2, 3, 0, 4, 2],val = 2
输出:5,nums = [0, 1, 4, 0, 3]
对于这一题来说,两种双指针思想都可以运用。
第一种:快慢双指针
首先我们定义两个指针slow、fast。slow表示当前位置之前的元素都是有效的,而fast则一直向后找,当slow位置的值是val,且fast位置的值不等于val时,用fast的值去将slow的值覆盖。
即定义两个初始值为0的指针slow和fast,slow之前的位置都是有效部分,fast表示当前要访问的元素。
遍历时fast不断向后移动:
-
如果nums[fast]的值不为val,则将其移动到nums[slow++]处。
-
如果nums[fast]的值为val,则fast继续向前移动,slow先等待。
图示如下:
代码如下:
public static int removeElement(int[] nums, int val){
int slow = 0;
//fast充当快指针
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
//最后剩余元素的数量
return slow;
}
第二种:对撞双指针
对撞双指针,又叫交换移除,核心思想是从右侧找到不是val的值来顶替左侧是val的值,还是以上面的数组为例:
当left==right时,left以及左侧的就是删除掉2的所有元素了。实现代码如下:
public int removeElement(int[] nums, int val) {
int right = nums.length - 1;
int left = 0;
for (left = 0; left <= right;) {
if ((num[left] == val) && (nums[right] != val)){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
if (nums[left] != val) left++;
if (nums[right] == val) right--;
}
return left;
}
拓展
本题还可以进一步融合上面两种方法,使用对撞双指针加覆盖法。当nums[left]等于val时,将nums[right]位置的元素覆盖nums[left],继续循环,如果nums[left]等于val就继续覆盖,否则才让left++。实现代码如下:
public int removeElement(int[] nums, int val) {
int right = nums.length - 1;
for (int left = 0; left <= right;) {
if(nums[left] == val) {
nums[left] = nums[right];
right--;
} else{
left++;
}
}
return right + 1;
}
从上面的分析中我们也可以发现,快慢型双指针留下的元素顺序与原始序列中的是一致的,而在对撞型双指针中元素的顺序和原来的可能不一样了。
2.2 删除有序数组中的重复项
LeetCode26. 给你一个有序数组nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后的数组的新长度。不要使用额外的数组空间,你必须在原地修改输入数组并在使用O(1)额外空间的条件下完成。
示例1:
输入: nums = [1, 1, 2]
输出:2,nums = [1, 2]
示例2:
输入:nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
输出:5,nums = [0, 1, 2, 3, 4]
本题还是使用双指针,一个指针负责遍历数组,一个指向有效数组的最后一个位置。图示如下:
代码如下:
public static int removeDuplicates(int[] nums){
//slow表示可以放入新元素的位置,索引为0的元素不用管
int slow = 1;
//循环为快指针
for (int fast = 0; fast < nums.length; fast++){
if (nums[fast] != nums[slow - 1]) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
3、总结
双指针的思想在处理一些数组和字符串的问题的时候非常好用,使用什么类型的双指针,只需要看是题目是需要两个指针一起向前走,还是从两头向中间走即可。
在使用过程中注意边界条件,以及遍历的停止条件即可。