相应大家的要求,所以我决定在开一个算法初阶的专栏,这样方便大家能够从我这的学习能够连贯。
一、双指针概念
常见的双指针有两种形式,⼀种是对撞指针,⼀种是快慢指针。
对撞指针:⼀般用于顺序结构中,也称左右指针。
- 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
- 对撞指针的终止条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循 环),也就是:
- left == right (两个指针指向同⼀个位置)
- left > right (两个指针错开)
快慢指针:又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。 这种方法对于处理环形链表或数组非常有用。 其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快 慢指针的思想。
快慢指针的实现方式有很多种,最常用的一种就是:
• 在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢。
二、题目实例
1. 移动零
1)题意解析![](https://i-blog.csdnimg.cn/direct/075abc54bfb84b189f2f3d1be0c05394.png)
这种题目我们统称为数组分两块类,我们用双指针解题最轻松。
2)算法思路
在本题中,我们可以用一个 cur 指针来扫描整个数组,另一个 dest 指针用来记录非零数序列 的最后一个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。在 cur 遍历期间,使 [0, dest] 的元素全部都是非零元素, [dest + 1, cur - 1] 的元素全是零。
算法流程:
- 初始化 cur = 0 (用来遍历数组), dest = -1 (指向非零元素序列的最后⼀个位置。 因为刚开始我们不知道最后⼀个非零元素在什么位置,因此初始化为 -1 )
- cur 依次往后遍历每个元素,遍历到的元素会有下面两种情况:
- 遇到的元素是 0 , cur 直接 ++ 。因为我们的目标是让 [dest + 1, cur - 1] 内的元素全都是零,因此当 cur 遇到 0 的时候,直接 ++ ,就可以让 0 在 cur - 1 的位置上,从⽽在 [dest + 1, cur - 1] 内;
- 遇到的元素不是 0 , dest++ ,并且交换 cur 位置和 dest 位置的元素,之后让cur++ ,扫描下一个元素。
- 因为 dest 指向的位置是非零元素区间的最后⼀个位置,如果扫描到⼀个新的非零元素,那么它的位置应该在 dest + 1 的位置上,因此 dest 先自增 1 ;
- dest++ 之后,指向的元素就是 0 元素(因为非零元素区间末尾的后⼀个元素就是0 ),因此可以交换到 cur 所处的位置上,实现 [0, dest] 的元素全部都是非零元素, [dest + 1, cur - 1] 的元素全是零。
3)代码实现
class Solution
{
public:
void moveZeroes(vector<int>& nums)
{
for(int cur = 0, dest = -1; cur < nums.size(); cur++)
if(nums[cur]) // 处理⾮零元素
swap(nums[++dest], nums[cur]);
}
};
2.复写零
1)题意解析
2)算法思路
如果「从前向后」进行原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆
盖掉」。因此我们选择「从后往前」的复写策略。 但是「从后向前」复写的时候,我们需要找到「最后一个复写的数」,因此我们的大体流程分两步:
- 先找到最后一个复写的数;
- 然后从后向前进行复写操作
3)代码实现
class Solution
{
public:
void duplicateZeros(vector<int>& arr)
{
// 1. 先找到最后⼀个数
int cur = 0, dest = -1, n = arr.size();
while(cur < n)
{
if(arr[cur])
dest++;
else
dest += 2;
if(dest >= n - 1)
break;
cur++;
}
// 2. 处理⼀下边界情况
if(dest == n)
{
arr[n - 1] = 0;
cur--; dest -=2;
}
// 3. 从后向前完成复写操作
while(cur >= 0)
{
if(arr[cur])
arr[dest--] = arr[cur--];
else
{
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
};
3.快乐数
1)题意解析
2)算法思路
根据上述的题目,我们可以知道,当重复执行
的时候,数据会陷入到一个「循环」之中。 而「快慢指针」有⼀个特性,就是在一个圆圈中,快指针总是会追上慢指针的,也就是说他们总会相遇在一个位置上。如果相遇位置的值是 1
,那么这个数一定是快乐数;如果相遇位置不是
1 的话,那么就不是快乐数。
3代码实现
class Solution
{
public:
int bitSum(int n) // 返回 n 这个数每⼀位上的平⽅和
{
int sum = 0;
while(n)
{
int t = n % 10;
sum += t * t;
n /= 10;
}
return sum;
}
bool isHappy(int n)
{
int slow = n, fast = bitSum(n);
while(slow != fast)
{
slow = bitSum(slow);
fast = bitSum(bitSum(fast));
}
return slow == 1;
}
};
4.盛最多水的容器
1)题意解析![](https://i-blog.csdnimg.cn/direct/46cc1855f99f44a7b4d9d661169245aa.png)
2)算法思路
1.暴力求解,但是很可惜会超时
2.对撞指针
设两个指针 left , right 分别指向容器的左右两个端点,
此时容器的容积 : v = (right - left) * min( height[right], height[left])
容器的左边界为 height[left] ,右边界为 height[right] 。
我们假设「左边边界」小于「右边边界」。如果此时我们固定一个边界,改变另一个边界,水的容积会有如下变化形式:
- 容器的宽度⼀定变小。
- 由于左边界较小,决定了水的高度。如果改变左边界,新的水面高度不确定,但是一定不会超过右边的柱子高度,因此容器的容积可能会增大。
- 如果改变右边界,无论右边界移动到哪里,新的水面的高度⼀定不会超过左边界,也就是不会超过现在的水面高度,但是由于容器的宽度减小,因此容器的容积⼀定会变小的。
由此可见,左边界和其余边界的组合情况都可以舍去。所以我们可以 left++ 跳过这个边界,继续去判断下⼀个左右边界。 当我们不断重复上述过程,每次都可以舍去大量不必要的枚举过程,直到 left 与 right 相遇。期间产生的所有的容积里面的最大值,就是最终答案。
3)代码实现
class Solution {
public:
int maxArea(vector<int>& height)
{
int l=0;
int r=height.size()-1;
int Max=0;
int s;
while(l<=r)
{
s=min(height[l], height[r]) * (r - l);
Max=max(s,Max);
if(height[l]<height[r])
{
l++;
}
else
{
r--;
}
}
return Max;
}
};
双指针一共就这点东西,希望大家能好好掌握,顺便也可以把这些题目完成,巩固一下。
感谢观看,如有错误欢迎指正