双指针
常见的双指针有两种形式,一种是对撞指针,一种是快慢指针。
对撞指针
一般用于顺序结构中,也称为左右指针
特点:
- 对撞指针从两端向中间移动。
- 终止条件一般是两个指针相遇或错开,或者在循环内部找到结果后直接跳出循环。
快慢指针
基本思想是使用两个移动速度不同的指针在数组或者链表等序列中移动。
特点:
- 常用于环形的链表或数组中,循环往复问题也可以使用
- 常见做法是:在一次循环中,每次让慢的指针向后移动一位,而快的指针向后移动两位。
实例
移动零

思路:让两个指针将数组划分成两个区域。
设计两个指针:int cur=-1,表示有0区域的开始下标;int des=0,表示有0区域的结尾下标,即(cur,des)范围内都是0
编写思路:当des下标的数不是0时,将cur下标的数与des下标的数交换。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int cur=-1,des=0;
while(des<nums.size())
{
if(nums[des])
{
cur++;
swap(nums[cur],nums[des]);
}
des++;
}
}
};
复写零

整体思路:
- 通过双指针找到原来数组中最后一个刚好复写后等于原来数组长度的下标
- 从后往前进行复写。
双指针:cur用来找到原数组需要复写的最后一个数,des用来表示当复写后的长度大于等于数组下标时刚好找到cur的值。
细节部分:当des等于原数组长度+1时,表示cur的值为0,这时候就需要特殊处理:最后那个0不需要复写,直接让数组最后一个位置等于0并且让des-=2,cur--;
算法流程:
a.初始化两个指针:cur=0,des=-1;
b.利用两个指针找到最后一个需要复写的数:
判断arr[cur]值:如果大于0:des++;否则,des+=2;
c.判断最后一个0是否要复写:
如果不用复写,即des刚好等于arr.size(),按照细节部分进行处理。
d.进行复写部分:
判断arr[cur]值:如果大于0:arr[des]=arr[cur];否则,arr[des--]=0 arr[des--]=0;
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int cur=0,des=-1;
int n=arr.size();
while(des<n)
{
if(arr[cur])
{
des++;
}else
des+=2;
if(des>=n-1)
{
break;
}
cur++;
}
if(des==n)
{
arr[n-1]=0;
cur--;
des-=2;
}//复写
for(;cur>=0;cur--)
{
if(arr[cur])
{
arr[des]=arr[cur];
des--;
}
else
{
arr[des--]=0;
arr[des--]=0;
}
}
}
};
快乐数

整体思路:这个模型是循环的数组,所以这个题目我们使用快慢指针。
这个题目有两种死法:
- 走到后面都是1,一直循环
- 在历史数据中死循环,一直变不到1
算法流程:
a.初始化指针,该题目的指针直接用算出来的结果来表示。
- 定义函数Sum_Squares:求得每个数的每个数字的平方和
- fast=Sum_Squares(n),slow=n;
b.进入循环,让快指针走两步,慢指针走一步:
fast=Sum_Squares(Sum_Squares(fast));
slow=Sum_Squares(slow);
出循环的条件为两指针相遇,相遇代表两指针对应的值是相同的,这时候我们只要判断相遇的那个值是不是1即可。
class Solution {
public:
int Sum_Squares(int n)
{
int sum=0;
while(n>0)
{
int t=n%10;
sum+=t*t;
n/=10;
}
return sum;
}
bool isHappy(int n) {
int fast=Sum_Squares(n),slow=n;
while(fast!=slow)
{
fast=Sum_Squares(Sum_Squares(fast));
slow=Sum_Squares(slow);
}
if(slow==1)
{
return true;
}
else
return false;
}
};
盛水最多的容器

有两种方法:
1.暴力求解
遍历全部组合求取最大值,但是该方法会超时。
class Solution {
public:
int maxArea(vector<int>& height) {
int maxV=0;
int n=height.size();
for(int i=0;i<n;i++)
{
for(int j=i;j<n;j++)
{
maxV=max(maxV,min(height[i],height[j])*(j-i));
}
}
return maxV;
}
};

2.优化暴力解法,运用算法:对撞指针,减少枚举的次数
设计思路:
a.运用两个指针,一个指向数组的开头left,一个指向数组的结尾right.
b.根据题目的要求:求取最大体积,即两个指针对应的最小值乘以两个指针数组下标差的最大值。
V=(right-left)*min(height[left],height[right])
举例假设:假设左边的值比右边的小,那么我们可以通过固定一个边界,去改变另一个边界,水的容积会有以下变化:
1.宽度减小。
2.左边的值小,决定了水的高度。如果改变左边界,新的水位的高度不确定,但肯定不会高于右边柱子的高度,因为右边的柱子要么是比左边柱子高,要么是比左边柱子矮的,容积可能会增加。
3.改变右边的柱子,无论右边的柱子怎么移动,新的水面的高度一定不会高于左边界,也不会超过现在的水面高度,但是宽度减小了,容积也就一定会减小。
所以我们可以通过固定高柱子,改变低柱子的方法,得到最大的容积。
class Solution {
public:
int maxArea(vector<int>& height) {
int n=height.size();
int left=0,right=n-1;
int maxV=0;
while(right>left)
{
int V=min(height[right],height[left])*(right-left);
if(height[left]<height[right])
{
left++;
}else
{
right--;
}
maxV=max(maxV,V);
}
return maxV;
}
};
有效三角形的个数

解题思路:能形成三角形的条件为两边之和大于第三边,或两边之差小于第三边
方法1:
暴力解法:枚举全部的能组成三角形的情况,并依次判断是否能组成三角形。时间复杂度为O(n³)。该方法会运行超时。
class Solution {
public:
int triangleNumber(vector<int>& nums) {
//两边之和大于第三边
//排序
sort(nums.begin(),nums.end());
int n=nums.size();
int count=0;
//固定最大值,即第三边
for(int i=n-1;i>=2;i--)
{
for(int left=0;left<=i-2;left++)
{
for(int right=i-1;right>left;right--)
{
if(nums[left]+nums[right]>nums[i])
{
count++;
}
}
}
}
return count;
}
};
方法2:优化暴力枚举+对撞指针
找规律:先给数组进行排序,根据两边之差小于第三边的特点,我们可以发现,只要一个区间内最大的两条边之差小于第三条边,就可以找到在这个区间内有的多少个有效三角形。
解题流程:
a.对数组进行排序。
b.固定的第三边并定义两个指针。left指针找能符合特点的最小的边,right指针找符合特点的区间中最大的边。
c.判断条件是否满足。满足条件,意味着区间内的值有right-left种可能满足nums[right]+nums[left]>nums[i],此时right位置上的值全部都确定了,让right--,进入下一轮判断否则 nums[left] + nums[right] <= nums[i] :
▪ 说明 left 位置的元素是不可能与 [left + 1, right] 位置上的元素构成满足条件的二元组
▪ left 位置的元素可以舍去, left++ 进入下轮循环
class Solution {
public:
int triangleNumber(vector<int>& nums) {
//两边之和大于第三边
//排序
sort(nums.begin(),nums.end());
int n=nums.size();
int count=0;
//固定最大值,即第三边
for(int i=n-1;i>=2;i--)
{
//双指针找到最小的两条边
int left=0,right=i-1;
while(right>left)
{
if(nums[right]+nums[left]>nums[i])
{
count+=right-left;
right--;
}
else
left++;
}
}
return count;
}
};
和为 s 的两个数字(easy)
LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)

解题思路:运用对撞指针,让两个指针对应的值的和等于target。
算法流程:
a.给数组进行排序。
b.初始化left,right指针。让两个指针分别指向数组的左右两端。
c.当left<right循环中:
当nums[left]+nums[right]>target时,left已经是小的了,那么就说明right对应的值太大了,所以要让right--,才能找到等于target的两个值。
当nums[left]+nums[right]<target时,
对于 nums[left] 而言,此时 nums[right] 相当于是 nums[left] 能碰到的最大值(别忘了,这里是升序数组哈~)。如果此时不符合要求,说明在这个数组里面,没有别的数符合 nums[left] 的要求了(最大的数都满足不了你,你已经没救了)。因此,我们可以大胆舍去这个数,让 left++ ,去比较下一组数据;
那对于 nums[right] 而言,由于此时两数之和是小于目标值的,nums[right] 还可以选择比 nums[left] 大的值继续努力达到目标值,因此 right 指针我们按兵不动。
当nums[left]+nums[right]=target时,记录结果,返回结果。
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
sort(price.begin(),price.end());
int n=price.size();
int left=0,right=n-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 {};
}
};
三数之和

理解题目要求:找到整个数组中全部等于0的三元组。
两处细节:1.i != j、i != k 且 j != k:表明每个下标对应的数值不能重复使用
2.答案中不能包含重复的三元组,即我们要对结果进行去重操作。
方法一:暴力解法
枚举全部可能情况,需要三层循环,时间复杂度为O(n³),会超时。
方法二:优化暴力解法:加对撞指针,而对撞指针只能控制两个数,所以还需要一个循环控制另一个数。
解题方法和和为s的两个数字方法类似,只是和为s两个数字方法的target的值是固定的,而三数之和中,当固定一个数后,求取的值为target-固定的数值。
解题流程:
a.对数组进行排序。
b.固定一个数。比如固定最后一个数,这里可以进行一个小的优化。如果固定的数是小于0的了,那么两个指针left和right对应的值都已经是小于0的了,即使在循环枚举,剩下的组合都不可能等于0了,这时候我们就可以break,跳出循环。
c.在循环中定义两个指针。left=0,right=固定的数的前一个数,这样就不会重复使用下标对应的数了。
在left<right的循环里,有三种情况:
当nums[left]+nums[right]>-nums[i],-nums[i]就是目标值(target),left已经是小的了,那么就说明right对应的值太大了,所以要让right--,才能找到等于target的两个值。
当nums[left]+nums[right]<target时,
对于 nums[left] 而言,此时 nums[right] 相当于是 nums[left] 能碰到的最大值(别忘了,这里是升序数组哈~)。如果此时不符合要求,说明在这个数组里面,没有别的数符合 nums[left] 的要求了(最大的数都满足不了你,你已经没救了)。因此,我们可以大胆舍去这个数,让 left++ ,去比较下一组数据;
那对于 nums[right] 而言,由于此时两数之和是小于目标值的,nums[right] 还可以选择比 nums[left] 大的值继续努力达到目标值,因此 right 指针我们按兵不动。
当nums[left]+nums[right]=target时,记录结果,返回结果。
d.去重。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n=nums.size();
sort(nums.begin(),nums.end());
vector<vector<int>> ret;
for(int i=n-1;i>=2;)
{
if(nums[i]<0)break;
int left=0,right=i-1;
while(left<right)
{
if(nums[left]+nums[right]>-nums[i])
{
right--;
}
else if(nums[left]+nums[right]<-nums[i])
{
left++;
}
else
{
ret.push_back({nums[left],nums[right],nums[i]});
right--;
left++;
while(right>left&&nums[right]==nums[right+1])right--;
while(left<right&&nums[left]==nums[left-1])left++;
}
}
i--;
while(i>=2&&nums[i]==nums[i+1])i--;
//细节部分,不要在for循环上--,while循环的时候已经找到下一个不同的了,
//for循环中i--就会跳过一些数,导致有些数没有被遍历到。
}
return ret;
}
};
练习

和三数之和解法相同,只是比三数之和多固定了一个数。
细节:target和nums[i]的取值范围,要用long long。

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 - 3;) {
for (int j = i + 1; j < n - 2;) {
long long sum = (long long)target - nums[i] - nums[j];
int left = j + 1, right = n - 1;
while (left < right) {
if (nums[left] + nums[right] > sum) {
right--;
} else if (nums[left] + nums[right] < sum) {
left++;
} else {
ret.push_back(
{nums[left], nums[right], nums[i], nums[j]});
left++;
right--;
while (right > left && nums[right] == nums[right + 1])
right--;
while (right > left && nums[left] == nums[left - 1])
left++;
}
}
j++;
while (j < n - 2 && nums[j] == nums[j - 1])
j++;
}
i++;
while (i < n - 3 && nums[i] == nums[i - 1])
i++;
}
return ret;
}
};