什么是双指针?
顾名思义,就是俩变量,特点是一个跑的快,一个跑得慢。通过一个快指针和一个慢指针在一个for循环下完成两个for循环的工作。
先来看看什么场景可以使用双指针
- 过滤字符串/数组,找出符合条件的元素
双指针使用方式
双指针使用方式一: 只定义一个指针slow,将i作为快指针(正常指针)
这种指针适用于什么场景呢?通过我少量的钻研,总结:不需要排序,单纯的把不想要的元素替换或者挪位或者删除。且看下面题 1. 示🌰1:去除不想要的后数组的长度(力扣27题)
- 题目:给你一个数组
nums和一个值val,你需要 原地移除所有数值等于val的元素,并返回移除后数组的新长度。 - 不要使用额外的数组空间,你必须仅使用
O(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],也会被视作正确答案
先来分析一波:要求原地移除,不能创建新数组。把不想要的数据干走行,那不就是不需要排序嘛,然后把不要的东西挪到后面或者改掉他。(元素x不要&无排序)因此:咱们优先选择
slow+i的双指针模式
时间复杂度,一个循环,所以O(n),空间复杂度O(1)
var removeElement = function(nums, val) {
let slow = 0;// 身为绿茶,我要跟你i保持一致先让你知道我的好
for(let i=0;i<nums.length;i++) {
if(nums[i]!=val) {
// slow开始装了说,你真棒,我要向你靠近,同时把你给我的钱给我守护的人(i哭了,原来我是舔狗)
nums[slow] = nums[i];
slow++;
}
}
// 那么新长度是多少呢?看s前进了多少步就好了,因为符合条件,才++
return slow
}
总结 这种题的思想就是我不想要你啊val,你不要过来啊。然后诱惑
i去看看数据里有没有val,没有那i太棒了,我要向你前进一步,并且让我(slow)守护的人nums[slow]拿到你的好东西nums[i]。
2. 示🌰2将不想要的移动到末尾(力扣283题)
-
题目:移动0
-
给定一个数组
nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。 -
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
(元素x不要&无排序&移位)因此:咱们优先选择
slow+i的双指针模式。通过你对上面题目的分析,你会不会有如下代码的想法
var moveZeroes = function(nums) {
let slow = 0;
for(let i=0;i<nums.length;i++) {
if(nums[i]!=0) {
nums[slow] = nums[i]
slow++
}
}
for(let j=slow;j<nums.length;j++) {
nums[j] = 0
}
return nums
};
但是这样不讲武德,题目是移动,不是让你在那替换
你说
可我想说
但迫于生活压力,我不得不正儿八经移动(圈起来要考)
移动到后面,其实就是在交换位置,我想要的我都拿来,但是因为不能丢,只能先交给i保管一下
于是正儿八经分析:如果i的值合我心意,身为绿茶的我(
slow)还是会像你靠近,只是这个i对不起了,你先委屈一下;因为你太安全了,我必须要把我守护的人的值给你,你把你的优秀的值给我,因为0不能扔,我必须得把0给你。呵,女人。最后i走过终点,0也走向了终点。
可以看到指针slow都是在符合条件的时候(
i!=x)向i靠近,那意味着我们下次遇到这种位移或者替换操作时,可以对slow进行绿茶的初步设定
其实和上面的题区别在于挪动不符合条件的,这样我们就可以将符合条件的(
i!=x)给slow的同时,将nums[i]变成nums[slow]
var moveZeroes = function(nums) {
let slow = 0,fast=0,right=0;
for(let i=0;i<nums.length;i++) {
if(nums[i]!=0) {
let temp = nums[slow];
nums[slow] = nums[i];
nums[i] = temp
slow++;
}
}
return nums
};
3. 示🌰3:去除#字符前面的字符(力扣844题)
- 题目:给定
s和t两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回true。#代表退格字符。 - 注意:如果对空文本输入退格字符,文本继续为空。
输入: s = "ab#c", t = "ad#c"
输出: true
解释: s 和 t 都会变成 "ac"。
分析
- 删除的是
#前面的和#,后面的是不变的,所以我们倒序遍历; - 肯定不能遍历完整个
s后,和t后再进行s和t的比较,不符合算法的快准狠; - 肯定是
s掏出来一个可比的,t掏出来一个可比的进行比较; - 什么是可比的呢?
- 该元素是不会被
#删除的元素
- 该元素是不会被
- 什么是不可比的呢?
- 该元素会被
#删除
- 该元素会被
- 由此可见,我们需要知道元素是否需要被删除,于是给
s和t分别整一个变量存储需要删除的元素的个数,有的话,不拿这个来比,没有的话,上比较板子 - 什么时候结束比较?当两个都比较到了头
var backspaceCompare = function(s, t) {
let i = s.length - 1, skipS=0;
let j = t.length - 1, skipT=0;
while(i>=0||j>=0) {
if(s[i]=='#') {
skipS++;
i--;
continue;
} else if(skipS>0) {
skipS--;
i--;
continue;
} else {
// 拿去比较
}
if(t[j]=='#') {
skipT++;
j--;
continue;
} else if(skipT>0) {
skipT--;
j--;
continue;
} else {
// 拿去比较
}
if(s[i]!=t[j]) {
return false
} else {
i--;
j--;
}
}
return true
};
双指针使用方式二: 定义left,right指针,i照样循环(循环的目的是改变i的值)
这种指针适用于什么场景呢?通过我少量的钻研,总结:需要排序,并且当前给到的数组是有排序规律的。且看下面题 1. 示🌰1:有序数组的平方(力扣977题)
- 题目:给你一个按 非递减顺序 排序的整数数组
nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
分析
- 数据依次递增,只有存在负数才可能会有挪位的可能,也就是最左边和最右边绝对有一个更大,所以我们左右比。这个特点是本题的关键,因为只有左比右大的可能,所以我们在左右比之后就能拿到第一大、第二大的值,避免了其他情况我们还要把一次比较后的大值再次拿出来进行比较
- 左右比有一个特点,就像二分法一样缩小了比较次数,因为一次比较拿到两个值,那么意味着我们可以有快的方法只比较大概数组长度1半的次数就能完成这道题
- 照着这个思路,那循环条件一定不能是i的循环啦,如二分法一样,我们将循环条件弄成
left<=right - 两两比较必有一大,谁大谁能够满足
i想要的,所以这里的i我们从nums.length-1开始。大的值给到i,之后谁大谁就有资格向i靠近,于是left++,right--意味着靠近。
var sortedSquares = function(nums) {
let left=0,right=nums.length-1,i=nums.length-1,arr=[];
while(left<=right) {
let leftNum = Math.pow(nums[left],2);
let rightNum = Math.pow(nums[right],2);
if(leftNum<rightNum) {
arr[i] =rightNum;
right--;
} else {
arr[i] = leftNum;
left++;
}
i--;
}
return arr
};
看了下leetcood底下的评论,我也想说俺也一样。
总结
- 双指针的使用场景有很多,如寻找一个数组中满足条件的最短子数组(滑动窗口);他们终究是通过两个变量的挪动去解题,关键在于如何利用双指针去解题
- 不需要排序,单纯的把不想要的元素替换或者挪位或者删除——
i+slow指针 - 需要排序或者不能改变原数组的考虑使用
left+right - 滑动窗口的自己琢磨吧,也是
left+right,就是left++, right++/--需要好好想想