双指针算法专题(移动零 复写零 快乐数)

news2025/1/18 8:51:35

目录

前言

1. 移动零

(1)题目及示例

(2)一般思路

(3)双指针解法

2. 复写零

(1)题目及示例

(2)一般解法

(3)双指针解法

3. 快乐数

(1)题目及示例

  (2) 题目分析及思路

(3) 证明

总结


前言

本文是讲解三道双指针相关的OJ题目,我会慢慢深入,一般的题目从暴力解法讲起,再进行优化,使用双指针。本文附有详细的图文示例,干货多多。


1. 移动零

(1)题目及示例

题目:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]

输出: [1,3,12,0,0]

示例 2:

输入: nums = [0]

输出: [0]

(2)一般思路

这个题目的要求是把所有的零放在后面,并且非零元素的顺序保持不变。

正常来说,我们可以通过开辟一个新数组,将非零元素按照顺序拷贝下来,后面剩下的元素置为0,再拷贝回去。因为只需要遍历几遍数组,时间复杂度是O(N),需要新开辟一个空间大小相同的数组,空间复杂度也是O(N)。

下面是解题代码,其中需要注意的是,创建好vector类tmp对象,需要改变容器的包含元素的个数,即改变size的大小,可以减少频繁扩容带来的消耗。

void moveZeroes(vector<int>& nums)
{
    vector<int> tmp;
    int n = nums.size();
    tmp.resize(n);

    for (int i = 0, j = 0; j < n; )
    {
        if (i < n)
        {
            if (nums[i] != 0)
                tmp[j++] = nums[i++];
            else
                ++i;
        }
        else
            tmp[j++] = 0;
    }
    
    for (int i = 0; i < n; i++)
        nums[i] = tmp[i];
}

(3)双指针解法

如果想要在数组原地进行修改,就要使用双指针算法。那么何为双指针呢?

  • 双指针算法顾名思义是使用两个指针的算法,但是不仅于此。我们还可以使用下标来代替指针,其中的内核是类似两个指针一起移动来记录信息的思路。双指针算法可以用来解决划分区间的题目。
  • 观察示例中输出的数组,这些数组被划分为两个区域,非零和只有零的区域。我们创建两个整型变量,名为dest和cur,表示数组元素的下标,通过这两个变量来划分区域。
  • 我们要做的是,将cur向后移动,如果碰到0,继续移动,如果碰到非0的数字,dest向前移动一格,然后交换这两个变量所指向元素的值。不断重复这个过程,直到cur指向最后一个元素的下一个位置,就结束了。
  • 以数组{0,1,0,3,5}为例,一开始dest赋值为-1,cur赋值为0。dest一开始没有指向数组,cur指向第一个元素。

  • cur往后移动,遇到非零元素,dest先往后移动一步,然后交换元素内容。

  • cur继续往后走,遇到零,不停下来。再往后走遇到3,非零元素。dest往后走一步,并交换两个变量所指元素内容。

  • cur继续往后走,遇到非零元素。dest完后走一步,再次交换元素内容。最后,cur走到末尾元素的下一个位置时,就结束这个操作。此时你会发现,非0元素全部排在前面,0元素排在后面,而dest和cur就划分出了这两个区域。

代码如下,创建两个整型变量,使用for循环,循环继续条件是cur变量的值小于数组元素个数大小,即指向最后一个元素的下标。当遇到非零元素,dest先加加,然后再交换各自指向的元素。

void moveZeroes(vector<int>& nums) 
{
    int cur, dest;
    for (cur = 0, dest = -1; cur < nums.size(); cur++)
        if (nums[cur] != 0)
            swap(nums[++dest], nums[cur]);

}

2. 复写零

(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:

输入:arr = [1,2,3]

输出:[1,2,3]

解释:调用函数后,输入的数组将被修改为:[1,2,3]

(2)一般解法

  • 如果使用额外的空间,再创建一个相同大小空间的临时数组,并将原数组元素全部拷贝过来。然后,使用两个整型变量表示两个数组的下标索引,临时数组遇到非零正常拷贝,遇到0,拷贝两个下来。
  • 代码如下,只需要遍历一遍数组,时间复杂度是O(N),但是消耗了原数组相同元素的空间,空间复杂度也是O(N)。
void duplicateZeros(vector<int>& arr)
{
    int n = arr.size();
    vector<int> tmp(arr);

    for (int i = 0, j = 0; j < n; i++)
    {
        if (tmp[i] != 0)
        {
            arr[j++] = tmp[i];
        }
        else
        {
            if (j + 1 < n)
                arr[j] = arr[j + 1] = 0;
            else
                arr[j] = 0;

            j += 2;
        }
    }
}

(3)双指针解法

这道题不允许开辟一个新的数组,必须就地修改。不过这道题也是关于处理区域划分的问题,可以使用双指针。

  • 按照第一道题的方法,创建两个表示下标的变量,往后移动划分区域,遇到0元素,往后再添加一个0,但是会覆盖后面的元素,需要临时变量存储,但是不直到需要几个临时变量,所以不能按照第一道题的方法。
  • 不过可以先使用类似第一题的双指针解法找到原数组填充完0后的最后一个数字,再从后往前使用双指针解法填充数组,这样就不会有被覆盖的风险。

  • 解法操作:先定义两个整型变量dest和cur,分别赋值为-1和0,代表首元素前一个位置的下标和首元素的下标。判断cur下标的元素是否为0,如果为不为0,dest变量就加一,如果为0,dest下标加二,表示填充两个零。不管dest加多少,cur只能加一,表示向后移动一个位置。
  • 在下面图示中,我们以数组[1,0,2,3,0,4,5,0]为例。一开始cur下标的元素不为0,dest加一,cur也加一。dest等于0,cur等于1。然后,此时cur下标的元素为0,dest加二,cur加一,都指向2这个元素。

  • 如上图,结束的条件是dest的值大于等于末尾元素下标值。此时,cur指向的数字就是扩充后数组的最后一个元素。我们仿照第一题的方法从后往前移动两个变量,这样就不会有前面元素覆盖后面元素的问题。
  • 解法操作:我们先判断cur下标的元素是否为0,如果不为0,dest指向的元素值赋值为cur指向的元素,dest减一;如果为0,dest指向的元素和前一个元素,都赋值为0,dest减二。不管dest如何变化,cur都要减一。重复这个操作,直到cur的值变为首元素下标的0时,才停止。
  • 根据解法操作,先判断cur指向元素,不是0,dest指向的元素赋值为4,dest减一,cur也减一

  • 下面是的图示过程,都是按照解法操作,进行赋值,对变量进行加加,在这里体现为向左移动。当

  • 不过需要注意的是,如果你按上面的解法操作写出代码并提交,会有上图堆栈溢出的报错,说明越界访问。这是为什么呢?
  • 我们将上面示例的数组中的4改成0,即是[1,0,2,3,0,0,5,0],然后进行第一次双指针算法,找到扩充后的最后一个数字。但是dest指向末尾元素的下一个位置,这是为何?
  • 0元素要乘于两倍,所以不过如何最后0出现偶数次。我们这里的总数是偶数,但是包含在内的非0元素有奇数个,奇数加上偶数还是奇数,并且刚好0元素出现在扩充数组后的最后一个位置上,所以dest指向的不是最后一个元素。同理,当总数是奇数个,0元素一定是偶数,如果非0元素也是偶数个时,dest也会指向末尾元素的下一个位置。

  • 如果不对这个情况进行处理,按照之前的从后往前移动进行操作,你会发现首元素放在越界的位置,此时下标为-1。

  • 如果要避免这个情况,需要在第一次双指针操作之后,就让dest减二,让它指向倒数第二个元素,并且最后一个元素位置赋值为0,cur也减一,指向前一个元素。

如果看完上面的图示,充分理解之后,写出代码不是困难的事情。

    void duplicateZeros(vector<int>& arr) 
    {
        int cur1 = 0, dest1 = -1;
        
        //找到修改完后数组的最后一个元素的位置
        while (cur1 < arr.size())
        {
            if (arr[cur1] == 0)
                dest1 += 2;
            else
                ++dest1;

            //当dest1大于等于末尾元素下标,就终止循环
            if (dest1 >= arr.size() - 1 )
                break;

            ++cur1;
        }

        //处理dest指向末尾元素的后一个位置的情况
        if (dest1 == arr.size())
        {
            arr[arr.size() - 1] = 0;
            cur1--;
            dest1 -= 2;
        }
        
        //第二次双指针操作,从后往前修改数组
        while (cur1 >= 0)
        {
            if(arr[cur1] != 0)
            {
                arr[dest1--] = arr[cur1];
            }
            else
            {
                arr[dest1] = arr[dest1 - 1] = 0;
                dest1 -= 2;
            }
            --cur1;
        }
    }

3. 快乐数

(1)题目及示例

题目:编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n快乐数 就返回 true ;不是,则返回 false

示例 1:

输入:n = 19

输出:true

解释:

1^{2}+9^{2}=82

8^{2}+2^{2}=68

6^{2}+8^{2}=100

1^{2}+0^{2}+0^{2}=1

示例 2:

输入:n = 2

输出:false

(2) 题目分析及思路

这道题本来有些难度,但是题目中有指出这个过程会成一个循环,所以就不用考虑不成环的情况,不然比较棘手。

  • 我们以11举例,两个1的平方相加等于2,以此类推到20,其中2的平方等于4,最后成为一个循环。用题目给的示例1数字19,最后到1。其实也是一个循环,1的平方等于1,是它本身。所以我们可以使用环形链表中的快慢双指针,先找到进循环的第一个数字,判断是否是1,如果是就是快乐数。
  • 其中使用快慢双指针,慢指针走一步,快指针走两步,就能寻找环形链表循环的结点入口的原因。我这篇《链表OJ题第二弹:环形链表和环形链表 II》中有详细讲解。http://t.csdnimg.cn/IsmYF

这是判断环形链表的代码。

bool hasCycle(struct ListNode *head) 
{
    struct ListNode* slow = head, *fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast)
        {
            return true;
        }  
    }
    return false;
}

我们只要再写一个函数用于拆分数字的每一位,并平方再相加。然后像环形链表的双指针方法一样,只不多每走一步,相当于调用一个函数。当这两个数字相等时,就是循环的第一个数字,再判断是否为0。

    int bitsum(int n)
    {
        int sum = 0, tmp;
        while(n > 0)
        {
            tmp = n % 10;
            sum += (tmp * tmp);
            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;
    }

(3) 证明

这道题的题目给出提示,按照寻找快乐数的方法持续下去,一定会成一个循环。我们可以证明这个过程。

  1. 数字n范围在1\leq n\leq2^{31} - 1之中。最大的数是2147483647,是个十位数字,我们取一个9999999999,把每一位的平方相加,等于810。
  2. 也就是说,在这个范围内随便给出一个数字,然后取每一位数,平方再相加,不会超过810。我们假设一个数字恰巧经过810次操作,都没有成一个循环,那么在第811次操作下,必然出现一个数和前面的数重复,组成循环。况且2^{31} - 1没有比9999999999小,进行操作后都达不到810这个数字,必然在810次操作内形成一个循环。


总结

如果认真做这三道题,并且通过画图来熟悉整个流程,对与双指针算法的理解更深入,不知可以使用指针,还可以使用变量函数来记录信息,从而提高效率。

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1853273.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kubernetes相关生态

1、Prometheus、Metrics Server与Kubernetes监控体系 简介&#xff1a; Prometheus 项目与 Kubernetes 项目一样&#xff0c;也来自于 Google 的 Borg 体系&#xff0c;它的原型系统&#xff0c;叫作 BorgMon&#xff0c;是一个几乎与 Borg 同时诞生的内部监控系统 Pro…

AG32 MCU Start Kit 开发板快速入门及 21天体验活动

AG32 IDE开发环境搭建-完整版 海振远科技 2024-6-18 AG32 MCU开发板的使用 使用准备 在使用开发板前&#xff0c;请确认已经安装好开发环境。 安装环境过程&#xff0c;请参考文档《AG32 开发环境搭建.pdf》 上电&#xff1a; 给开发板5V 供电&#xff0c;打开开关&#…

平面设计软件PS/AI/ID/CDR怎么选怎么下载(附教程)

随着设计行业的普遍化&#xff0c;平面设计软件也越来越多且功能越来越强大。平面设计软件需要在电脑上运行使用&#xff0c;来进行平面画面、平面文字的设计工作。如大家所了解的&#xff0c;Adobe Photoshop、Adobe Illustrator、CorelDRAW、Adobe InDesign是平面设计中最常用…

PostgreSQL计算 queryid 原理

数据库版本 PG 16.1 queryid 是什么 queryid 是将 sql 规范化 (normalization) 后&#xff0c;通过哈希函数计算出来的 64 位整数。 以 SELECT id, data FROM tbl_a WHERE id < 300 ORDER BY data; 这条 SQL 为例。当我们在 PG 中执行这条 sql 时&#xff0c;内核在语义…

技术性屏蔽百度爬虫已经一周了!

很久前明月就发现百度爬虫只抓取、只收录就是不给流量了&#xff0c;加上百度搜索体验越来越差&#xff0c;反正明月已经很久没有用过百度搜索&#xff0c;目前使用的浏览器几乎默认搜索都已经修改成其他搜索引擎了&#xff0c;真要搜索什么&#xff0c;一般都是必应谷歌结合着…

【设计模式深度剖析】【11】【行为型】【解释器模式】| 以算术表达式求值为例加深理解

&#x1f448;️上一篇:状态模式 设计模式-专栏&#x1f448;️ 文章目录 解释器模式定义英文原话直译 解释器模式中的角色1. 抽象表达式&#xff08;AbstractExpression&#xff09;2. 终端表达式&#xff08;TerminalExpression&#xff09;3. 非终端表达式&#xff08;Non…

Linux 特殊变量 $?

一. 说明 在 Linux 和其他类 Unix 系统中&#xff0c;$? 是一个特殊的变量&#xff0c;用于获取上一个命令的退出状态码。 退出状态码是一个整数值&#xff0c;通常用来表示命令的执行结果。 ⏹退出状态码的含义 0&#xff1a;命令成功执行。0以外的数字&#xff1a;命令执…

[SAP ABAP] 插入内表数据

语法格式 INSERT <wa> INTO <itab> INDEX <idx>. <wa>&#xff1a;代表工作区 <itab>&#xff1a;代表内表 <idx>&#xff1a;代表索引值 示例1 结果显示&#xff1a; 语法格式 INSERT <wa> INTO TABLE <itab>. <wa>&…

文件创建与查看

touch touch命令用于创建一个新的文件。 语法&#xff1a;touch Linux路径 其中路径可以是相对路径、绝对路径或者特殊路径符都可以。 改图展示了通过 touch test.txt 命令创建了一个 test.txt文件&#xff0c;其中深色的代表文件夹&#xff0c;白色的代表文件。 使用 ls -lh…

昇思25天学习打卡营第5天|模型训练|保存与加载

学AI还能赢奖品&#xff1f;每天30分钟&#xff0c;25天打通AI任督二脉 (qq.com) 模型训练 模型训练一般分为四个步骤&#xff1a; 构建数据集。定义神经网络模型。定义超参、损失函数及优化器。输入数据集进行训练与评估。 现在我们有了数据集和模型后&#xff0c;可以进行…

QTreeView双击任意列展开

一.效果 二.原理 重点是如何通过其他列的QModelIndex(假设为index),获取第一列的QModelIndex(假设为firstColumnIndex)。代码如下所示: QModelIndex firstColumnIndex = model->index(index.row(), 0, index.parent()); 这里要注意index函数的第三个参数,第三个参…

切换国内yum源

切换国内yum源 一、备份现有YUM源二、下载新的YUM源配置文件三、先清理YUM缓存再生成四、测试新的YUM源五、重启系统服务(生效可以不重启)可选 首先&#xff0c;切换国内YUM源的具体步骤取决于您使用的Linux发行版和当前的YUM源配置。以下是一般步骤&#xff0c;适用于大多数基…

学会python——对目录的操作(python实例十)

目录 1、认识Python 2、环境与工具 2.1 python环境 2.2 Visual Studio Code编译 3、遍历当前目录 3.1 代码构思 3.2 代码示例 3.3 运行结果 4、删除目录中的文件 4.1 代码构思 4.2 代码示例 4.3 运行结果 5、总计 1、认识Python Python 是一个高层次的结合了解释性…

CTF-蓝帽杯 2022 初赛Misc计算机取证题目详解

使用工具&#xff1a;Volatility、Passware Kit、Arsenal Image Mounter、DiskGenius 题目文件如下&#xff1a; 首先要知道这些文件是什么&#xff1a; dmp后缀指Dump文件&#xff0c;是windows系统中的错误转储文件。包含计算机程序运行时的内存信息的文件。通常操作系统或…

基于SpringBoot+Vue二手交易平台设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝1W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;还…

经典面试题【作用域、闭包、变量提升】,带你深入理解掌握!

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;我是前端菜鸟的自我修养&#xff01;今天给大家分享经典面试题【作用域、闭包、变量提升】&#xff0c;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;原创不易&#xff0c;如果能帮助到带大家&#xff0…

Codeforces Round 953 (Div. 2) A~F

A.Alice and Books&#xff08;思维&#xff09; 题意&#xff1a; 爱丽丝有 n n n本书。第 1 1 1本书包含 a 1 a_1 a1​页&#xff0c;第 2 2 2本书包含 a 2 a_2 a2​页&#xff0c; … \ldots …第 n n n本书包含 a n a_n an​页。爱丽丝的操作如下&#xff1a; 她把所有的…

CTO的职责是什么?

看《架构思维》作者是这样讲的&#xff1a; CTO 到底是做什么的&#xff1f; 我当下的答案是&#xff1a;“CTO 就是一个从技术视角出发&#xff0c;为公司或者所在的部门做正确决策的 CEO。”怎么理解这句话呢&#xff1f;作为一个 CTO&#xff0c;其长期目标和决策优先级与…

【Linux】系统文件IO·文件描述符fd

前言 C语言文件接口 C 语言读写文件 1.C语言写入文件 2.C语言读取文件 stdin/stdout/stderr 系统文件IO 文件描述符fd&#xff1a; 文件描述符分配规则&#xff1a; 文件描述符fd&#xff1a; 前言 我们早在C语言中学习关于如何用代码来管理文件&#xff0c;比如文件的…

搜狐视频全自动工具

项目介绍&#xff1a; 首先我们要用自己的一个号&#xff0c;作为主号&#xff0c;主号上发作品&#xff0c;利用不实名的小号通过脚本自动去浏览视频 小号看视频的同时会出现搜狐官方强行插入的广告&#xff0c;从而获得收益 收益介绍&#xff1a; 小号看一个视频会出现3-…