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

目录

前言

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/diannao/32702.shtml

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

相关文章

黑马苍穹外卖6 清理redis缓存+Spring Cache+购物车的增删改查

缓存菜品 后端服务都去查询数据库&#xff0c;对数据库访问压力增大。 解决方式&#xff1a;使用redis来缓存菜品&#xff0c;用内存比磁盘性能更高。 key :dish_分类id String key “dish_” categoryId; RestController("userDishController") RequestMapping…

批量重命名神器揭秘:一键实现文件夹随机命名,自定义长度轻松搞定!

在数字化时代&#xff0c;我们经常需要管理大量的文件夹&#xff0c;尤其是对于那些需要频繁更改或整理的文件来说&#xff0c;给它们进行批量重命名可以大大提高工作效率。然而&#xff0c;传统的重命名方法既繁琐又耗时&#xff0c;无法满足高效工作的需求。今天&#xff0c;…

ic基础|功耗篇03:ic设计人员如何在代码中降低功耗?一文带你了解行为级以及RTL级低功耗技术

大家好&#xff0c;我是数字小熊饼干&#xff0c;一个练习时长两年半的ic打工人。我在两年前通过自学跨行社招加入了IC行业。现在我打算将这两年的工作经验和当初面试时最常问的一些问题进行总结&#xff0c;并通过汇总成文章的形式进行输出&#xff0c;相信无论你是在职的还是…

TI毫米波雷达可以用串口调试助理来获取原始数据吗?

摘要&#xff1a;本文介绍一下如何使用普通的串口调试助理来读取到AWR1843毫米波雷达的数据的。 使用的硬件如下图所示。 软件就是普通的串口助理&#xff0c;我用的是SSCOM&#xff0c;其他串口助理也是可以的&#xff0c;核心作用其实就是发送一行行的指令而已。 操作方法&am…

20240623(26.0) 重要财经新闻

财经关注 ► 券商中国&#xff1a;北交所于6月21日晚间受理了3家企业的IPO申请。6月20日晚间&#xff0c;沪深交易所各受理了1家IPO申请。这也意味着&#xff0c;三大交易所IPO受理全部恢复。与此同时&#xff0c;三大交易所IPO上市委会议也已经全部重启。 ► 全球多地近期遭遇…

微信小程序学习(六):常用原生 API

&#x1f517;API官方文档 1、网络请求 wx.request({// 接口地址&#xff0c;仅为示例&#xff0c;并非真实的接口地址url: example.php,// 请求的参数data: { x: },// 请求方式 GET|POST|PUT|DELETEmethod: GET,success (res) {console.log(res.data)},fail(err) {console.…

msvcp140.dll丢失的解决方法,msvcp140.dll丢失下载办法

一、msvcp140.dll丢失或损坏的影响 系统更新影响 系统更新是导致msvcp140.dll丢失或损坏的常见原因之一。在自动更新过程中&#xff0c;可能会引入与现有应用程序不兼容的DLL版本&#xff0c;从而引发错误。根据用户反馈和技术支持数据&#xff0c;大约15%的msvcp140.dll问题…

2-3KW户储、家储逆变器设计资料

储能电源方案双向逆变器板资料&#xff0c;原理文件&#xff0c;PCB文件&#xff0c;源代码&#xff0c;bom清单。 bom表&#xff1a; PCB&#xff1a; 变压器电感 2-3KW户储、家储逆变器设计通常需要考虑以下几个方面&#xff1a; 输入电压范围&#xff1a;逆变器需要能够适应…

接口性能优化方法总结

接口性能优化是后端开发人员经常碰到的一道面试题&#xff0c;因为它是一个跟开发语言无关的公共问题。 这个问题既可以很简单&#xff0c;也可以相当复杂。 导致接口性能问题的原因多种多样&#xff0c;不同项目的不同接口&#xff0c;其原因可能各不相同。 下面列举几种常…

2024-6-18(沉默Spring,Springboot)

1.Spring小结 我们最后再来体会一下用 Spring 创建对象的过程&#xff1a; 通过 ApplicationContext 这个 IoC 容器的入口&#xff0c;用它的两个具体的实现子类&#xff0c;从 class path 或者 file path 中读取数据&#xff0c;用 getBean() 获取具体的 bean instance。 那…

oracle发送https请求

参照 https://docs.oracle.com/cd/E11882_01/appdev.112/e40758/u_http.htm#i1025869 https://docs.oracle.com/cd/E11882_01/network.112/e40393/asowalet.htm#ASOAG160 https://docs.oracle.com/cd/E11882_01/appdev.112/e40758/d_networkacl_adm.htm#ARPLS148 https://d…

Tailwindcss 提取组件

背景 随着项目的发展&#xff0c;您不可避免地会发现自己需要重复使用常用样式&#xff0c;以便在许多不同的地方重新创建相同的组件。这在小组件&#xff08;如按钮、表单元素、徽章等&#xff09;中最为明显。在我的项目中是图表标题样式如下&#xff1a; <div class&qu…

基于Openmv的色块识别代码及注意事项

在给出代码之前我先说注意事项以及需要用到的函数 1、白平衡和自动增益的关闭 打开白平衡和自动增益会影响颜色识别的效果&#xff0c;具体影响体现在可能使你颜色阈值发生改变 关闭代码如下 sensor.set_auto_gain(False) #关闭自动增益 sensor.set_whitebal(False) …

喜报!极限科技新获得一项国家发明专利授权:“搜索数据库的正排索引处理方法、装置、介质和设备”

近日&#xff0c;极限数据&#xff08;北京&#xff09;科技有限公司&#xff08;简称&#xff1a;极限科技&#xff09;新获得一项国家发明专利授权&#xff0c;专利名为 “搜索数据库的正排索引处理方法、装置、介质和设备”&#xff0c;专利号&#xff1a;ZL 2024 1 0479400…

Node.js版Selenium WebDriver教程

目录 介绍 导言 Selenium基础 环境设置 使用npm安装selenium-webdriver模块 配置和管理浏览器驱动器 下载火狐 下载安装 webDriver 第一个WebDriver脚本 介绍 导言 在当今数字化时代&#xff0c;Web应用程序的质量和性能至关重要。为了确保这些应用的可靠性&#xf…

我国人工智能核心产业规模近6000亿元

以下文章来源&#xff1a;中国证券报 2024世界智能产业博览会6月20日至6月23日在天津举行。会上发布的《中国新一代人工智能科技产业发展报告2024》显示&#xff0c;我国人工智能企业数量已经超过4000家&#xff0c;人工智能已成为新一轮科技革命和产业变革的重要驱动力量和战略…

【数据结构】链表的大概认识及单链表的实现

目录 一、链表的概念及结构 二、链表的分类 三、单链表的实现 建立链表的节点&#xff1a; 尾插——尾删&#xff1a; 头插——头删&#xff1a; 查找&#xff1a; 指定位置之后删除——插入&#xff1a; 指定位置之前插入——删除指定位置&#xff1a; 销毁链表&am…

【GD32】从零开始学兆易创新32位微处理器——RTC实时时钟+日历例程

1 简介 RTC实时时钟顾名思义作用和墙上挂的时钟差不多&#xff0c;都是用于记录时间和日历&#xff0c;同时也有闹钟的功能。从硬件实现上来说&#xff0c;其实它就是一个特殊的计时器&#xff0c;它内部有一个32位的寄存器用于计时。RTC在低功耗应用中可以说相当重要&#xf…

stm32学习笔记---OLED调试工具(理论部分和代码部分)

目录 理论部分 三种常用的程序调试方法 第一种是串口调试 第二种是显示屏调试 第三种是Keil调试模式 其他调试方式 OLED显示屏的介绍 OLED的硬件电路 OLED驱动程序中所包含的驱动函数 OLED_Init(); OLED_Clear(); OLED的显示函数 OLED_ShowChar(1, 1, A); OLED_S…

【教学类-36-09】20240622钓鱼(通义万相)-A4各种大小的鱼

背景需求&#xff1a; 用通义万相获得大量的简笔画鱼的图片&#xff0c;制作成不同大小&#xff0c;幼儿用吸铁石钓鱼的纸片&#xff08;回形针&#xff09;&#xff0c;涂色、排序等 补一张通义万相的鱼图 素材准备 &#xff08;一&#xff09;优质的鱼图片 &#xff08;二&a…