【算法】专题一:双指针之移动零,复写零,快乐数

常见的双指针有两种形式 ,⼀种是对撞指针,⼀种是左右指针。

对撞指针:⼀般⽤于顺序结构中,也称左右指针。

  • 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
  • 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:

left == right (两个指针指向同⼀个位置)

left > right (两个指针错开)


快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。这种⽅法对于处理环形链表或数组⾮常有⽤。其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。

快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:

  • 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。

一.移动零(数组划分,数组分块)

1.题目描述:

2.算法原理:

在这道题目中,我们可以⽤⼀个 cur 指针来扫描整个数组,另⼀个dest 指针⽤来记录⾮零数序列的最后⼀个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。在 cur 遍历期间,使 0, dest 的元素全部都是⾮零元素, dest + 1, cur - 1

元素全是零,cur,n-1就是未处理的区间。

3.算法步骤:

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 的元素全是零。
cpp 复制代码
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]);
    }
};

4.代码运行:

补充:

这里只关心非零元素 ,遇到非零就往前挪零元素什么都不做,自然就被"留"在了后面,所以不是"处理零元素",而是"忽略零元素,只处理非零元素"。

前置++ 是先加后用,后置++ 是先用后加。

5.算法总结:

这个⽅法是往后我们学习快排算法的时候,数据划分过程的重要⼀步。如果将快排算法拆解的话,这⼀段⼩代码就是实现快排算法的核⼼步骤。


二.复写零

1.题目描述:

2.算法原理:

双指针 + 倒序遍历

如果从前往后复写,会遇到一个问题:复写 0 会覆盖掉后面的元素。所以需要从后往前操作。


如果从前向后进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数被覆

盖掉。因此我们选择从后往前 的复写策略。但是从后向前 复写的时候,我们需要找到最后⼀个复写 的数,因此我们的⼤体流程分两步:

1.先找到最后⼀个复写的数;

2.然后从后向前进⾏复写操作。

从后往前遍历:

碰到 0:就把后面连续位置都填0;碰到 非0数:就把它挪到当前 dest 位置.

指针始终同步向前走,直到遍历完整个数组。

3.算法步骤:

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-- ,复写下⼀个位置。

cpp 复制代码
class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        //先找到最后一个数
        int cur = 0,dest = -1,n = arr.size();
        //因为从后面复写所以cur和dest的位置要进行说明
        while(cur<n)
        {
            if(arr[cur]) dest++;
            else dest += 2;
            if(dest>=n-1) break;//看dest是否结束怕越界
            cur++;
        }

        //处理一下边界问题
        if(dest==n)
        {
            arr[n-1] =0;
            cur--;
            dest -= 2;
        }

        //从后面完成复写操作
    while(cur>=0)
    {
    if(arr[cur]) arr[dest--]  = arr[cur--];
    else
        {
        arr[dest--] = 0;
        arr[dest--] = 0;
        cur--;

        }
    }

    }
    
};

4.代码运行:


三.快乐数

1.题目描述:

方便大家理解这个题目用两个例子(快乐数):

题⽬分析:

为了⽅便叙述,将对于⼀个正整数,每⼀次将该数替换为它每个位置上的数字的平⽅和这⼀个操作记为 x 操作;

题⽬告诉我们,当我们不断重复 x 操作的时候,计算⼀定会死循环,死的⽅式有两种:

  • 情况⼀:⼀直在 1 中死循环,即 1 -> 1 -> 1 -> 1......
  • 情况⼆:在历史的数据中死循环,但始终变不到 1

由于上述两种情况只会出现⼀种,因此,只要我们能确定循环是在情况⼀中进⾏,还是在情况⼆中进⾏,就能得到结果。

简单证明:

题目:给一个正整数,每次把它替换成各位数字的平方和,重复这个过程。如果能变成 1,就是快乐数;如果进入循环且不是 1,就不是快乐数。

你的几行笔记对应的是:

a. 一次变化后的最大值

一个数经过一次变化(各位平方和)之后,能有多大?,9² × 位数,如果位数固定,比如 10 位数,最大值是 81×10 = 810,哪怕你给一个超大数(比如 10 个 9),平方和也不会超过 810,简单理解就是:任何数变化一次后,结果都会落在 1~810 之间

  • b. 为什么一定会循环

    因为变化结果永远在 1~810 之间,总共有 810 种可能的结果。

    变化的过程就是一个不断往下走的过程:如果你连续走 811 步,一共只有 810 个"不同的目的地",那么根据鸽巢原理(抽屉原理),至少有一个数被重复走到一旦某一步重复,后面就会无限循环

  • c. 可以用快慢指针

    因为一定会进入循环,不是到 1 结束,就是进其他循环。

    这和判断链表是否有环是一类问题。

    快慢指针:快指针一次走两步,慢指针一次走一步,如果相遇,说明有环判断相遇点是不是1

2.算法原理:

利用平方和会将大数缩小到有限区间的特点,把数的变化过程抽象成一个有环链表,再用快慢指针判环。判断环中是否包含 1,即可得出快乐数。

3.算法思路:

根据上述的题⽬分析,我们可以知道,当重复执⾏ x 的时候,数据会陷⼊到⼀个「循环」之中。⽽快慢指针有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会相遇在⼀个位置上。如果相遇位置的值是 1 ,那么这个数⼀定是快乐数;如果相遇位置不是 1的话,那么就不是快乐数。

cpp 复制代码
class Solution {
public:
   
    int bitsum(int n)
    {
        int sum = 0;
        while(n)
        {
    int t = n%10;
     sum += t*t;
     n = 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.代码运行:

5.补充知识:

如何求⼀个数 n 每个位置上的数字的平⽅和。

  • a. 把数 n 每⼀位的数提取出来:

循环迭代下⾯步骤:

i. int t = n % 10 提取个位;

ii. n /= 10 ⼲掉个位;

直到 n 的值变为 0 ;

  • b. 提取每⼀位的时候,⽤⼀个变量 tmp 记录这⼀位的平⽅与之前提取位数的平⽅和

tmp = tmp + t * t

鸽巢原理:

鸽巢原理 (也叫抽屉原理):就是说,如果你有 n 个鸽巢 ,但是有 n+1 只鸽子 ,那么至少有一个鸽巢里有 至少 2 只鸽子

再举两个例子:

比如说你有 5 个抽屉,却有 6 件衣服,那么至少有一个抽屉里塞了 2 件衣服;

一年只有 365 天(或 366 天),如果有 366 个人,至少有两个人在同一天过生日。

相关推荐
lqqjuly1 小时前
KAN 网络深度解析
算法
睡一觉就好了。1 小时前
C++11(三)
c++
阿里matlab建模师2 小时前
【机场停机位分配】matlab实现基于遗传算法的机场停机位分配优化研究
开发语言·算法·数学建模·matlab·全国大学生数学建模竞赛
星恒随风2 小时前
C++ 类和对象入门(四):日期类 Date 的运算符重载实现详解
开发语言·c++·笔记·学习
wuminyu3 小时前
Java锁机制之park与futex系统级协同机制解析
java·linux·c语言·jvm·c++
小雨下雨的雨7 小时前
井字棋AI机器人实现详解 - Minimax算法实战-鸿蒙PC Electron框架完成
前端·人工智能·算法·华为·electron·鸿蒙
xieliyu.10 小时前
Java算法精讲:双指针(三)
java·开发语言·算法
一条小锦吕*10 小时前
基于Spring Boot + 数据可视化 + 协同过滤算法的推荐系统设计与实现(源码+论文+部署全讲解)
spring boot·算法·信息可视化
cfm_291412 小时前
Redis五大基本数据结构底层了解
数据结构·数据库·redis