目录
前言
常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。
对撞指针
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
•
对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼
近。
•
对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循
环),也就是:
◦
left == right
(两个指针指向同⼀个位置)
◦
left > right
(两个指针错开)
快慢指针
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。 这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快 慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
•
在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
习题练习
1.移动零. - 力扣(LeetCode)
算法思路
在本题中,我们可以⽤⼀个
cur
指针来扫描整个数组,另⼀个
dest
指针⽤来记录⾮零数序列
的最后⼀个位置。根据
cur
在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。
在
cur
遍历期间,使
[0, dest]
的元素全部都是⾮零元素,
[dest + 1, cur - 1]
的元素全是零。
算法流程
a.
初始化
cur = 0
(⽤来遍历数组),
dest = -1
(指向⾮零元素序列的最后⼀个位置。
因为刚开始我们不知道最后⼀个⾮零元素在什么位置,因此初始化为
-1
)
b.
cur
依次往后遍历每个元素,遍历到的元素会有下⾯两种情况:
i.
遇到的元素是
0
,
cur
直接
++
。因为我们的⽬标是让
[dest + 1, cur - 1]
内
的元素全都是零,因此当
cur
遇到
0
的时候,直接
++
,就可以让
0
在
cur - 1
的位置上,从⽽在
[dest + 1, cur - 1]
内;
ii.
遇到的元素不是
0
,
dest++
,并且交换
cur
位置和
dest
位置的元素,之后让
cur++
,扫描下⼀个元素。
•
因为
dest
指向的位置是⾮零元素区间的最后⼀个位置,如果扫描到⼀个新的⾮零元
素,那么它的位置应该在
dest + 1
的位置上,因此
dest
先⾃增
1
;
•
dest++
之后,指向的元素就是
0
元素(因为⾮零元素区间末尾的后⼀个元素就是
0
),因此可以交换到
cur
所处的位置上,实现
[0, dest]
的元素全部都是⾮零
元素,
[dest + 1, cur - 1]
的元素全是零。
代码实现
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n=nums.size();
int cur=0;int dest=-1;
for(cur;cur<n;cur++)
{
if(nums[cur])
{
swap(nums[++dest],nums[cur]);
}
}
}
};
2. 复写零. - 力扣(LeetCode)
算法思路
如果「从前向后」进⾏原地复写操作的话,由于
0
的出现会复写两次,导致没有复写的数「被覆
盖掉」。因此我们选择「从后往前」的复写策略。
但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两
步:
i.
先找到最后⼀个复写的数;
ii.
然后从后向前进⾏复写操作。
算法流程
a.
初始化两个指针
cur = 0
,
dest = 0
;
b.
找到最后⼀个复写的数:
i.
当
cur < n
的时候,⼀直执⾏下⾯循环:
•
判断
cur
位置的元素:
◦
如果是
0
的话,
dest
往后移动两位;
◦
否则,
dest
往后移动⼀位。
•
判断
dest
时候已经到结束位置,如果结束就终⽌循环;
•
如果没有结束,
cur++
,继续判断。
c.
判断
dest
是否越界到
n
的位置:
i.
如果越界,执⾏下⾯三步:
1.
n - 1
位置的值修改成
0
;
2.
cur
向移动⼀步;
3.
dest
向前移动两步。
d.
从
cur
位置开始往前遍历原数组,依次还原出复写后的结果数组:
i.
判断
cur
位置的值:
1.
如果是
0
:
dest
以及
dest - 1
位置修改成
0
,
dest -= 2
;
2.
如果⾮零:
dest
位置修改成
0
,
dest -= 1
;
ii.
cur--
,复写下⼀个位置。
代码实现
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int n=arr.size();
int cur=0;int dest=-1;
while(1)
{
if(arr[cur])dest++;
else
dest+=2;
if(dest>=n-1)break;
cur++;
}
if(dest==n)
{
arr[dest-1]=0;
dest-=2;
cur--;
}
while(dest>=0)
{
if(arr[cur])arr[dest--]=arr[cur];
else
{
arr[dest--]=0;
arr[dest--]=0;
}
cur--;
}
}
};
3.快乐数. - 力扣(LeetCode)
算法思路
根据上述的题⽬分析,我们可以知道,当重复执⾏
x
的时候,数据会陷⼊到⼀个「循环」之中。
⽽「快慢指针」有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会
相遇在⼀个位置上。如果相遇位置的值是
1
,那么这个数⼀定是快乐数;如果相遇位置不是
1
的话,那么就不是快乐数。
算法流程
如何求⼀个数 n 每个位置上的数字的平⽅和。
a.
把数
n
每⼀位的数提取出来:
循环迭代下⾯步骤:
i.
int t = n % 10
提取个位;
ii.
n /= 10
⼲掉个位;
直到
n
的值变为
0
;
b.
提取每⼀位的时候,⽤⼀个变量
tmp
记录这⼀位的平⽅与之前提取位数的平⽅和
▪
tmp = tmp + t * t
代码实现
class Solution {
public:
void bitsum(int &n)
{
int sum=0;
int k=n;
while(k)
{
sum+=pow(k%10,2);
k/=10;
}
n=sum;
}
bool isHappy(int n) {
int slow=n;
int fast=n;
bitsum(fast);
while(slow!=fast)
{
bitsum(slow);
bitsum(fast);
bitsum(fast);
}
if(slow==1)return true;
else
return false;
}
};
4.盛水最多的容器. - 力扣(LeetCode)
算法思路
设两个指针
left
,
right
分别指向容器的左右两个端点,此时容器的容积 : v = (right - left) * min( height[right], height[left]) 容器的左边界为 height[left]
,右边界为
height[right]
。
为了⽅便叙述,我们假设「左边边界」⼩于「右边边界」。
如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
◦
容器的宽度⼀定变⼩。
◦
由于左边界较⼩,决定了⽔的⾼度。如果改变左边界,新的⽔⾯⾼度不确定,但是⼀定不会超右边的柱⼦⾼度,因此容器的容积可能会增⼤。
◦
如果改变右边界,⽆论右边界移动到哪⾥,新的⽔⾯的⾼度⼀定不会超过左边界,也就是不会过 现在的⽔⾯⾼度,但是由于容器的宽度减⼩,因此容器的容积⼀定会变⼩的。
由此可⻅,左边界和其余边界的组合情况都可以舍去。所以我们可以
left++
跳过这个边界,继续去判断下⼀个左右边界。
当我们不断重复上述过程,每次都可以舍去⼤量不必要的枚举过程,直到
left
与
right
相遇。期间产⽣的所有的容积⾥⾯的最⼤值,就是最终答案。
代码实现
class Solution {
public:
int maxArea(vector<int>& height) {
vector<int>h(height);
int n=h.size();
int begin=0;int end=n-1;
int max_v=0;
while(begin<end)
{
int v=min(h[begin],h[end])*(end-begin);
max_v=max(max_v,v);
if(h[begin]<h[end])begin++;
else
end--;
}
return max_v;
}
};
5.有效三角形的个数. - 力扣(LeetCode)
算法思路
先将数组排序。
我们可以固定⼀个「最⻓边」,然后在⽐这条边⼩的有序数组中找出⼀个⼆元组,使这个⼆元组之和⼤于这个最⻓边。由于数组是有序的,我们可以利⽤「对撞指针」来优化。设最⻓边枚举到 i 位
置,区间
[left, right]
是
i
位置左边的区间(也就是⽐它⼩的区间):
◦
如果
nums[left] + nums[right] > nums[i]
:
▪
说明
[left, right - 1]
区间上的所有元素均可以与
nums[right]
构成⽐nums[i] ⼤的⼆元组
▪
满⾜条件的有
right - left
种
▪
此时
right
位置的元素的所有情况相当于全部考虑完毕,
right--
,进⼊下⼀轮判断
◦
如果
nums[left] + nums[right] <= nums[i]
:
▪
说明
left
位置的元素是不可能与
[left + 1, right]
位置上的元素构成满⾜条件的⼆元组
▪
left
位置的元素可以舍去,
left++
进⼊下轮循环。
代码实现
class Solution {
public:
int triangleNumber(vector<int>& nums) {
int n=nums.size();
int cnt=0;
sort(nums.begin(),nums.end());
for(int i=n-1;i>=2;i--)
{
int left=0;int right=i-1;
while(left<right)
{
if(nums[left]+nums[right]>nums[i])
{
cnt+=right-left;
right--;
}
else
{
left++;
}
}
}
return cnt;
}
};
6.和为S的两个数. - 力扣(LeetCode)
算法思路
a.
初始化
left
,
right
分别指向数组的左右两端(这⾥不是我们理解的指针,⽽是数组的下
标)
b.
当
left < right
的时候,⼀直循环
i.
当
nums[left] + nums[right] == target
时,说明找到结果,记录结果,并且返回;
ii.
当
nums[left] + nums[right] < target
时:
•
对于
nums[left]
⽽⾔,此时
nums[right]
相当于是
nums[left]
能碰到的最⼤值(别忘了,这⾥是升序数组哈~)。如果此时不符合要求,说明在这个数组⾥⾯,没有别的数符合 nums[left]
的要求了(最⼤的数都满⾜不了你,你已经没救了)。
因此,我们可以⼤胆舍去这个数,让
left++
,去⽐较下⼀组数据;
•
那对于
nums[right]
⽽⾔,由于此时两数之和是⼩于⽬标值的,
nums[right]还可以选择nums[left]
⼤的值继续努⼒达到⽬标值,因此
right
指针我们按兵不动;
iii.
当
nums[left] + nums[right] > target
时,同理我们可以舍去nums[right](最⼩的数都满⾜不你,你也没救了)。让
right--
,继续⽐较下⼀组数据,⽽ left
指针不变(因为他还是可以去匹配⽐
nums[right]
更⼩的数的)。
代码实现
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target)
{
int left=0;int right=price.size()-1;
while(left<right)
{
if(price[left]+price[right]>target)
right--;
else if(price[left]+price[right]<target)
left++;
else
return {price[left],price[right]};
}
return {};
}
};
7.三数之和. - 力扣(LeetCode)
算法思路
本题与两数之和类似,是⾮常经典的⾯试题。
与两数之和稍微不同的是,题⽬中要求找到所有「不重复」的三元组。那我们可以利⽤在两数之和
那⾥⽤的双指针思想,来对我们的暴⼒枚举做优化:
i.
先排序;
ii.
然后固定⼀个数
a
:
iii.
在这个数后⾯的区间内,使⽤「双指针算法」快速找到两个数之和等于
-a
即可。
但是要注意的是,这道题⾥⾯需要有「去重」操作~
i.
找到⼀个结果之后,
left
和
right
指针要「跳过重复」的元素;
ii.
当使⽤完⼀次双指针算法之后,固定的
a
也要「跳过重复」的元素
代码实现
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
sort(nums.begin(),nums.end());
int n=nums.size();
vector<vector<int>>s;
for(int i=0;i<n;)
{
if(nums[i]>0)break;
int left=i+1,right=n-1;
int target=-nums[i];
while(left<right)
{
int sum=nums[left]+nums[right];
if(sum<target)
left++;
else if(sum>target)
right--;
else
{
s.push_back({nums[i],nums[left],nums[right]});
left++;right--;
while(left<right&&nums[left]==nums[left-1])
left++;//去重操作
while(left<right&&nums[right]==nums[right+1])
right--;//去重操作
}
}
//对基准元素去重
i++;
while(i<n&&nums[i]==nums[i-1])i++;
}
return s;
}
};
8.四数之和. - 力扣(LeetCode)
算法思路
a.
依次固定⼀个数
a
;
b.
在这个数
a
的后⾯区间上,利⽤「三数之和」找到三个数,使这三个数的和等于
target
- a
即可。
代码实现
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
int n=nums.size();
sort(nums.begin(),nums.end());
vector<vector<int>>ret;
for(int i=0;i<n;)
{
for(int j=i+1;j<n;)
{
long long aim=(long long)target-(nums[i]+nums[j]);
int left=j+1;int right=n-1;
while(left<right)
{
int sum=nums[left]+nums[right];
if(sum>aim)right--;
else if(sum<aim)left++;
else
{
ret.push_back({nums[i],nums[j],nums[left++],nums[right--]});
while(left<right&&nums[left]==nums[left-1])left++;
while(left<right&&nums[right]==nums[right+1])right--;
}
}
j++;
while(j<n&&nums[j]==nums[j-1])j++;
}
i++;
while(i<n&&nums[i]==nums[i-1])i++;
}
return ret;
}
};