所谓的双指针其实就是两个变量,不一定真的是指针。双指针可以分为快慢双指针(以不同速度向同一边走)、对撞双指针(从两边向中间走)、相反双指针(从中间向两边走)
适用场景:处理数组、字符串问题,比如删除有序数组中的重复项、元素奇偶移动、数组轮转、数组区间、字符串空格替换等。
val
的元素力扣27题,给你一个数组 nums
和一个值 val
,你需要原地移除所有数值等于 val
的元素,并返回移除后数组的新长度。要求:不要使用额外的数组空间,你必须仅使用O(1)
额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
本题可以使用双指针方式,三种都可以来解决。
思想:定义两个指针
fast
和slow
,初始值都为0
,slow
之前的位置都是有效部分,fast表示当前要访问的元素。这样遍历的时候,
fast
不断向后移动:
- 如果
nums[fast]
的值不为val
,则将其移动到nums[slow++]
处。- 如果
nums[fast]
的值为val
,则fast
继续向前移动,slow
先等待。
过程如上图所示,这样的话slow的前面就都是不重复的数字。
// 快慢双指针
var removeElement = function(nums, val) {
let slowPointer = 0;
for (let fastPointer = 0; fastPointer < nums.length; fastPointer++) {
if (nums[fastPointer] !== val]) {
nums[slowPointer] = nums[fastPointer];
slowPointer++;
}
}
// 最后剩余元素的数量
return slowPointer;
};
对撞双指针,也叫交换移除,核心思想是从右侧找到不是val
的值来顶替左侧是val
的值。分别让两个指针left
和right
指向数组首位和末位。当nums[left] !== val
时,left
指针向前移动,当nums[left] === val
时停止移动,right
指针开始向左移动,直到nums[right] !== val
时,用nums[right]
交换nums[left]
的值。重复上述操作,直到left === right
。
当left === right
时,left
以及左侧的值就是删除掉val
的所有数据元素了。
// 对撞双指针,交换左右指针位置的值
var removeElement = function(nums, val) {
let leftPointer = 0;
let rightPointer = nums.length - 1;
for (leftPointer = 0; leftPointer <= rightPointer;) {
if (nums[leftPointer] === val) {
if (nums[rightPointer] !== nums[leftPointer]) {
let tempNum = nums[leftPointer];
nums[leftPointer] = nums[rightPointer];
nums[rightPointer] = tempNum;
leftPointer++;
} else {
rightPointer--;
}
}else {
leftPointer++;
}
}
return leftPointer;
}
这种方法是融合了第一种和第二种方法,它与第二种方法思路是相同的,只不过这种方法是当nums[left] === val
时,就用nums[right]
位置的元素覆盖nums[left]
的值。
// 对撞双指针+覆盖
var removeElement = function(nums, val) {
let rightPointer = nums.length - 1;
for (let leftPointer = 0; leftPointer <= rightPointer;) {
if (nums[leftPointer] === val) {
nums[leftPointer] = nums[rightPointer];
rightPointer--;
} else {
leftPointer++
}
}
return rightPointer + 1;
}
快慢双指针执行完操作后留下的元素顺序与原始序列中的是一致的(参考第一种方法),而在对撞型指针中元素的顺序和原来的可能不一样了(参考第三种方法)。
力扣26题,给你一个有序数组 nums ,请你原地删除重复出现的元素,使每个元素只出现一次 ,返回删除后数组的新长度。不要使用额外的数组空间,你必须在原地修改输入数组 并在使用 O(1) 额外空间的条件下完成。
思路:这一题用双指针最佳,一个指针负责数组遍历,一个指向有效数组的最后一个位置
为了减少不必要的操作,令slow = 1
,并且比较的对象换做nums[slow - 1]
。
// 快慢双指针,数组里只保留一个重复数字
var removeDuplicates = function(nums) {
let slowPointer = 1;
for (let fastPointer = 0; fastPointer < nums.length; fastPointer++) {
if (nums[fastPointer] !== nums[slowPointer - 1]) {
nums[slowPointer] = nums[fastPointer];
slowPointer++;
}
}
return slowPointer;
}
拓展:既然重复元素可以保留一个,也可以保留k
个。(k >= 0
)
力扣80题,给你一个有序数组 nums
,请你原地删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 给你一个有序数组 nums
,请你** 原地** 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1)
额外空间的条件下完成。 并在使用 O(1)
额外空间的条件下完成。
思路:这个是上一题的变种题,就还是用快慢双指针,只不过这次要保留两个重复数字,我们让
slow = fast = 2
,不仅要比较nums[slow - 1]
还要比较nums[slow - 2]
是否与nums[fast]
相等,
// 快慢双指针,数组里数字重复最多不超过两次
var removeDuplicates = function(nums) {
const lengthOfNums = nums.length;
if (lengthOfNums <= 2) {
return lengthOfNums;
}
let slowPointer = fastPointer = 2;
while (fastPointer < lengthOfNums) {
if (nums[slowPointer - 2] !== nums[fastPointer]) {
nums[slowPointer] = nums[fastPointer];
++slowPointer;
}
++fastPointer;
}
return slowPointer;
}
双指针解法在数组、字符串的去重排序问题上真的有很大应用空间,非常有效率。