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

目录

前言

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;}elsetmp[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;elsearr[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.mzph.cn/news/863361.shtml

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

相关文章

61.ThreadLocal认识和使用

ThreadLocal介绍 ThreadLocal类用来提供给线程内部的局部变量。 这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。 ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。 ThreadLocal的作用…

MySQL之索引创建原则

索引创建原则有哪些&#xff1f; 1.针对数据量较大&#xff0c;且查询比较频繁的表建立索引。&#xff08;单表超过10w数据&#xff09; 2.针对常作为查询条件&#xff08;where&#xff09;、排序&#xff08;order by&#xff09;、分组&#xff08;group by&#xff09;操…

Hadoop 安装与伪分布的搭建

目录 1 SSH免密登录 1.1 修改主机名称 1.2 修改hosts文件 1.3 创建hadoop用户 1.4 生成密钥对免密登录 2 搭建hadoop环境与jdk环境 2.1 将下载好的压缩包进行解压 2.2 编写hadoop环境变量脚本文件 2.3 修改hadoop配置文件&#xff0c;指定jdk路径 2.4 查看环境是否搭建完成 3 …

Clickhouse 常见操作

数据查询 从json array string中解析字段 json array string 为json.dumps(array(dict)) select JSONExtractString(row,"Date") as Date from( select arrayJoin(JSONExtractArrayRaw(Remarks)) as row from table x )JSONExtractArrayRaw&#xff1a; 将JsonS…

python中的相对路径

在Python中&#xff0c;相对路径是相对于当前工作目录&#xff08;由os.getcwd()返回&#xff09;的路径。当你想要引用当前目录、父目录或子目录中的文件或目录时&#xff0c;你会使用相对路径。 以下是一些常见的相对路径写法&#xff1a; 引用当前目录下的文件或目录&#…

C# Modbus设备信息加载的实现方式(2)

GlobalProperties是一个全局的数据&#xff0c;类似CoreData&#xff1a; public class GlobalProperties{public static Device Device { set; get; }public static Action<int, string> AddLog;public static SysAdmin CurrentAdmin;public static ModbusTCP Modbus { …

基于Spring Boot的药房信息管理系统

1 项目介绍 1.1 研究的背景及意义 随着社会的飞速进步和药房行业竞争的白热化&#xff0c;传统的手工管理模式已难以适应药房信息管理的现代化需求。在计算机科学技术日臻完善的背景下&#xff0c;药房信息管理者们日益认识到运用计算机技术进行信息管理的迫切性和重要性。计…

【Git】LFS

什么是lfs Git 是分布式 版本控制系统&#xff0c;这意味着在克隆过程中会将仓库的整个历史记录传输到客户端。对于包涵大文件&#xff08;尤其是经常被修改的大文件&#xff09;的项目&#xff0c;初始克隆需要大量时间&#xff0c;因为客户端会下载每个文件的每个版本**。Gi…

快手正式推出Vision Pro版本,引领虚拟现实社交新潮流

6月28日&#xff0c;快手正式推出其专为Apple Vision Pro打造的版本——快手vp版app&#xff0c;成为国内首批登陆Apple Vision Pro的短视频平台。 借助先进的虚拟现实技术&#xff0c;用户可以在快手上体验更真实生动的视频内容&#xff0c;无论是观看趣味短视频内容&#xf…

产品是应该有生命力的

产品是应该有生命力的 在日新月异的商业环境中&#xff0c;产品被寄予厚望&#xff0c;不仅仅满足基本功能需求&#xff0c;而是要能够自我革新&#xff0c;适应市场和技术的快速变化&#xff0c;以及持续吸引并留住用户。 这种生命力体现在产品的迭代升级能力、对用户需求的精…

[鹏城杯 2022]babybit

发现一个压缩包提取出来提取出来两个压缩包里面是注册表使用MiTeC Windows Registry Recovery 恢复注册表 flag在ROOT\ControlSet001\Control\FVEStats里的OsvEncryptInit和OsvEncryptComplete中 NSSCTF{2022/6/13_15:17:39_2022/6/13_15:23:46}

互联网信任危机:Perplexity搜索引擎如何破坏内容创作者的权益

前段时间&#xff0c;Perplexity搜索引擎还是一颗冉冉升起的明日之星&#xff0c;手握巨额投资&#xff0c;有很美好的未来前景&#xff0c;这时&#xff0c;如果不出意外的话&#xff0c;要出意外。 喜好儿网 Perplexity这家公司&#xff0c;它正试图通过创建一个新型的“答…

LoRaWAN网关源码分析(基础概念篇)

目录 一、简介 1、lora_gateway 2、packet_forwarder 二、目录结构 1、lora_gateway 2、packet_forwarder 一、简介 LoRaWAN网关的实现主要依赖两个源代码&#xff1a;lora_gateway和packet_forwarder。接下来&#xff0c;我们将从分析源代码入手&#xff0c;移植LoRaWAN源…

LeetCode:经典题之21、24 题解及延伸

系列目录 88.合并两个有序数组 52.螺旋数组 567.字符串的排列 643.子数组最大平均数 150.逆波兰表达式 61.旋转链表 160.相交链表 83.删除排序链表中的重复元素 389.找不同 1491.去掉最低工资和最高工资后的工资平均值 896.单调序列 206.反转链表 92.反转链表II 141.环形链表 …

深入探讨目标检测算法:原理、方法与应用

目录 1. 目标检测的基本原理 1.1 分类与定位 1.2 评价指标 2. 常见目标检测算法 2.1 传统方法 2.2 基于深度学习的方法 2.2.1 区域提议方法 2.2.2 单阶段检测方法 3. 目标检测算法的发展历程 3.1 早期阶段 3.2 深度学习时代 4. 目标检测的实际应用 4.1 自动驾驶 …

网页背景全屏就这?分享 1 段优质 CSS 代码片段!

大家好&#xff0c;我是大澈&#xff01; 本文约 700 字&#xff0c;整篇阅读约需 1 分钟。 每日分享一段优质代码片段。 今天分享一段 CSS 代码片段&#xff0c;使用 CSS 设置网页全屏背景图片&#xff0c;很简单。 老规矩&#xff0c;先阅读代码片段并思考&#xff0c;再看…

Android Focused Window的更新

启动App时更新inputInfo/请求焦点窗口流程&#xff1a; App主线程调ViewRootImpl.java的relayoutWindow()&#xff1b;然后调用到Wms的relayoutWindow()&#xff0c;窗口布局流程。焦点窗口的更新&#xff0c;通过WMS#updateFocusedWindowLocked()方法开始&#xff0c;下面从这…

MIX OTP——监督树和应用

在上一章关于 GenServer 的内容中&#xff0c;我们实现了 KV.Registry 来管理存储容器。在某个时候&#xff0c;我们开始监控存储容器&#xff0c;这样每当 KV.Bucket 崩溃时&#xff0c;我们就能采取行动。虽然变化相对较小&#xff0c;但它提出了一个 Elixir 开发人员经常问的…

独家原创 | Matlab实现CNN-Transformer多变量时间序列预测

SCI一区级 | Matlab实现BO-Transformer-GRU多变量时间序列预测 目录 SCI一区级 | Matlab实现BO-Transformer-GRU多变量时间序列预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现CNN-Transformer多变量时间序列预测&#xff1b; 2.运行环境为Matlab2023b…

【JavaScript】JavaScript简介

希望文章能给到你启发和灵感&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏 支持一下博主吧&#xff5e; 阅读指南 JavaScript入门&#xff08;1&#xff09;————JavaScript简介开篇说明一、什么是JavaScript二、JavaScript的使用2.1 开发工具的选择…