算法练习(C++)---双指针

1.移动0

283. 移动零 - 力扣(LeetCode)https://leetcode.cn/problems/move-zeroes/description/移动零这样的问题可以划分到数组划分或数组分块这样的问题里面,这类题特点是给了我们一个数组,制定一个规则,通过规则让我们把数组划分为若干区间:

移动零这道题,相当于给了我们一个数组,给我们的标准是:把非零元素左移,零元素右移:

因此这个数组会在该标准下分为两个区间。像这样的一类题我们可以把它归为数组划分或数组分块这类题,解决这类题有个经典算法是双指针算法(在数组中是利用数组下标来充当指针,为什么利用数组下标来充当指针?因为数组中有个很好的特性是可用下标访问到里面的元素,因此没必要真正定义C语言学过的指针)。接下来看如何用双指针算法解决数组分块问题:双指针肯定有两个指针:

首先来看看两个指针作用:cur用来遍历数组,从左到右扫描数组;dest是在已处理的区间内,非零元素的最后一个位置。下面解释一下,cur从左往右扫指针数组是会把数组分为左右两部分:

右边是待处理区间(cur还没扫到),左边是处理过区间(怎么处理的先不管)。处理过的区间我们想让它达到的目的是和数组的要求一样:左边是非零元素,右边是零元素,那么dest是一个分割线的作用,所以dest是在已处理的区间内,非零元素的最后一个位置。因此这两个指针把整个数组划分为三个区间:

如果我们可以保证dest和cur从左向右移动过程中一直让这三个区间保持这样的性质,当cur指针移动到n位置的时候(扫描完毕)区间划分就完成了。那如何划分呢?

cur要从左往右走,最开始指向0位置,表示没有处理;dest刚开始还没有处理,在-1的位置。cur先走,因为要扫描;当cur遇到0元素时,因为要保证cur走过的位置的区间满足非0和0,所以cur++;当cur遇到非0元素时,要把非0元素插入到已处理非0区间的后面,也就是dest的下一个位置,不能覆盖,交换处理完后cur++,这样不断走,当cur走到n位置说明这时候完成了划分。总结一下上述:cur从前往后遍历的过程中:1.遇到0元素cur++;2.遇到非0元素swap(dest+1, cur),dest++,cur++。最后拓展一下,双指针算法其实是快排最核心的一步,快排就是有个数组,选个基础元素tmp,要求讲数组划分为2部分,左边<=tmp,右边>tmp,和上述过程很像:

下面来实现代码:

2.复写0

1089. 复写零 - 力扣(LeetCode)https://leetcode.cn/problems/duplicate-zeros/题目说给了我们一个数组,让我们把数组中所有零都复写一遍,然后其余元素往后平移一位。像一般在数组中移动一些元素或操作数组中元素的题我们一般也是用双指针算法来解决问题,但这个双指针算法不是凭空来的,先根据"异地"操作,然后优化成双指针下的"就地"操作。如下图:

现在不管题目要求先进行异地操作:

开一个一样大小的数组,定义cur指针用来扫描数组,定义dest指针来表示复写的最终位置。当扫描时cur遇到非零元素,需要在dest的位置复写一遍,然后让两个指针同时向后移动一位;当扫描时cur遇到零元素,需要在dest位置复写两遍,先在dest位置复写一遍,然后dest向后移动一位,继续在dest的位置复写一遍,然后让两指针同时向后移动一位,最后dest走出数组时代表复写完了。现在看能不能把异地操作改为就地操作,直接模拟就行:

此时把两个指针定义在一个数组上,cur还是用来扫描,dest指向复写的位置。cur遇到非零元素,在dest位置复写一遍,然后两指针同时向后移动;当cur遇到零时先在dest位置复写一遍,dest向后移动一位,再到dest位置复写一遍,然后cur和dest同时向后移动一位。随着过程进行发现,原本2是要复写的,但提前被dest覆盖了,这样会导致之后都被复写成零,因此从左往右模拟是错误的,dest跑cur后面把还没有复写的数都覆盖了。再来试一试从右向左,dest指向最后要复写的位置,cur指向左后要被复写的数(例子中是4):

cur指向非零元素,在dest位置复写一次,写完后cur和dest同时左移一次;cur指向零元素,在dest位置复写一次,写完后dest向左移动一位,再向dest位置复写一次,然后dest和cur同时向左移动,最后走完复写成功。这里和前面不同的是,走向方面cur在dest前面,cur走过的位置代表已经处理过,dest再次走到该位置时是可以复写的。因此总结一下这里,双指针算法在复写的时候:1.先找到最后一个"复写"的数;2.从后向前完成复写操作。那第一步怎么找呢?这里也是一个双指针算法

cur指向零位置,表示开始被复写的数;dest指向-1位置,表示最终要复写的位置。cur指针的作用是从左向右遍历数组,决定dest向后走一步还是两步。1.先判断cur的位置;2.根据cur位置的数据决定dest向后走一步还是两步;3.判断dest是否是已经到结束的位置;4.若没有cur++。这样提交后会遇到这样的特列:

这样在开始复写操作的时候就会引发越界错误,因此还需要处理一下边界错误。就是最后复写的数是零,dest会到外面去,越界一定是cur为0引起的。因此单独处理一下:

因此该情况让n-1位置的值变为0就行了,然后dest向前移动两步,cur向前一步,然后恢复正常写就可以。下面来实现代码:

3.快乐数

202. 快乐数 - 力扣(LeetCode)https://leetcode.cn/problems/happy-number/description/给了我们一个快乐数的定义,让我们判断这个数是否是快乐数。先用题中的例子演示感受一下:

现在把这两种情况抽象一下:

一种相当于这个数一直变最后变到1;一种相当于这个数在变化过程中进入到一个循环里面,在循环里面一直转圈。其实可以把这两种情况抽象成一种情况:

第一种情况虽然最终结果到1了,但是1再经变化操作一直是1,所以第一种情况相当于也有一个环,只不过这个环中的所有数都是1罢了;第二种情况是环里所有数都不是1。所以这两种情况可以抽象为一种:这个数变化时总会进入到循环里面。抽象为这样一种情况后,看到这个图可以联想到判断链表是否有环的问题:链表有环问题是在题中给了一个链表判断链表是否有环,在这道题中可以把这些数据抽象成之前遇到的链表(因为一个数变化为一个数,相当于把这些数串了起来),我们在这不用判断链表是否有环,仅需判断一下环里面的值是多少就可以了。之前判断链表是否有环用的是快慢指针法,这道题也可以用快慢双指针法解决:1.先定义两个指针,一个快指针,一个慢指针。2.慢指针每次向后移动一步,快指针每次向后移动两步。3.判断相遇的时候的值即可。链表中定义了Node*指向链表的头结点,依次移动就可以。这里是一个个的数,这里指针怎么和数匹配到一起呢?这的指针不是真的定义一个指针,是一种思想,这道题中可把某个数当一个指针:

比如让slow是2,相当于一个指针指向2。接下来slow向右移一位就相当于进行一次操作(把它替换为每个数的平方),让slow变为4,也相当于修改slow让其指向4;同理fast开始为2,相当于一个指针指向2,接下来fast向后移动2步,因此变化2次,fast先变为4,再变为16,相当于修改指针让fast指向16。题目中已经说了要么变成1循环,要么无限循环变不到1。但假如题目中没有这句话,可能会想着有一直往下变永不成环的情况,但其实没有这样的情况,一定是有环的。接下来证明一下:这里用到了鸽巢原理,就是说有n个巢,有n+1个鸽子,结论是至少有一个巢里面的鸽子数大于1。题目中说n的最大值是整型的最大值,大概是2.1*10^9。我让这个数再大变为10个9,这个数第一变化是81*10=810。这个数已经超过了int最大值变化一次的值,因此int中的数第一次变化后一定介于[1,810],这样巢出来了。接下来随便找个数x,让这个数变化811次,必定会出现重复的数。因为假设x运气好经过810次也没有出现重复的数,正好把[1,810]填满,接下来的一次一定在1到810之间,因此不可能有第三种情况。下面来实现题目:

4.盛最多水的容器

11. 盛最多水的容器 - 力扣(LeetCode)https://leetcode.cn/problems/container-with-most-water/description/通过读题,相当于给了我们一堆数组,每个数组里面的数都代表一个高度。比如题目中给的例子:

第零条线的高度是1,第一条线的高度是8。该题让我们找出其中的两条线,使得它们与x轴共同构成的容器可以容纳最多的水,让我们返回存储的最大水量。说明中说了不能倾斜容器,意思是只能水平放置。上图题目的实例中已经把最大容器画出来了,选择了1号线和8号线。我们来计算一下若是选择了1号线和8号线它的容器怎么算:根据木桶效应,我们选择1号和8号,水的高度是由较低的线组成,所以高度是7;接下来计算宽度,宽度是从1到8的距离,8-1=7,所以宽度为7;这样下来容积就是7*7=49。下面再说最有解法之前先说一下暴力解法:题目要求是在数组中选两根线,看看哪两根线组成的容积最大,那就暴力的把所有情况都枚举出来,这样肯定可以找的到。因此解法1是暴力枚举:先固定最左边的线,依次枚举右边的线:

把所有情况算出来后找最值记录一下;然后选择固定8,依次枚举右边的线:

依次这样,两次for循环后一定能找到盛水最到的容器,但这个方法是超时的:

接下来想个优秀的解法:

先随便拿两个数,先研究[6, 2 , 5 , 4]这个区间:如果选6和4计算容积,此时计算容积的公式是v=h*w(容积=高*宽),在这里高是4,宽是3,4*3=12。

接下来4和2计算,4和5计算,发现这个过程宽度永远是在减小的(因为在向内枚举),h变化有两种情况:第一种是向内枚举时碰到了比我小的数,那这个高度要减小,这样高度*宽的最终结果一定减小(h和w都在变小);第二种是碰到了比我大的数,这样高度是不变的,这样高度*宽度最终结果也是减小的(h不变w减小)。如果总体积是减小的,我要盛水最多的容器,那这两条线就不需要向内枚举了。当我选最左和最右两个数时算出一个容积,若拿比较小的数向内枚举会发现容积是一直减小的,所以可以在小区间内把4去了:

有了这样的规律就可以理解这道题了:

拿最左和最右的数,算出v1后大胆的把1去了;研究8和7这段区间,然后再算出一个容积v2,此时可以把7去了;接下来看8和3的区间,这样依次类推(两数相同去谁都可以)。这样两指针相遇时算了很多体积,在这些体积中求一个max就是最终结果。总结一下解法2:利用单调性,使用双指针来解决问题;先定义两指针指向最左边和最右边,然后算一个体积v,算完后哪个指针指向的数小哪个就移动,然后再算容积,更新最大值,依此类推。下面来实现:

5.有效三角形的个数

611. 有效三角形的个数 - 力扣(LeetCode)https://leetcode.cn/problems/valid-triangle-number/description/题目给了我们一个数组,让我们在数组中挑出3个数,看这三个数能否组成一个三角形,让我们看有几种挑法(结合示例一看到,选法不一样,重复的也需要统计在内)。在说算法之前先补充一个数学知识:给我们三个数,判断是否能构成三角形。以前知道判断任意两边和大于第三边就行,比如给了a,b,c:

若满足上述就认为可构成三角形。这个方法可以解决问题,但是需要判断三次。我们有一种方法可以仅需判断一次就可以判断是否构成三角形,比如已经知道了三个数的大小顺序:a <= b <= c,仅需判断较小的两数和大于第三个数就行:a+b>c,为什么呢?此时知道c这个数是最大的,我们没有判断的是这样两种情况:

这两种情况都是c加一个数,c已经是最大的数了,c是一定大于等于b和a的,此时再加一个数绝对大于单独的b或a,所以这两种情况就不用判断了。当我们知道这三个数的大小顺序,仅需做一次判断就能得出这三个数是否可以构成三角形。此时有个疑问,不就少了两次判断,时间复杂度能提升到那里去?接下来讲一种解法就能发现时间复杂度是质的提升。下面先看一下暴力解法:只要找出三个数,判断一下是否是三角形就可以了,然后把所有情况都统计一下,暴力枚举大概这样:

这样三层循环得到了i,j,k组合的三元组,然后判断三元组能否构成三角行。接下来在暴力枚举的基础上看看为啥优化:三层for循环是O(N^3),check判断那里如果选判断三次的方法,相当于每次进到里面都要执行三次,这样时间复杂度是3N^3;若排个序,仅需判断一次,这样时间复杂度是N*logN + N^3;第一个明显的大于第二个。因此先对整个数组排序,这样的优化对暴力枚举是有很大提升的。还发现如果数组有序了,可利用单调性带来更多的优化,接下来看解法2:利用单调性,使用双指针算法来解决问题。如:

该数组已经有序了,判断三角形的策略是找三个数,拿最小的两个数的和看是否大于最大的:

我先固定最大的数,然后在剩下的数中标记最小和最大值。设left指的数是a,right指的数是b,记下来看两数相加能得出什么规律,相加后无非是这样两种情况:

如果相加后是第一种情况:

此时有序的缘故中间的数都是大于a的,那么这些数和b相加一定大于c,此时不用在判断中间数和b相加是否都大于c,这样就直接有了right-left这么多种情况。这样以后9就没有价值了(因为在区间枚举二元组,和9有关的都大于c,所以9不用看了),仅需让right向左移动一位:

再看该区间中有多少个构成三角形的情况。此时相加后是第二种情况,这样a和b之间的数一定都是小于等于b的,a和b相加都小于c的,a和这些数相加肯定更小于c了,所以和a有关的相加都是没有结果的,就让left向右移一位去下个区间找结果:

不断重复这样的过程,固定10的情况就会都找完。然后固定9,在前面的区间继续重复上述过程。总结一下:1.先固定最大的数 2.在最大的数的左区间内使用双指针算法,快速统计出符合要求的三元组个数。简单分析一下时间复杂度:第一步是N,第二步是N,总计是O(N^2)。下面来实现:

6.和为s的两个数字

LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/description/题目要求在数组中找两个数,让两个数的和等于target就可以,如果有多对数字和等于target,输出任意一对即可。先来说一下暴力解法。如:

既然要找两数之和相加等于t,那就把所有的两个数之和都枚举出来:

先固定第一个数2和所有数依次相加,若没有结果再固定7和所有数相加,依次类推,这样仅需两次for循环就可以解决问题:

但是题中说了数组有序,所以可以继续优化,利用单调性,使用双指针算法解决问题:

让left指向第一个数,right指向最后一个数,这两个数相加无非就三种情况:1.sum > t;2.sum < t; 3.sum == t。上述例子中两个数相加小于t,中间区间这些数都是小于right指向的数的,所以中间这些数没必要和2相加(因为right此时是整个区间中最大的数,最小的和最大的数相加都小与目标值,那其余数和最小的数相加更加小于目标值了)。因此2不可能是最终结果,把2舍去到下一个区间找

此时两数和还是小于t,继续left++去下一个区间找。接下来两数之和大于t了,此时可以把right舍去。因为此时right是最大的数,最小的数和最大的数相加都大于目标值,那其余数相加更大了,所以可以right右移舍去21:

下面来实现:

可能会遇到这样的错误,这个意思是不是所有路径都有返回值。虽然我们知道else那里一定可以有返回值,但编译器觉得while假如不成立没进去就永远没有返回值了,因此为了照顾一下编译器,我们强行返回一个-1这样一个结果。另外返回值如果是vector并且需要返回两个变量,仅需大括号括一下两个变量就可以返回:

7.三数之和

https://gitee.com/link?target=https%3A%2F%2Fleetcode.cn%2Fproblems%2F3sum%2Fhttps://gitee.com/link?target=https%3A%2F%2Fleetcode.cn%2Fproblems%2F3sum%2F题目给了一个数组,让我们找出3个位置不同的数,满足这三个数之和为0,最终把所有不重复的三元数组返回。题目的注意中说不可包含重复的三元组,结合示例一可以看到,示例一中不同位3个数和为0的有三种情况,但是输出结果是两个。发现顺序不一样,但里面元素是一样的情况表示重复,只能选一个。首先来看一下暴力解法:前面两数和的暴力解法是把所有二元数组枚举求两数之和,三数之和就是把所有三元数组枚举出来求一下和,然后枚举出来还要去重。比如示例一枚举出了符合要求的三元数组后发现两个是一样的要去重,那如何完成去重操作?可以排个序:

排完序发现有一样的,选其中一个就行。但结果中可能还会枚举出[-1, 1, 0],也就是可能好多个需要去重,此时再一个个找会非常麻烦。这时可以想到的策略是先把数组排序,为什么呢?比如先把示例一排个序:

然后规定从左向右枚举,这样最终符合要求的有两个:

发现枚举完后不用再一个个排序了,有一样的可利用set去重,所有解法一就是排序+暴力枚举+去重。下面看解法二,在暴力的基础上优化:看到有序,可以利用排序+双指针来解决。举个例子:

暴力枚举是先固定一个数,然后再剩下的数中找两个数,让这两个数的和加上我这个数等于0就行。看下图:

相当于在剩下的区间中找两个数让它们和为4就行,这样就转化为了和为s的两个数的那道题。因此固定一个数,可用双指针思想来解决问题(固定一个数后,剩下的问题变成了在有序数组中利用双指针快速找到两个数的和为4),因此解法二是排序+固定一个数a+找两数和为-a。但是还有两个细节问题:1.去重 2.不漏。先来解决不漏的问题,模拟一下:

刚开始left+right和为2小于4,让left++;接下来和为5大于4,让right--;然后找到了一个结果。

找完结果和i拼接完后,让left++,right--,缩小区间后继续找。因此解决不漏的方法是找到一种结果后不要停,继续缩小区间找。接下来解决去重,举个特例:

当0和4加完后找到了结果后,连续的0和4就不用考虑了。因此解决去重的方法是找到一种结果后,left和right指针要跳过重复元素。现在固定完第一个-4后又去固定第二个-4,这样又会出现下个区间找4的情况,又是一种重复计算,因此当使用完一次双指针算法后,i也需要跳过重复元素。还有个问题:

去重可能引发越界,所以要控制left是小于right的;i也要小于n。还有个优化点:当固定到一个正数时,不可能再后面区间找到两数之和为负数了,所以固定i时仅需在负数和0固定即可。下面来实现

8.四数之和

https://gitee.com/link?target=https%3A%2F%2Fleetcode.cn%2Fproblems%2F4sum%2Fhttps://gitee.com/link?target=https%3A%2F%2Fleetcode.cn%2Fproblems%2F4sum%2F题目给了一个由n个整数组成的数组和一个目标值,找出满足要求且不重复的四元组。先来看看暴力解法,还是先排序(枚举完后不用再重新排序了),然后依次暴力枚举,暴力枚举要用4层循环,最后利用set去重。下面看一下最优解:在前面三数之和那里是利用双指针解决的问题,这里依旧可以利用排序加双指针。比如先给示例一排个序:

三数之和那里是先固定一个数,然后在后面的区间利用双指针找到两数之和等于一个目标值。四数之和这依旧固定一个数记为a:

在后面的区间利用三数之和的思想找到一个值为target-a就可以了。因此大概思路是这样:1.依次固定一个数a。2.在a后面的区间内,利用三数之和的思想找到三个数,使这三个数的和等于target-a即可。三数之和的解决方式是:在区间中依次固定一个数b:

然后在后面的区间利用双指针找到两个数,使两数之和是target-a-b。以上是整体思路,也要处理细节问题:1.不重 2.不漏。在寻找两数之和为target-a-b时,当找到一个结果时不要停继续缩小区间往里找,这样可保证不漏。不重要在三个地方保证:当找到一个结束后,left和right要跳过相同的数;利用完双指针算法后b也要跳过重复元素;在a后面的区间,利用三数之和的思想找出三个数后,a也要跳过重复元素。下面来实现:

提交后有可能有上面这样的风险,可用longlong进行强转:

相关推荐
玖笙&5 小时前
✨WPF编程基础【1.3】:XAML 名称空间
c++·wpf·visual studio
玖笙&5 小时前
✨WPF编程基础【1.4】:类型转换器(含示例及源码)
c++·wpf·visual studio
Ling_Ze9 小时前
visual studio快捷键
c++
Stanford_11069 小时前
关于单片机的原理与应用!
c++·单片机·嵌入式硬件·微信小程序·微信公众平台·微信开放平台
雪域迷影9 小时前
C++/C#游戏开发引擎和2D/3D图形库
c++·3d·c#
HY小海11 小时前
【C++】二叉搜索树
开发语言·数据结构·c++
code monkey.11 小时前
【探寻C++之旅】第十五章:哈希表
数据结构·c++·哈希算法·散列表
今天也好累11 小时前
贪心算法之船舶装载问题
c++·笔记·学习·算法·贪心算法
lingzhilab11 小时前
零知IDE——基于STM32F407VET6和雨滴传感器的多界面TFT降雨监测显示系统
c++·stm32·单片机