优选算法——双指针专题
本章先分享关于优选算法的双指针的思路:
主要是以题目来展示常见使用双指针的思路。
ps:
双指针做法:不要被表面所迷惑,它其实是通过用一个数组的下标来充当指针
数组分两块:是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部分。这种类型的题,⼀般就是使⽤「双指针」来解决
OJ(一)
283. 移动零 - 力扣(LeetCode)
1)题目展示
2)解法思路:
数组分两块:一边非0,一边0,可以使用双指针法。
1.定义两个指针:
cur:从左到右扫描数据,来遍历数组
dest:已处理的区间内,非零元素的最后一个位置。
由下面我们可以看出:
分为三个区间:
[0,dest][]dest+1,cur-1][cur,n-1]
非0 0 未处理
过程
具体做法:
1.cur遇到0,cur++
2.cur遇到非零元素:swap(dest+1,cur),cur++
代码:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int cur=0;
int dest=-1;
for(cur=0;cur<nums.size();cur++)
{
if(nums[cur])
{
swap(nums[cur],nums[++dest]);
}
}
}
};
OJ(二)
1089. 复写零 - 力扣(LeetCode)
1)题目展示:
2)解法思路:
1.双指针法:
如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」。因此我们选择「从后往前」的复写策略。
但「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两步:
i. 先找到最后⼀个复写的数;
ii. 然后从后向前进⾏复写操作
总结:
1.先根据“异地”操作,然后再进行双指针的“就地”操作
2.先找到最后一个复写的数:
双指针算法:
1)先判断cur的值
2)决定dest是走一步还是两步(0两步,非0一步)
3)判断dest是否已经到了结束位置
4)cur++
3. 然后从后向前进⾏复写操作。
1)i. 判断 cur 位置的值:
1. 如果是 0 : dest 以及 dest - 1 位置修改成 0 , dest -= 2 ;
2. 如果⾮零: dest 位置修改成 0 , dest -= 1 ;
2)cur-- ,复写下⼀个位置
4.越界处理:判断 dest 是否越界到 n 的位置
eg:1 0 2 3 0 0(会越界)
如果越界,执⾏下⾯三步:
1. n - 1 位置的值修改成 0 ;
2. cur 向移动⼀步(即cur--);
3. dest 向前移动两步(即dest-=2)
3)代码
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int cur=0,dest=-1;
int n=arr.size();
while(cur<n)
{
if(arr[cur]) dest++;
else 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--;
}
}
}
};
OJ快乐数(三)
202. 快乐数 - 力扣(LeetCode)
1)题目展示
2)解法思路
分析题目意思:
看到这里,就与我们之前在数据结构里面OJ题写过的判断链表是否有环那题非常相似的。
那里,我们使用的是快慢指针的方法,实质也是双指针。
十道OJ题帮你深入认识链表-CSDN博客
所以,思路:
双指针的快慢指针:
1)慢指针每次向后移动一位
2.快指针每次向后移动两位
3.判断相遇时的值是否为1即可。
ps:其实这里不可能不成环的。(证明:鸽巢原理:n个巢,n+1个鸽,必有一个巢会有2只鸽子及以上)
3)代码
class Solution {
public:
int GetNum(int n)
{
int num=0;
while(n)
{
int ret=n%10;
num+=ret*ret;
n/=10;
}
return num;
}
bool isHappy(int n) {
int slow=n,fast=GetNum(n);
while(slow!=fast)
{
slow=GetNum(slow);
fast=GetNum(GetNum(fast));
}
return fast==1;
}
};
OJ(四)
11. 盛最多水的容器 - 力扣(LeetCode)
1)题目展示
2)解法思路
解法1:暴力枚举(超时)即一个一个遍历去找
解法二:利用单调性,来进行使用双指针法解决
设两指针 left , right ,分别指向⽔槽板的最左端以及最右端,此时容器的宽度为 right - left 。由于容器的⾼度由两板中的短板决定,因此可得容积公式 : v = (j - i) * min(height[left], height[right])
为了下面的写法更加简洁,我们取高为h,宽为w,即V=h*w
下面我们就以题目的实例举例:
我们仔细观察会发现:
V=h*w,h由两板的短板决定
如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
--->容器的宽度⼀定变⼩。
---> 由于左边界较⼩,决定了⽔的⾼度。如果改变左边界,新的⽔⾯⾼度不确定,但是⼀定不会超
过右边的柱⼦⾼度,因此容器的容积可能会增⼤。
--->如果改变右边界,⽆论右边界移动到哪⾥,新的⽔⾯的⾼度⼀定不会超过左边界,也就是不会
超过现在的⽔⾯⾼度,但是由于容器的宽度减⼩,因此容器的容积⼀定会变⼩的。
因此:左边界和其余边界的组合情况都可以舍去。所以我们可以 left++ 跳过这个边界,继续去判断下⼀个左右边界
总结思路:
1.定义了left和right指针后,算出体积就可以舍去小的那个值了(即left的话就++,right的话就--)
2.高取小那个。
2.接着继续算体积,每次比较那个大就保留哪个。
3)代码
class Solution {
public:
int maxArea(vector<int>& height) {
int left=0,right=height.size()-1;
int ret=0;
while(left<right)
{
int v=min(height[left],height[right])*(right-left);
ret=max(ret,v);
//移动指针
if(height[left]<height[right])
{
left++;
}
else
{
right--;
}
}
return ret;
}
};
OJ(五)
611. 有效三角形的个数 - 力扣(LeetCode)
1)题目展示
2)解法思路
数学知识:
怎么才能构成三角形?
设a<=b<=c,那么要想构成三角形,则a+b>c
解法1:暴力枚举(弄三层for循环,再检查是否构成三角形,此时的时间复杂度就为O(n^3))
解法二:
1.我们先进行数组优化(即对数组进行排序)
2.接着利用单调性,使用双指针来解决:
1) 先固定最大的数 ----->O(N)
2)在最大数的左区间内,使用双指针法,快速通过符号构成三角形的数O(N)
若如果 nums[left] + nums[right] > nums[i] :
▪ 说明 [left, right - 1] 区间上的所有元素均可以与 nums[right] 构成⽐nums[i] ⼤的⼆元组
▪ 满⾜条件的有 right - left 种
如果 nums[left] + nums[right] <= nums[i] :
▪ 说明 left 位置的元素是不可能与 [left + 1, right] 位置上的元素构成满⾜条件的⼆元组
▪ left 位置的元素可以舍去, left++ 进⼊下轮循环
最后,时间复杂度为O(n^2)效率提高了.
3)代码
class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(),nums.end());
int n=nums.size();
int ret=0;
for(int i=n-1;i>=2;i--)
{
int left=0,right=i-1;
while(left<right)
{
if(nums[left]+nums[right]>nums[i])
{
ret+=right-left;
right--;
}
else
{
left++;
}
}
}
return ret;
}
};
OJ(六)
LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)
1)题目展示
2)解法思路
解法一:暴力枚举
解法二:利用单调性,使用双指针来找(跟上面的很像)
因为它已经排好序了,所以我们就省去了自己是排序那一步。
定义两个指针min(指向左边),max(右边)
若price[min]+price[max]>target,利用它的单调性,我们是不是就可以直接移动max--了?你想想你连最小的数都大于target了,其他是不是更加大于?
同样的道理:price[min]+price[max]<target ,直接min++。
当price[min]+price[max]==target时,就return {price[left],price[right]};
3)代码
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
int left=0,right=price.size()-1;
int sum=0;
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 {};
}
};
OJ(七)
15. 三数之和 - 力扣(LeetCode)
1)题目展示
2)解法思路
解法1:排序+暴力枚举+使用set(去重)
解法2:排序+双指针
1.排序
2.固定一个数a(我们从最左边第一个数先固定)
3.在固定的数的后面区间内,利用双指针,快速找到两个数的和等于-a
跟上面的题也是类似的,利用它的单调性,优化。
如果nums[left]+nums[right]>-a,即可直接right--;
nums[left]+nums[right]<-a,即可直接left++;(原因看上面的解析)
整体大概思路就完成了,
接下来我们来处理细节问题:
1.做到不漏:
解决:找到一种结果后,不要停,缩小区间,继续找
2.做到不重复
解决:
找到一种结果后,left和right都要跳过重复的数
当使用完一次双指针算法后,固定的数也要跳过重复的数。
此外,解决上面的问题的同时,还要注意避免越界的问题(代码中显示)
3)代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
int i=0;
vector<vector<int>> ret;
while(i<nums.size())
{
int left=i+1,right=nums.size()-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]});
left++;
right--;
1.防止越界
while(left<right && nums[left-1]==nums[left])
{
left++;
}
2.防止越界
while(left<right && nums[right+1]==nums[right])
{
right--;
}
}
}
i++;
3.防止越界
while(i<nums.size() && nums[i-1]==nums[i])
{
i++;
}
}
return ret;
}
};
OJ(八)
18. 四数之和 - 力扣(LeetCode)
1)题目展示
2)解法思路
解法1:排序+暴力枚举+set去重(4个循环)
解法2:排序+双指针
1.依次固定一个数a(我们从最左边第一个数开始)
2.在a的后面的区间内,使用“三数之和“找到数,使这三个数的和等于target-a即可
3.那么,根据上面的三数之和题目我们知道:
->固定一个数b
->在b的后面区间,利用双指针找到两个数,使这两个数的和等于target-a-b即可
整体大概思路就完成了,
接下来我们来处理细节问题:
1.做到不漏:
解决:找到一种结果后,不要停,缩小区间,继续找
2.做到不重复
解决:
找到一种结果后,left和right都要跳过重复的数
当使用完一次双指针算法后,固定的数也要跳过重复的数。
此外,解决上面的问题的同时,还要注意避免越界的问题(代码中显示)
3)代码
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
int n=nums.size();
vector<vector<int>> ret;
for(int a=0;a<n;)
{
for(int b=a+1;b<n;)
{
int left=b+1,right=n-1;
long long aim=(long long)target-nums[a]-nums[b];
while(left<right)
{
if(nums[left]+nums[right]>aim)
{
right--;
}
else if(nums[left]+nums[right]<aim)
{
left++;
}
else
{
ret.push_back({nums[left],nums[right],nums[b],nums[a]});
left++;
while(left<right && nums[left]==nums[left-1])
{
left++;
}
right--;
while(left<right && nums[right+1]==nums[right])
{
right--;
}
}
}
++b;
while(b<n && nums[b]==nums[b-1])
{
b++;
}
}
++a;
while(a<n && nums[a]==nums[a-1])
{
a++;
}
}
return ret;
}
};
好了,双指针法专题到这里就分析完了,希望对你有所进步!
最后,到了本次鸡汤部分:
切勿轻言放弃,最好的东西,总会压轴出场!