双指针
常见的双指针两种形式,一种是对撞指针,一种是左右指针。
对撞指针
一般用于顺序结构中,也称左右指针。
- 对撞指针从两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐向中间靠近。
- 对撞指针的终止条件一般是两个指针相遇或者错开(也可能再循环内部找到结果直接跳出循环那),也就是:left==right(两个指针指向同一个位置)、left>right(两个指针错开)。
快慢指针
又称龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。
这种方法对于处理环形链表或数组非常有用。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可以考虑使用快慢指针的思想。
快慢指针发实习方式右很多种,最常用的一种就是:
- 在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢。
示例题目
1.移动零. - 力扣(LeetCode)
解法(快排的思想,数组划分区间-数组分两块):
我们可以用一个cur指针来扫描整个数组,另一个dest指针用来记录非零序列的最后一个位置。根据cur扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。在cur遍历期间,使得[0,dest]的元素全部是非零元素,[dest+1,cur-1]的元素全部是零。
class Solution {
public:
void moveZeroes(vector<int>& nums)
{
int cur=0;
int dest=-1;
for(;cur<nums.size();cur++)
{
if(nums[cur])//处理非零元素
{
swap(nums[++dest],nums[cur]);
}
}
}
};
总结:这个方法是我们学习快排算法的时候,数据划分过程的重要一步。如果将快排算法拆解的话,这一小段代码就是实现快排算法的核心步骤。
2.复写零. - 力扣(LeetCode)
解法(原地复写-双指针):
如果 从前往后 进行复写操作的话,由于0的出现会复写两次,导致没用复写的数被覆盖掉。因此我们选择 从后往前的复写策略。
但是 从后往前 复写的时候,我们需要找到最后一个复写的数,因此我们的大体流程分两步:
- 先找到最后一个复写的数;
- 然后从后往前进行读写操作。
class Solution {
public:
void duplicateZeros(vector<int>& arr)
{
int cur=0,dest=-1,n=arr.size();
//1.先找到最后一个数
while(cur<n)
{
if(arr[cur])
dest++;
else
dest+=2;//arr[cur]等于零,dest需要加等2占用一个复写零的位置。
if(dest>=n-1)
break;
cur++;
}
if(dest==n)
{
arr[n-1]=0;
cur--;
dest-=2;
}
while(cur>=0)
{
if(arr[cur])
arr[dest--]=arr[cur--];
else
{
arr[dest--]=0;
arr[dest--]=0;cur--;
}
}
}
};
3.快乐数. - 力扣(LeetCode)
分析题目得到当我们不断进行替换操作时,计算一定会死循环:
- 情况一:一直在1中死循环,1->1->1......
- 情况二:在历史的数据中死循环,但始终变不到1
由于上述两种情况只会出现一种,因此,只要我们能确定是在情况一还是情况二,就能得到结果。
算法思路:
变化的过程最终会变成循环,也就是一个圈里面,因此可以用快慢指针来解决。快慢指针有一个特性,就是在一个圆圈中,快指针总是会追上慢指针的,也就是说他们总会相遇在一个位置上。如果相遇的位置的值是1,那么这个数一定是快乐数;如果相遇的位置的值不是1,那么就不是快乐数。
class Solution {
public:
int bitsum(int n)//求每个位置的数字平方和
{
int sum=0;
while(n)
{
int m=n%10;
sum+=m*m;
n/=10;
}
return sum;
}
bool isHappy(int n)
{
int slow=n;
int fast=bitsum(n);
while(slow!=fast)
{
slow=bitsum(slow);//慢指针走一步
fast=bitsum(bitsum(fast));//快指针走两步
}
return slow==1;
}
};
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>& h)
{
int l=0;
int r=h.size()-1;
int num=0;
while(l<r)
{
int sum=min(h[l],h[r])*(r-l);
num=max(num,sum);
//移动指针
if(h[l]<h[r])
l++;
else
r--;
}
return num;
}
};
5.有效三角形的个数. - 力扣(LeetCode)
解法(排序+双指针)--利用单调性
思路:先将数组排序。
我们可以固定一个最长边,然后在比这条边小的有序数组中找出一个二元组,使这个二元组之和大于这个最长边。由于数组是有序的,我们可以利用「对撞指针」来优化。
class Solution {
public:
int triangleNumber(vector<int>& nums)
{
sort(nums.begin(),nums.end());
//利用双指针解决问题
int ret=0;
int n=nums.size();
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])
{
ret+=right-left;//证明左右指针中间的数都符合要求,因为数组是有序的。
right--;
}
else
{
left++;
}
}
}
return ret;
}
};
6.查找总价值为目标值的两个商品. - 力扣(LeetCode)
算法思路:本题是升序的数组,因此可以用对撞指针。
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)
return {price[left],price[right]};
else if(price[left]+price[right]>target)
{
right--;//说明最右端的数值太大,需要往左边找
}
else if(price[left]+price[right]<target)
{
left++;//说明左端数值太小
}
}
return {};
}
};
7.三数之和. - 力扣(LeetCode)
解法(排序+双指针)
算法思路:我们可以利用在两数之和那里用的双指针思想,来对我们的暴力枚举做优化:
- 先排序;
- 然后固定一个数 a:
- 在这个数后面的区间内,使用双指针算法快速找到两个数之和等于 -a 即可。
但是要注意的是,这道题里面需要有去重操作:
- 找到一个结果之后,left 和 right 指针要跳过重复的元素
- 当使用完一次双指针算法之后,固定的 a 也要跳过重复的元素。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
vector<vector<int>> vv;
int n = nums.size();
sort(nums.begin(), nums.end());
int i = 0;
while (i<n && nums[i] <= 0 )
{
int left = i + 1;
int right = n - 1;
int m = -nums[i];
while (left < right)
{
if (nums[left] + nums[right] == m)
{
vv.push_back({ nums[i],nums[left],nums[right] });
int l = nums[left];
int r = nums[right];
left++;
right--;
while (left < right && l == nums[left] )//去除重复元素
{
left++;
}
while (right > left && r == nums[right])//去除重复元素
{
right--;
}
}
else if (nums[left] + nums[right] > m)
{
right--;
}
else
{
left++;
}
}
int b = nums[i++];
while (i<n&&b == nums[i])
{
i++;//去除重复的固定元素
}
}
return vv;
}
};
8.四数之和. - 力扣(LeetCode)
解法(排序+双指针)
算法思路:
- 依次固定一个数a
- 在这个数的后面区间上,利用三数之和找到三个数,使三个数的和等于target-a即可。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target)
{
vector<vector<int>> vv;
int n = nums.size();
sort(nums.begin(), nums.end());
int a = 0;
while (a < n)//固定数a
{
int i = a + 1;
while (i < n)//固定数b
{
int left = i + 1;
int right = n - 1;
long long ta = target - nums[a];
long long tb = ta - nums[i];
while (left < right)//双指针
{
int sum = nums[left] + nums[right];
int b = nums[right];
int c = nums[left];
if (sum == tb)
{
vv.push_back({ nums[a],nums[i],nums[left],nums[right] });
right--;
left++;
while (right > left && b == nums[right])
{
right--;//去重
}
while (left < right && c == nums[left])
{
left++;//去重
}
}
else if (sum > tb)
{
right--;
while (right > left && b == nums[right])
{
right--;
}
}
else
{
left++;
while (left < right && c == nums[left])
{
left++;
}
}
}
int d = nums[i];
i++;
while (i < n && d == nums[i])
{
i++;//去重
}
}
int e = nums[a];
a++;
while (a < n && e == nums[a])
{
a++;//去重
}
}
return vv;
}
};