双指针算法第一弹(移动零 复写零 快乐数)

目录

前言

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次操作内形成一个循环。


总结

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

计算机基础知识——C基础+C指针+char类型

指针 这里讲的很细 https://blog.csdn.net/weixin_43624626/article/details/130715839 内存地址&#xff1a;内存中每个字节单位都有一个编号&#xff08;一般用十六进制表示&#xff09; 存储类型 数据类型 *指针变量名&#xff1b;int *p; //定义了一个指针变量p,指向的数…

在Redis中使用Lua脚本实现多条命令的原子性操作

Redis作为一个高性能的键值对数据库&#xff0c;被广泛应用于各种场景。然而&#xff0c;在某些情况下&#xff0c;我们需要执行一系列Redis命令&#xff0c;并确保这些命令的原子性。这时&#xff0c;Lua脚本就成为了一个非常实用的解决方案。 问题的提出 假设我们有一个计数…

【深度学习】图形模型基础(2):概率机器学习模型与人工智能

1.引言 1.1.背景 当机器需要从经验中汲取知识时&#xff0c;概率建模成为了一个至关重要的工具。它不仅为理解学习机制提供了理论框架&#xff0c;而且在实际应用中&#xff0c;特别是在设计能够从数据中学习的机器时&#xff0c;概率建模展现出了其独特的价值。概率框架的核…

Power BI可视化表格矩阵如何保持样式导出数据?

故事背景&#xff1a; 有朋友留言询问&#xff1a;自己从Power BI可视化矩阵表格中导出数据时&#xff0c;导出的表格样式会发生改变&#xff0c;需要线下再手动调整&#xff0c;重新进行透视组合成自己想要的格式。 有没有什么办法让表格导出来跟可视化一样&#xff1f; Po…

汽车电子工程师入门系列——CAN 规范系列通读

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

SiteSucker Pro for Mac:一键下载整站,轻松备份与离线浏览!

SiteSucker Pro for Mac是一款专为苹果电脑用户设计的网站下载与备份工具&#x1f578;️。它以其强大的整站下载能力和用户友好的界面&#xff0c;成为了众多Mac用户备份网站、离线浏览的得力助手&#x1f4bb;。 这款软件允许用户一键下载整个网站&#xff0c;包括所有的网页…

Docker(八)-Docker运行mysql8容器实例

1.运行mysql8容器实例并挂载数据卷 -e:配置环境变量 --lower_case_table_names1 设置忽略表名大小写一定要放在镜像之后运行mysql8容器实例之前&#xff0c;先查看是否存在mysql8镜像以及是否存在已运行的mysql实例docker run -d -p 3306:3306 --privilegedtrue -v 【宿主机日…

L03_Redis知识图谱

这些知识点你都掌握了吗?大家可以对着问题看下自己掌握程度如何?对于没掌握的知识点,大家自行网上搜索,都会有对应答案,本文不做知识点详细说明,只做简要文字或图示引导。 Redis 全景图 Redis 知识全景图都包括什么呢?简单来说,就是“两大维度,三大主线”。 Redis …

MySQL连接IDEA(Java Web)保姆级教程

第一步&#xff1a;新建项目(File)->Project 第二步&#xff1a;New Project(JDK最好设置1.8版本与数据库适配&#xff0c;详细适配网请到MySQL官网查询MySQL :: MySQL 8.3 Reference Manual :: Search Results) 第三步&#xff1a;点中MySQLTest(项目名)并连续双击shift键-…

昇思25天学习打卡营第2天|数据集Dataset

学习目标&#xff1a;熟练掌握mindspore.dataset mindspore.dataset中有常用的视觉、文本、音频开源数据集供下载&#xff0c;点赞、关注收藏哦 了解mindspore.dataset mindspore.dataset应用实践 拓展自定义数据集 昇思平台学习时间记录: 一、关于mindspore.dataset minds…

【STM32】在标准库中使用定时器

1.TIM简介 STM32F407系列控制器有2个高级控制定时器、10个通用定时器和2个基本定时器。通常情况下&#xff0c;先看定时器挂在哪个总线上APB1或者APB2&#xff0c;然后定时器时钟需要在此基础上乘以2。 2.标准库实现定时中断 #ifndef __BSP_TIMER_H #define __BSP_TIMER_H#if…

.[emcrypts@tutanota.de].mkp勒索病毒新变种该如何应对?

引言 在数字化时代&#xff0c;随着信息技术的迅猛发展&#xff0c;网络安全问题日益凸显。其中&#xff0c;勒索病毒作为一种极具破坏力的恶意软件&#xff0c;给个人和企业带来了巨大的经济损失和数据安全风险。近期&#xff0c;一种名为“.mkp勒索病毒”的新型威胁开始在网络…

多线程引发的安全问题

前言&#x1f440;~ 上一章我们介绍了线程的一些基础知识点&#xff0c;例如创建线程、查看线程、中断线程、等待线程等知识点&#xff0c;今天我们讲解多线程下引发的安全问题 线程安全&#xff08;最复杂也最重要&#xff09; 产生线程安全问题的原因 锁&#xff08;重要…

在 Python 中创建列表时,应该写 `[]` 还是 `list()`?

在 Python 中&#xff0c;创建列表有两种写法&#xff1a; # 写法一&#xff1a;使用一对方括号 list_1 []# 写法二&#xff1a;调用 list() list_2 list() 那么哪种写法更好呢&#xff1f; 单从写法上来看&#xff0c;[] 要比 list() 简洁&#xff0c;那在性能和功能方面…

江科大笔记—读写内部闪存FLASH读取芯片ID

读写内部闪存FLASH 右下角是OLED&#xff0c;然后左上角在PB1和PB11两个引脚&#xff0c;插上两个按键用于控制。下一个代码读取芯片ID&#xff0c;这个也是接上一个OLED&#xff0c;能显示测试数据就可以了。 STM32-STLINK Utility 本节的代码调试&#xff0c;使用辅助软件…

[机缘参悟-200] - 对自然、人性、人生、人心、人际、企业、社会、宇宙全面系统的感悟 - 全图解

对自然、人性、人生、人心、人际、企业、社会、宇宙进行全面系统的感悟&#xff0c;是一个极其深邃且复杂的主题。以下是对这些领域的简要感悟&#xff1a; 自然&#xff1a; 自然是人类生存的根基&#xff0c;它充满了无尽的奥秘和美丽。自然界的平衡和循环规律&#xff0c;教…

运算符重载之日期类的实现

接上一篇文章&#xff0c;废话不多说&#xff0c;直接上代码 Date.h #pragma once #include<iostream> using namespace std; #include<assert.h>class Date {//友元函数声明friend ostream& operator<<(ostream& out, const Date& d);friend …

学编程容易遇到的误区,请提前规避

随着互联网行业的蓬勃发展和编程技术的普及&#xff0c;越来越多的人开始对编程感兴趣。然而&#xff0c;编程学习并非一蹴而就&#xff0c;新手入门时常常会陷入误区&#xff0c;影响学习状态效率。 今天&#xff0c;我们来一起揭开编程学习常见的五大误区&#xff0c;希望能…

Workbench密码登录登录失败

Workbench密码登录登录失败操作系统禁用了密码登录方式&#xff0c;会导致使用了正确的用户名和密码仍无法登录 sudo vim /etc/ssh/sshd_config 输入O进入编辑 改完后重启 systemctl restart sshd.service 登录报错 有试了几遍登上了 可能是改完还要等一会儿

Python:探索高效、智能的指纹识别技术(简单易懂)

目录 概括 导入库 函数一 参数&#xff1a; 函数二 函数三 主函数 运行结果 src&#xff1a; model_base 7.bmp ​编辑 总结 概括 指纹识别是一种基于人体生物特征的身份验证技术。它通过捕捉和分析手指上的独特纹路和细节特征&#xff0c;实现高准确度的身份识别。…