双指针
常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
- 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼 近。
- 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循 环),也就是:
- left == right (两个指针指向同⼀个位置)
- left > right (两个指针错开)
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列 结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快 慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
- 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
注:双指针只是一种思想,我们不一定真的要定义指针来实现双指针,我们可以使用特定的数值来表示指针,比如说数组的下标,某元素的值
第一题——移动零
「数组分两块」是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部 分。这种类型的题,⼀般就是使⽤「双指针」来解决。
题目来源. - 力扣(LeetCode)
思路(快排的思想:数组划分区间-数组分两块)
在本题中,我们可以⽤⼀个 cur 指针来扫描整个数组,另⼀个 dest 指针⽤来记录⾮零数序列的最后一个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。
在 cur 遍历期间,使 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的 元素全是零。
那我们要怎么做呢?很简单
我们直接看例子
我们发现1——5步均符合下面这个区间的要求
代码
void swap(int*a,int*b)
{
int tmp=*a;
*a=*b;
*b=tmp;
}
void moveZeroes(int* nums, int numsSize) {
for(int cur = 0, dest = -1; cur < numsSize; cur++)
if(nums[cur]) // 处理⾮零元素
swap(&nums[++dest], &nums[cur]);
}
第二题——复写零
题目来源:. - 力扣(LeetCode)
我们先审题,它这个题目到底要我们干什么,我们看下面这个图就明白了
假设上面这个是传入的数组,下面这个就是完成复写零的数组
思路:
来看看另外开辟一个数组的做法
把异地操作模拟成就地操作
我们先看看从前往后的双指针法行不行
显然不行
我们再看看从后往前的方式
完全可以嘿!那我们怎么找最后一个被复写的数呢?我们还是用双指针去找
但是,这个算法还不健全,它在下面这个情况会发生越界访问
代码:
void duplicateZeros(int* arr, int arrSize) {
// 1. 先找到最后⼀个数
int cur = 0, dest = -1, n = arrSize;
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--;
}
}
}
第三题——快乐数
题目来源:. - 力扣(LeetCode)
思路
先审题
要么变到一,要么循环,我们直接把它抽象成第一种情况为最后是形成所有元素都是一的环,第二种情况是所有元素都不是1的环
这不就是判断链表里是不是有环的问题吗?
有人就想了,我们怎么定义指针?
这里我们就要注意了,双指针只是一种思想,我们不一定真的要定义指针来实现双指针,我们可以使用特定的数值来表示指针,比如说数组的下标,但是在这里我们可以用数字来表示指针
我们先看slow指针
fast指针也是同理
我们就抽象成下面这个
有人就想,万一它一直不成环,一直无限循环下去怎么办?
但是,这是不可能的
我们来证明一下
我们直接取10个9,这是超过题目范围的,而且这个值产生拆分后的数一定是超过题目的
所以假设题目的数的范围和这个一样大,那它的取值范围是【1,810】,也就是说在这个数值范围产生的拆分后的数值一定是在【1,810】的,也就是说它最多循环810次,到811次之后一定会产生重复的值的,所以一直循环的情况不存在
根据上述的题⽬分析,我们可以知道,当重复执⾏ x 的时候,数据会陷⼊到⼀个「循环」之中。 ⽽「快慢指针」有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会 相遇在⼀个位置上。如果相遇位置的值是 1 ,那么这个数⼀定是快乐数;如果相遇位置不是 1 的话,那么就不是快乐数。
补充知识:如何求⼀个数n每个位置上的数字的平⽅和。
a. 把数 n 每⼀位的数提取出来: 循环迭代下⾯步骤:
- int t = n % 10 提取个位;
- n /= 10 ⼲掉个位; 直到 n 的值变为 0 ;
b. 提取每⼀位的时候,⽤⼀个变量 tmp 记录这⼀位的平⽅与之前提取位数的平⽅和
tmp = tmp + t * t
代码
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,我们就想把4变成大点的数容器的容积不就上去了吗?于是我们向内枚举,情况如下
我们发现,我们取6为墙的时候,右边的数向左枚举,没有一个容器的容积比原来的大的
我们再看看一个例子
算法思路: 设两个指针 left , right 分别指向容器的左右两个端点,此时容器的容积:
容器的左边界为 height[left] ,右边界为 height[right] 。
为了⽅便叙述,我们假设「左边边界」⼩于「右边边界」。
如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
- 容器的宽度⼀定变⼩。
- 由于左边界较⼩,决定了⽔的⾼度。如果改变左边界,新的⽔⾯⾼度不确定,但是⼀定不会超 过右边的柱⼦⾼度,因此容器的容积可能会增⼤。
- 如果改变右边界,⽆论右边界移动到哪⾥,新的⽔⾯的⾼度⼀定不会超过左边界,也就是不会 超过现在的⽔⾯⾼度,但是由于容器的宽度减⼩,因此容器的容积⼀定会变⼩的。
由此可⻅,左边界和其余边界的组合情况都可以舍去。所以我们可以 left++ 跳过这个边界,继 续去判断下⼀个左右边界。 当我们不断重复上述过程,每次都可以舍去⼤量不必要的枚举过程,直到 遇。期间产⽣的所有的容积⾥⾯的最⼤值,就是最终答案。
class Solution {
public:
int maxArea(vector<int>& height)
{
int left = 0, right = height.size() - 1, 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;
}
};