
◆ 博主名称: 晓此方-CSDN博客 大家好,欢迎来到晓此方的博客。
⭐️算法系列个人专栏 【插曲】算法实战论
⭐️ 踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰
概要&序論
大家好,这里是还在拼命肝C++的此方,但是今天开始,我将同步连载新的专栏【算法实战论】 ,本专栏未来内容力求做到------不仅会做题,更理解本质。还是感谢大家一直以来的支持。
本文将围绕双指针这一常见技巧展开 ,重点讲清它在什么情况下可以使用,以及指针是如何移动的 。内容会结合一些典型场景 ,通过具体例子一步步分析,让你看清每一步为什么这么做,从而真正掌握双指针的使用方法,而不是只会套固定写法。
本文及以后文章所使用平台:
最常用 :LeetCode
偶尔用 : 牛客
本文参考代码仓库:
【小此方的GitHub仓库】
一,双指针算法理论
常见的双指针有两种形式,一种是对撞指针,一种是左右指针。
1.1对撞指针
- 对撞指针一般用于顺序结构中,也称左右指针。
- 对撞指针从两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐往中间逼近。
- 对撞指针的终止条件一般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳 出循环),也就是:
- eft == right (两个指针指向同一个位置)
- left > right (两个指针错开)
1.2快慢指针
快慢指针又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。这种方法对于处理环形链表或数组非常有用。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想。
快慢指针的实现方式有很多种,最常用的一种就是:在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢。
二,双指针实战01------移动零
题目传送门:移动零-力扣

2.1算法思想讲解:数组分块思想初见
2.1.1我们要做什么
移动零这种问题一般的我们把它们归在数组划分数组分块这种题目的大类。「数组分两块」 是非常常见的⼀种题型,主要就是根据一种划分方式,将数组的内容分成左右两部分。这种类型的题,⼀般就是使用「双指针」来解决。
放在这一题里面,就是将数组分成两个部分:零和非零。

2.1.2如何实现分块成零和非零
我们边看题目边讲,数组分块这类题目有一种常见的解法:双指针算法 ,这里利用数组下标来充当指针(双指针并非一定指两个指针)
我们用两个指针dest和cur,两个指针的作用:
- cur:从左往右扫描数组,遍历数组
- dest:已处理的区间内,非零元素的最后一个位置。
设定起始位置:
-
dest的起始位置是-1,表示没有为0的数据。
-
cur的起始位置是0,表示从0开始遍历。
cur ↓ -1 [ 2, 0, 0, 3, 12 ] ↑ dest
按照数组分块方法的思路,我们的这两个指针的目的是将数组分成两个部分,如下图,cur不断向前走,把非零元素往前面扔,dest在屁股后面标记着非0元素的末尾,当cur走到数组的尽头时,结束。

在这个过程当中,数组有分成三个部分: 而指针在运动过程中必须时刻保持着这三个区间的性质不变性。

所以我们规定运动方式: cur和向右移动,dest向右移动。cur遇到的元素区分成两类,零和非零:
-
如果cur遇到的元素是非0,让dest++并且让dest指向的数据和cur指向的数据交换。
-
如果cur遇到的元素是0,就直接cur++。
2.2代码实现
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums)
{
//定义起始位置
for(int dest=-1,cur = 0;cur<nums.size();cur++)//如果cur遇到0元素直接++
if(nums[cur])//如果遇到的值不是0,
std::swap(nums[cur],nums[++dest]);//dest前置++再交换
}
};
三,双指针实战02------复写零
题目传送门力扣-复写零

3.1算法思路详解:从异地模拟到就地解决
3.1.1异地思路
这道题,异地做绝对简单,只需要再开辟一个数组,然后让cur指针在原数组上遍历一遍,遇到非零就抄一个到新数组,遇到零就抄两个到新数组。最后返回新数组即可。

3.1.2本地错误思路
但是这道题目堆空间复杂度有要求的。所以前面的方法不行,那么能不能将这种双指针思路放在原地进行操作呢?我们来尝试一下:
cur从左往右,还是遇到非零就抄一个到新数组,遇到零就抄两个到新数组。停!显然不行,为什么不行,就拿上面的例子:cur=1的时候,复写了两个0,把下标为2的2覆盖掉了 ,那么cur继续往后面走会不断复写覆盖掉原本前面数据的0,并且生成新的0覆盖掉前面的数据------导致最后整个数组变成了[1,0,0,0,0,0,0,0,0]
3.1.3基于异地映出正确的本地思路
这怎么办?有方法:从后往前复写就可以了。
初始位置确定: dest的初始位置很明显是数组有效位置在最后一个。cur的初始位置怎么搞?
回到3.1.1,我们把那个异地的方法走完:cur最后没有走到数组结尾,而是在4,所以cur的起始位置就定4。
cur
↓
[1,0,2,3,0,4,5,0]
[1,0,0,2,3,0,0,4]
↑
dest
剩下的思路就跟上面一样了,我们直接上代码:
3.2代码实现
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr)
{
int cur=0,dest=-1,n=arr.size();
while(dest<n)
{
if(arr[cur]) dest++; // 非0 -> 在新数组中占1个位置
else dest+=2; // 0 -> 在新数组中占2个位置(复制0)
if(dest>=n-1) // 一旦即将或已经到达边界,停止
break;
cur++; // 原数组继续往后走
}
if(dest==n)
{
// 特殊情况:最后一个位置溢出0,只能放一个0
arr[n-1]=0;
cur--; // 回退一个位置
dest-=2; // 回到
}
while(dest>=0)
{
if(arr[cur])
// 非0:直接搬运
arr[dest--]=arr[cur--];
else
{
// 0:复制两次(倒着写)
arr[dest--]=0;
arr[dest--]=0;
cur--; // 原数组指针左移
}
}
}
};
四,双指针实战03------快乐数
题目传送门力扣-快乐数

3.1题意解析------有点像环形链表
前两题很简单,但是从这一题开始,我要开始讲题意解析了。(当然这题也简单)如图,19这个数每个位上的平方和是82,82这个数每个位上的平方和是68,以此类推,最后会变成1的自循环。

很神奇吧,再来看这个示例;不断的执行各位平方和 操作,最后20的各位平方和变成了4,于是开始循环。

19,经历了这个过程,结果为1,所以19是快乐数,2经厉了这个过程,结果不为1,所以就不是快乐数。题目就是这个意思。
3.2算法思路讲解:和环形链表如出一辙
3.2.1快慢指针复现环形链表思路
快乐数的本质是:判断链表是否有环,于是解法:快慢双指针判断相遇时候的值即可。
先说结论,两个指针必然相遇。为什么两个指针必然相遇?我在这篇文章中介绍了严格的数学证明,有兴趣的大佬可以看一看
定义快慢指针所谓双指针,不知可能是一对指针,一对数组下标,实际上也可以是数字。
- slow:慢指针每次向后移动一步。
- fast:快指针每次向后移动两步。
重定义所谓"指针的移动",我们可以把【每进行一次各位平方和】的操作定义为一次移动。什么意思?我们就拿上面的例子:
- slow指针和fast指针首先指向开始位置:slow和fast初始值为2.
- slow每次移动一步:slow前进 2^2------>4。
- fast每次移动两步:fast前进 2^ 2 ------>4^4------>16。

3.2.2论证"环"为什么存在

这道题目的简单之处在于它把这个问题的可能性告诉你只有这两种情况。如果没有这句话,难度大幅提升,可能有第三种情况:永远变化下去没有环。
接下来论证:为什么这个环必然存在。
简单鸽巢原理的定义:
如果将n 个物体放入 m 个盒子中,且 n>m,那么至少有一个盒子中包含两个或以上的物体。
一个数x,它有y位,那么这个数的【最大各位平方和 】应该等于9 * 9 * y,也就是81y。这个局域是被限定死的。数x的任意次各位平方和的结果必定会落在0~81y之间。
经过无限次迭代,由于结果始终被限制在一个有限的取值集合中,因此必然会出现某个取值被重复访问,从而产生环。
3.3代码实现
cpp
class Solution
{
public:
// 计算一个数 n 的"快乐数转换"结果
// 即:将每一位数字平方后求和
int happy(int n)
{
int count = 0;
// 逐位处理数字
while(n)
{
// n % 10 获取当前最低位数字
// (n % 10) * (n % 10) 进行平方累加
count += (n % 10) * (n % 10);
// 去掉最低位
n /= 10;
}
return count;
}
// 对 happy 函数再执行一次 happy,相当于两步转换
int doublehappy(int n)
{
return happy(happy(n));
}
bool isHappy(int n)
{
// 慢指针:每次走一步
int slow = n;
// 快指针:先走一步
int fast = happy(n);
// 利用 Floyd 判圈思想检测是否出现循环
while(slow != fast)
{
// 慢指针每次走一步
slow = happy(slow);
// 快指针每次走两步(通过 doublehappy 实现)
fast = doublehappy(fast);
}
// 如果最终相遇点为 1,则说明是快乐数
if(fast == 1)
return true;
else
return false;
}
};
好了,本期内容到此结束,我是此方,我们下期再见。バイバイ!