【双指针】算法入门与简单三题——移动零、复写零、快乐数

文章介绍了双指针算法,包括对撞指针用于顺序结构和环形链表处理,快慢指针用于解决循环问题,如数组分两块(将0移动到末尾)和快乐数判断(判断数是否通过平方和循环至1)。
摘要由CSDN通过智能技术生成

双指针

常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
• 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼
近。
• 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循
环),也就是:
◦ left == right (两个指针指向同⼀个位置)
◦ left > right (两个指针错开)
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列 结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快 慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
• 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。

1. 移动零(easy)

leetcode题目
「数组分两块」是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部
分。这种类型的题,⼀般就是使⽤「双指针」来解决。
1.题目描述
给定⼀个数组 nums ,编写⼀个函数将所有 0 移动到数组的末尾,同时保持⾮零元素的相对顺
序。
请注意 ,必须在不复制数组的情况下原地对数组进⾏操作。
⽰例 1:
输⼊: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
⽰例 2:
输⼊: nums = [0]
输出: [0]
2. 解法(快排的思想:数组划分区间 - 数组分两块):
算法思路:
在本题中,我们可以⽤⼀个 cur 指针来扫描整个数组,另⼀个 dest 指针⽤来记录⾮零数序列
的最后⼀个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。
cur 遍历期间,使 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零
算法流程:
a. 初始化 cur = 0 (⽤来遍历数组), dest = -1 (指向⾮零元素序列的最后⼀个位置。
因为刚开始我们不知道最后⼀个⾮零元素在什么位置,因此初始化为 -1
b. cur 依次往后遍历每个元素,遍历到的元素会有下⾯两种情况:
i. 遇到的元素是 0 cur 直接 ++ 。因为我们的⽬标是让 [dest + 1, cur - 1]
的元素全都是零,因此当 cur 遇到 0 的时候,直接 ++ ,就可以让 0 cur - 1
的位置上,从⽽在 [dest + 1, cur - 1] 内;
ii. 遇到的元素不是 0 dest++ ,并且交换 cur 位置和 dest 位置的元素,之后让
cur++ ,扫描下⼀个元素。
因为 dest 指向的位置是⾮零元素区间的最后⼀个位置,如果扫描到⼀个新的⾮零元
素,那么它的位置应该在 dest + 1 的位置上,因此 dest 先⾃增 1
dest++ 之后,指向的元素就是 0 元素(因为⾮零元素区间末尾的后⼀个元素就是
0 ),因此可以交换到 cur 所处的位置上,实现 [0, dest] 的元素全部都是⾮零
元素, [dest + 1, cur - 1] 的元素全是零。
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. 复写零(easy)

1. 题⽬描述:
给你⼀个⻓度固定的整数数组 arr ,请你将该数组中出现的每个零都复写⼀遍,并将其余的元素
向右平移。
注意:请不要在超过该数组⻓度的位置写⼊元素。请对输⼊的数组就地进⾏上述修改,不要从函数返
回任何东西。
⽰例 1:
输⼊: arr = [1,0,2,3,0,4,5,0]
输出: [1,0,0,2,3,0,0,4]
解释:
调⽤函数后,输⼊的数组将被修改为: [1,0,0,2,3,0,0,4]
2. 解法(原地复写 - 双指针):
算法思路:
如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆
盖掉」。因此我们选择「从后往前」的复写策略。
但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两
步:
i. 先找到最后⼀个复写的数;
ii. 然后从后向前进⾏复写操作。
算法流程:
a. 初始化两个指针 cur = 0 dest = 0
b. 找到最后⼀个复写的数:
i. cur < n 的时候,⼀直执⾏下⾯循环:
判断 cur 位置的元素:
如果是 0 的话, dest 往后移动两位;
否则, dest 往后移动⼀位。
判断 dest 时候已经到结束位置,如果结束就终⽌循环;
如果没有结束, cur++ ,继续判断。
c. 判断 dest 是否越界到 n 的位置:
i. 如果越界,执⾏下⾯三步:
1. n - 1 位置的值修改成 0
2. cur 向移动⼀步;
3. dest 向前移动两步。
d. cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:
i. 判断 cur 位置的值:
1. 如果是 0 dest 以及 dest - 1 位置修改成 0 dest -= 2
2. 如果⾮零: dest 位置修改成 0 dest -= 1
ii. cur-- ,复写下⼀个位置。
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. 快乐数(medium)

1. 题⽬描述:
编写⼀个算法来判断⼀个数 n 是不是快乐数。
「快乐数」 定义为:
对于⼀个正整数,每⼀次将该数替换为它每个位置上的数字的平⽅和。
然后重复这个过程直到这个数变为 1,也可能是⽆限循环但始终变不到 1
如果这个过程 结果为 1 ,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false
⽰例 1:
输⼊: n = 19
输出: true
解释:
19 -> 1 * 1 + 9 * 9 = 82
82 -> 8 * 8 + 2 * 2 = 68
68 -> 6 * 6 + 8 * 8 = 100
100 -> 1 * 1 + 0 * 0 + 0 * 0 = 1
⽰例 2:
输⼊: n = 2
输出: false
解释:(这⾥省去计算过程,只列出转换后的数)
2 -> 4 -> 16 -> 37 -> 58 -> 89 -> 145 -> 42 -> 20 -> 4 -> 16
往后就不必再计算了,因为出现了重复的数字,最后结果肯定不会是 1
2. 题⽬分析:
为了⽅便叙述,将「对于⼀个正整数,每⼀次将该数替换为它每个位置上的数字的平⽅和」这⼀个
操作记为 x 操作;
题⽬告诉我们,当我们不断重复 x 操作的时候,计算⼀定会「死循环」,死的⽅式有两种:
情况⼀:⼀直在 1 中死循环,即 1 -> 1 -> 1 -> 1......
情况⼆:在历史的数据中死循环,但始终变不到 1
由于上述两种情况只会出现⼀种,因此,只要我们能确定循环是在「情况⼀」中进⾏,还是在「情
况⼆」中进⾏,就能得到结果。
简单证明:
a. 经过⼀次变化之后的最⼤值 9^2 * 10 = 810 ( 2^31-1=2147483647 。选⼀个更⼤的最
9999999999 ),也就是变化的区间在 [1, 810] 之间;
b. 根据「鸽巢原理」,⼀个数变化 811 次之后,必然会形成⼀个循环;
c. 因此,变化的过程最终会⾛到⼀个圈⾥⾯,因此可以⽤「快慢指针」来解决。
3. 解法(快慢指针):
算法思路:
根据上述的题⽬分析,我们可以知道,当重复执⾏ x 的时候,数据会陷⼊到⼀个「循环」之中。
⽽「快慢指针」有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会
相遇在⼀个位置上。如果相遇位置的值是 1 ,那么这个数⼀定是快乐数;如果相遇位置不是 1
的话,那么就不是快乐数。
补充知识:如何求⼀个数 n 每个位置上的数字的平⽅和。
a. 把数 n 每⼀位的数提取出来:
循环迭代下⾯步骤:
i. int t = n % 10 提取个位;
ii. n /= 10 ⼲掉个位;
直到 n 的值变为 0
b. 提取每⼀位的时候,⽤⼀个变量 tmp 记录这⼀位的平⽅与之前提取位数的平⽅和
tmp = tmp + t * t
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;
 }
};

  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风铃子加油

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值