关于双指针你必须知道的事
前言
对于双指针题型,大家肯定都不陌生:快慢指针,头尾指针,二分查找等等,所以一般双指针题型都会出现在哪里呢?
“
数
组
”
\color{red}{“数组”}
“数组”。因为数组的结构特点,它可以把双指针的特点和考点发挥的淋漓尽致。
所以今天我们的题型讲解也主要是针对于数组内容。下面开始上干货!
一丶模板讲解
双指针题型的坑点挺多,但是按照指针走向,我们可以大致分为两种。一种是同向,一种是异向。今天的模板也是大致根据这两种来进行。
(1)同向指针
首先就是同向指针
关于同向指针,我们要把数组分为这三部分。
[0,i),在这个区间的数据代表处理过并且我需要的数据,[i,j)这个区间代表我处理过但是不需要的数据,[ j , arr.length)代表我们目前没有处理过的数据。
但是这里注意了!! 这 里 区 间 的 开 和 闭 是 需 要 根 据 题 目 来 确 定 的 \color{red}{这里区间的开和闭是需要根据题目来确定的} 这里区间的开和闭是需要根据题目来确定的,我们不能一概而论全。但是总体思想是一致的。而且根据这种方法处理过的数组,它的稳定性不会被破坏。这里的稳定性指的是相对位置。
这里的解题步骤大致就是两步:
1.定义两个指针i和j,并且i和j一般都等于0。
2.while j < arr.length:
if 如果说我们需要arr[j],那么我们就让arr[i] = arr[j],然后i往前走
一步,让它指向下一个位置。
else 如果不需要j这个元素,那么我们就跳过arr[j]这个元素,让j指向下一
个位置,同时不需要改变i的位置。
当然,这里的 if 后面的 arr[i] = arr[j]并不固定,根据题目来确定。
(2)异向指针
接着就是异向指针
异向指针还是把数组分为三部分
[0,i)和(j,arr.length)都是已经处理好的数据,是我们需要的,[i,j]是我们待处理的元素。
这里注意,我们异向双指针后的数组元素是不具有稳定性的。也就是相对元素位置会改变。
这里的解题步骤还是可以大致分为两部分。
1.定义双指针,i = 0,j = arr.length - 1.
2.while i <= j:
根据arr[i] 和 arr[j]的值来确定你接下来要做什么。
移动至少一个指针向他们自身的方向
这里对上面的内容进行说明:这里根据arr[i] 和 arr[j]的元素来确定你要做什么,意思就是你要根据题目的内容,然后确定他们自身的动作,比如说:交换。动作做完之后,再移动至少一个指针。
二丶考点说明
这里的考点说明用单个题来讲有点笼统,所以就抛出对应的题目,然后题目涉及到了的考点,我来说明。
初级考察
(1)344. 反转字符串
这个题就很简单了,我们直接套异向指针的模板就行。
class Solution {
public void reverseString(char[] s) {
int i = 0,j = s.length - 1;
while(i < j){
char tmp = s[i];
s[i] = s[j];
s[j] = tmp;
i++;
j--;
}
}
}
这里的就是
i 和 j 操作考察。但是注意了,我这里写的是 i < j,不是 <= ,这里不固定,并不影响题目。这道题先上手一下异向指针。
(2)26. 删除有序数组中的重复项
这道题就是对于细节的考察了。如果说是一个乱序的数组,那么暴力遍历就可以解决。但是这是有序数组,而且你去掉重复元素之后元素相对位置也是不变的。所以这道题我们就可以使用同向双指针处理。
class Solution {
public int removeDuplicates(int[] nums) {
int cur = 0;
for(int i = 0;i < nums.length;i++){
if(nums[i] != nums[cur]){
cur++;
nums[cur] = nums[i];
}
}
cur++;
return cur;
}
}
这道题哪里的点需要注意呢?(注意这里的 i 和 j 说的是模板中的标记方式)
[0,j]是我们处理过的元素集合,而在这段元素集合中,[0,i]是我们需要的元素,也就是不重复元素。而(i,j]是我们不需要的元素,也就是相同的元素。所以我们的条件就很清楚,arr[i] != arr[j],但是arr[i + 1] == arr[j]。
所以这里考察的点有两个:
1.
关
于
开
区
间
和
闭
区
间
的
选
择
\color{red}{1.关于开区间和闭区间的选择}
1.关于开区间和闭区间的选择
2.
关
于
i
和
j
元
素
前
移
的
判
断
\color{red}{2.关于i和j元素前移的判断}
2.关于i和j元素前移的判断
(3)1047. 删除字符串中的所有相邻重复项
首先一拿到题肯定第一思路就是同向指针,因为这题内涵就是元素一定是要相对稳定。虽然这题比较特殊,但是大体思路和上面一致,我们还是调用模板套用就好啦。但是细节处肯定是要注意的。
class Solution {
public String removeDuplicates(String s) {
int i = -1;
int j = 0;
char[] arr = s.toCharArray();
while(j < arr.length){
if(i >= 0 && arr[i] == arr[j]){
i--;
}else{
i++;
arr[i] = arr[j];
}
j++;
}
return String.copyValueOf(arr,0,i+1);
}
}
然后说一下这道题特殊的点:
1.关于初始 i 和 j 的下标,这里注意我定义的是 i 是 -1,我这样定义的目的是什么呢?对,就是[0,i]元素是处理过的并且是我们需要的元素。而(i,j]是我们处理过的不需要的元素。
2.就是关于 i 的移动,可以发现,当i位置的元素不是我们需要的元素时候,它不是不动,而是往后移一位。
所以这题考点如下:
1.
关
于
开
区
间
和
闭
区
间
的
选
择
\color{red}{1.关于开区间和闭区间的选择}
1.关于开区间和闭区间的选择
2.
关
于
i
下
标
移
动
的
选
择
,
以
及
移
动
的
条
件
判
断
\color{red}{2.关于 i 下标移动的选择,以及移动的条件判断}
2.关于i下标移动的选择,以及移动的条件判断
(4)80. 删除有序数组中的重复项 II
注意了,这道题和leetcode的26题还是有点相似的,但是多了一个细节,就是让每个元素最多出现两次,这就有一个前提的隐含条件。
如果说元素个数 <= 2,那么直接就返回数组本身的长度就好啦。
那么继续讨论,如果说我们想要每个元素最多出现两次,那么前提条件是什么呢? arr[j] != arr[i - 1] 或者说 arr[j] != arr[i - 2]。这里i - 1还是 i - 2要根据我们i元素是开区间还是闭区间来选择。那么如果等于的时候我们怎么办呢?那么就让 j往前走,知道 arr[j] != arr[i - 2]的时候停止,然后把arr[j]的值赋值给arr[i]。
(注意:这里其实思路很清晰了,如果我们选择用 i - 1作为判断条件,也就是i为闭区间的时候,代码会变得很麻烦,所以我们要选择用 i - 2,上面为了思路完整和逻辑的严谨所以我才会说两个都可以)
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length < 3){
return nums.length;
}
int i = 2;
int j = 2;
while(j < nums.length){
if(nums[j] != nums[i - 2]){
nums[i] = nums[j];
i++;
}
j++;
}
return i;
}
}
所以考点其实还是很明显:
1.
关
于
开
区
间
和
闭
区
间
的
选
择
\color{red}{1.关于开区间和闭区间的选择}
1.关于开区间和闭区间的选择
2.
移
动
的
条
件
判
断
\color{red}{2.移动的条件判断}
2.移动的条件判断
进阶考察
(5)121. 买卖股票的最佳时机
注意了嗷,这个题还是比较特殊的。因为除了我们的i 和 j 以外,我们还需要一个中间变量,来串联起这个题所有的线索。
但是我们的思路还是不变的,先给代码,然后开始讲解:
class Solution {
public int maxProfit(int[] prices) {
int i = 0;//记录最小价格下标
int max = 0;//记录差价
int j = 0;//用来进行遍历
while(j < prices.length){
if(prices[j] < prices[i]){
i = j;
}else if(prices[j] - prices[i] > max){
max = prices[j] - prices[i];
}
j++;
}
return max;
}
}
是不是感觉if 和 else的判断完全不一样但是我却还说模板能用上有点矛盾?
1.这一题和我们前面的题不一样的是在哪里?我们都是用arr[i]来作为判断条件,但是这题不一样,是用 arr[i] 和 arr[j]的差值来作为判断条件。
2.这一道题[0,i]还可以用“已经是处理好的,并且是我们需要的元素集合”来理解嘛?当然可以,但是不是所有的元素集合,而是其中一组的元素集合。而且我们也只需要其中一对元素集合。
3.那 i 和 j之间的元素集合还可以用“处理过但是我们不需要的元素集合”来理解嘛?当然可以,因为i和j之间任何一对元素的差值都没有[0,i]之间元素差值最大的那个大。
所以这道题考点在哪里呢?
1.
关
于
元
素
i
移
动
的
判
断
\color{red}{1.关于元素i移动的判断}
1.关于元素i移动的判断
2.
动
态
规
划
思
想
的
考
察
\color{red}{2.动态规划思想的考察}
2.动态规划思想的考察
(6)11. 盛最多水的容器
这一题更加特殊了,因为定睛一看,和121题是挺像的,但是仔细一看是完全不一样的。因为这一题增加了一种新的思想的考察,也就是递归思想的考察。
那么先从头说起,如果我们用同向双指针来做的话是一种怎样的情况?
如果是同向双指针的话,可以发现无从下手,甚至如果你仔细思考,可以发现如果是同向双指针其实就是暴力解法。这样的话就违背了我们使用双指针的初衷。
那么就要从两边走起,可是问题又来了?
如果说我们[0,arr.length - 1]对应了一组盛水容量,那么长度为arr.length - 1的底层下标长度就对应了两组,arr.length - 2就对应了四组,最后我们要怎样知道那一组最大呢?
不还是暴力遍历嘛?
那么这个时候就要用上我们的递归思想
把一个大问题一层一层转换为小的问题来进行考察。就比如说,我们知道底层长度为 arr.length对应了一组雨水容量,那么如果说我们能知道arr.length - 1的底层长度对应的水容量大于 或者 说可能大于arr.length对应的水溶量。那么问题是不是就转化为了,已经知道 目前arr.leng -1 水容量最大,求对应的arr.length - 2水容量。
注意,这个时候对应的就不是四组了,而是两组数据,也就是进入了套娃模式。
所以还是要比较嘛?当然不是,看下面的图,文字有点难以理解,用图来讲解能好理解很多。
现在对应的雨水容量是不是就只有这一组?也就是arr.length 对应的水容量就只有一组?
那么我们继续往下看:
这是不是就是arr.length - 1对应的水容量?是不是就是两组?
如果要知道那一组水容量要大于arr.length是不是要比较?当然不是!
注意,我们arr.length - 1两组水溶量的两边其中一遍都用到了arr.length的边长。那么就一个问题需要我们考虑清楚:
arr.length的两条边长,短的那一边对应的水溶量可不可能大于原来的水容量呢?当然不可能,短板效应嘛。只能小于或者等于了。因为能盛水多少是由短的那一边决定了的。所以为了保证不固定,所以那边小我们就要移动那一边。
代码如下:
class Solution {
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxWater = 0;
while(left < right){
int max = Math.min(height[left],height[right]) * (right - left);
maxWater = max > maxWater ? max : maxWater;
if(height[left] > height[right]){
right--;
}else{
left++;
}
}
return maxWater;
}
}
是不是感觉和我们的模板有点类似?
所以这一道题的考点用一句话总结:
用
递
归
思
想
处
理
题
目
方
向
,
用
动
态
规
划
思
想
来
处
理
细
节
。
\color{red}{用递归思想处理题目方向,用动态规划思想来处理细节。}
用递归思想处理题目方向,用动态规划思想来处理细节。
(7)42. 接雨水
先来看,如果说,我们用同向指针来进行处理,我们可以怎样想?
如果arr[i] >= arr[j],那么雨水容量增加arr[i] - arr[j],如果arr[i] < arr[j],那么i = j,然后继续j++。
对吧?乍一看没啥问题,但是有一个很重要前提,就是arr[arr.length - 1]最后一个元素是最大的,这样才可以保证,中途这个变换条件成立,但是很明显,上面图形最后一位元素高度不是最大的,所以会出现什么情况呢?
就是从图中黄色柱子开始,条件全面崩塌。
那么图中的黄色柱子有什么特点呢?没错,就是图中最高的那个柱子。
那么解题思想就出来了
用异向指针,i和j分别从两边开始,像上面我们同向指针处理那样向自己的方向前进,一遍前进,一边处理。
下面给出代码:
class Solution {
public int trap(int[] height) {
int left = 0,right = height.length - 1;
int water = 0;
int leftmax = 0,rightmax = 0;
while(left < right){
leftmax = Math.max(leftmax,height[left]);
rightmax = Math.max(rightmax,height[right]);
if(height[left] > height[right] ){
water += rightmax - height[right];
--right;
}else{
water += leftmax - height[left];
++left;
}
}
return water;
}
}
是不是感觉有点迷?
那来叙述一下:
和11题的整体思路差不多,那一边小那一边就往自己对应的方向前进一步。然后一
直循环,直到left >= right。
具体的考点如下:
1.
动
态
规
划
思
想
的
考
察
\color{red}{1.动态规划思想的考察}
1.动态规划思想的考察
2.
i
和
j
移
动
过
程
中
数
据
的
处
理
\color{red}{2.i和j移动过程中数据的处理}
2.i和j移动过程中数据的处理
3.
移
动
条
件
的
判
断
(
其
实
也
就
是
动
态
规
划
的
考
察
)
\color{red}{3.移动条件的判断(其实也就是动态规划的考察)}
3.移动条件的判断(其实也就是动态规划的考察)
三丶总结
双指针的考点总结其实就一下几个点:
1.
开
区
间
闭
区
间
的
判
断
\color{red}{1.开区间闭区间的判断}
1.开区间闭区间的判断
2.
i
和
j
移
动
过
程
中
数
据
的
处
理
\color{red}{2.i和j移动过程中数据的处理}
2.i和j移动过程中数据的处理
3.
移
动
条
件
的
判
断
\color{red}{3.移动条件的判断}
3.移动条件的判断
但是题目难度上升一般都是在移动条件判断,为什么呢?
因为这里引用重难点动态规划等等题型和处理方法,也就是说,双指针其实也只是其中处理方法的一环而已。