目录
算法介绍
许多题目的最优解,往往都是对暴力解法的优化
有序数组两大常用算法:双指针、二分 (能用双指针,就别用二分)
双指针算法实际上是用数组下标充当指针,对数组中的元素进行操作
题目要求一般是对某个数组进行划分 / 操作数组中的元素。
常见的双指针题目形式有两种:对撞指针,快慢指针
对撞指针
对撞指针又称左右指针,一般用于顺序结构。
对撞指针一般是两个指针,一个从最左端开始,一个从最右端开始,逐渐向中间移动。
对撞指针的终止条件:一般是两个指针相遇 / 错开(也可能在循环内部找到结果,直接跳出循环)
即 left == right (两个指针指向同一位置)/ left > right(两个指针错开)
对撞指针 例题1 盛最多水的容器
AC代码
class Solution {
public:
int Min(int x, int y)
{
if(x > y)
{
return y;
}
else
{
return x;
}
}
int maxArea(vector<int>& height)
{
// 利用单调性 + 双指针来实现
int left = 0;
int right = height.size() - 1;
int max = Min(height[right], height[left]) * (right - left); // 最大盛水体积
int value = Min(height[right], height[left]) * (right - left); // 每移动一次计算一次体积
while(left != right)
{
// 1、小的那边移动(因为左右指针内部的元素,无论比小的那边大/小/相等,容积总是减少的)
if(height[left] < height[right])
{
left++;
value = Min(height[right], height[left]) * (right - left);
if(value > max)
{
// 2、大于max,就更新max
max = value;
}
}
else
{
right--;
value = Min(height[right], height[left]) * (right - left);
if(value > max)
{
// 2、大于max,就更新max
max = value;
}
}
}
return max;
}
};
题解思想
设两个指针 left , right 分别指向容器的左右两个端点,此时容器的容积为:
v = (right - left) * min( height[right], height[left])
容器的左边界为 height[left] ,右边界为 height[right] 。
为方便叙述,假设「左边边界」小于「右边边界」。
如果此时我们固定⼀个边界,改变另⼀个边界,水的容积会有如下变化形式:
1、容器的宽度⼀定变小。
2、由于左边界较小,决定了水的高度。如果改变左边界,新的水面高度不确定,但是⼀定不会超
过右边的柱子高度,因此容器的容积可能会增大。
3、如果改变右边界,则无论右边界移动到哪儿,新的水面的高度⼀定不会超过左边界,也就是不会 超过现在的水面高度,但是由于容器的宽度减小,因此容器容积一定变小。
由此可得,左边界和其余边界的组合情况都可以舍去。
所以可以 left++ 跳过这个边界,继续去判断下⼀个左右边界。
当不断重复上述过程,每次都可以舍去大量不必要的枚举过程,直到 left 与 right 相遇。期间产生的所有的容积里面的最大值,即是最终答案。
对撞指针 例题2 有效三角形个数
题解思想
先把数组进行排序,就可以只凭借a + b > c判断三角形了,固定第三条边,然后对撞指针依次判断是否是有效三角形
对撞指针 例题3 三数之和
AC代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
vector <vector<int>> res; // 最终返回结果,要做到不重+不漏
// 1、排序
sort(nums.begin(), nums.end());
// 2、固定一个数,剩下的区间内采用对撞指针
int third = 0; // 固定一个数所在的位置
while (third < nums.size())
{
int left = third + 1;
int right = nums.size() - 1;
while (left < right)
{
if (nums[left] + nums[right] > -nums[third])
{
right--;
}
else if (nums[left] + nums[right] < -nums[third])
{
left++;
}
else
{
res.push_back({nums[left], nums[right], nums[third]});
// 确保不漏:一次结果正确后接着往后走
left++;
right--;
// 去重1:如果left的数和right的数重复,不用再算了(同时还要防越界)
while(left < right && nums[left - 1] == nums[left])
{
left++;
}
while(left < right && nums[right + 1] == nums[right])
{
right--;
}
}
}
third++;
// 去重2:如果出现重复的third执行一次即可
while(third < nums.size() && nums[third - 1] == nums[third])
{
third++;
}
}
return res;
}
};
题解思想
本质是对暴力枚举进行优化
1、把数组先排序,然后固定一个数 count 。
2、在这个数后面的区间内,使用对撞指针快速找到两个数之和等于 -count 的即可。
但是要注意的是,这道题里面需要有 去重 + 不漏 操作
1、找到⼀个结果之后, left 和 right 指针要「跳过重复」的元素;
2、当使用完一次对撞指针算法之后,固定的 count 也要「跳过重复」的元素。
3、确保不漏:一次结果正确后接着往后走
快慢指针
快慢指针的基本思想就是使用两个移动速度不同的指针在数组 / 链表等序列上移动。
这种方法在处理环形链表 / 数组时非常有用,当然也不单单是这两类,只要问题出现循环往复的情况时,均可考虑使用快慢指针思想。
快慢指针最常见的实现方式
在一次循环中,慢的指针每次向后移动一位,快指针一次移动两位,实现一个快一个慢
快慢指针 例题1 移动零
题解思想
在本题目中,我们可以使用两个指针 dest 和 cur。cur 负责遍历数组;dest 则是一个标记,dest左边的元素都是非零元素,右边都是 0 。
因为 cur 负责遍历数组,所以初始化为0;而 dest 因为是标记,左边(包括其自己)必须都是非零元素,故为了防止数组第一个就是0,我们把 dest 初始化为 -1 。
然后 cur 遍历数组;遇到非零元素,dest 后移一位,与 cur 指向元素进行交换,直至 cur 遍历结束。这样就可以满足dest左边(包括其自己)都是非零元素,右边都是零元素,满足题目条件
(PS:本质上也利用了快排的思想,即数组划分区间 + 数组分两块)
快慢指针 例题2 复写零
AC代码
class Solution {
public:
void duplicateZeros(vector<int>& arr)
{
// 1、先找到复写后数组的最后一个数是哪一个(同样是一个双指针算法)
int cur = 0; // 指向复写后数组的最后一个数
int dest = -1; // 用来标记,当这个指针到原数组末尾时,cur指向的位置一定是复写后数组的最后一个数
while(cur < arr.size())
{
if(arr[cur] != 0)
{
dest++;
}
else
{
dest+=2;
}
if(dest >= arr.size() - 1)
{
break;
}
cur++;
}
// 如果cur指向复写后数组的最后一个数是0,dest在最后一步会+=2,此时会越界,所以需要处理
if(dest == arr.size())
{
arr[arr.size() - 1] = 0;
cur--;
dest-=2;
}
// 2、因为从前往后遍历会覆盖元素,所以从后往前遍历
while(cur >= 0)
{
if(arr[cur] != 0)
{
arr[dest] = arr[cur];
dest--;
}
else
{
arr[dest] = 0;
arr[dest-1] = 0;
dest -=2;
}
cur--;
}
}
};
题解思想
如果从前向后进行原地复写操作的话,由于 出现 0 会写两次,导致没有复写的数被覆盖掉。因此我们选择从后往前的复写策略。
但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的大体流程分两
步:
1、先找到最后⼀个复写的数;
2、然后从后向前进行复写操作。
快慢指针 例题3 快乐数
题解思想
根据鸽巢原理可以得知,在循环执行数据的时候,数据必然会陷入到一个循环中,此时就是可以使用快慢指针,快指针一次走两步,慢指针一次走一步,由于数据陷入循环,所以必然两个指针会相遇,然后可以根据相遇的情况判断是否是快乐数。
双指针算法&spm=1001.2101.3001.5002&articleId=141869351&d=1&t=3&u=77208f738e104da3a75f052d8dcac9b2)
1301

被折叠的 条评论
为什么被折叠?



