优选算法——双指针


《数据结构与算法》《c++起始之路》


目录

1.双指针的用法

2.双指针的相关题解


1.双指针的用法

常见的双指针有两种形式,一种是对撞指针,一种为左右指针。

**对撞指针:**一般用于顺序结构中,也称左右指针。

●对撞指针从两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐往中间逼近

●对撞指针的终止条件一般是两个指针相遇或错开(也可能是在循环内部找到结果直接跳出循环)

left==right(两个指针指向同一个位置)

left<right(两个指针错开)

**快慢指针:**又称为龟兔赛跑算法,基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。

这种方法对处理环形链表或数组非常有用。不只是环形链表或数组,若我们要研究的问题出现循环往复的情况时,均可以考虑使用快慢指针的思想。

快慢指针的实现方式有很多种,最常用的一种就是:

●在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一块一慢。

2.双指针的相关题解

2.1移动零

【数组分两块】是非常常见的一种题型,主要就是根据一种划分方式,将数组的内容分为左右两部分。这种类型的题,一般就是使用【双指针】来解决。

算法思路(快排的思想:数组划分区间~数组分两块):

在本题种,我们可以用一个cur指针来遍历整个数组,另一个dest指针来记录将要排的非零数序列位置。根据cur在遍历的过程中,遇到的不同情况,分类处理,实现数组的划分。

在cur遍历期间,是[0,dest-1]的元素全部是非零元素,[dest,cur-1]的元素全是零。

算法流程:

a.初始化cur=0(用来遍历数组),dest=-1(指向将要排的非零序列的位置)

b.cur依次往后遍历每个元素,遍历到的元素会有下面两种情况:

i.遇到的元素为0,cur直接++。因为我们的目标是让[dest,cur-1]内的元素全为0,因此当cur遇到0的时候,直接++;

ii.遇到的元素不是0,交换cur位置和dest位置的元素,之后让dest++、cur++,扫描下一个元素。

●因为dest指向的位置是将要排的非零元素区间的第一个位置,若扫描到一个新的非零元素,那么它的位置应在dest的位置上;

●dest位置直接与cur位置的元素交换,dest++,实现[0,dest-1]的元素全部都是非零元素,[dest,cur-1]的元素全是零。

代码:

算法总结:

是个方法是【快排算法】的【数据划分】的重要一步。若将快排算法拆解的话,这一段小代码是实现快排算法的【核心步骤】

2.2复写零

解法思路(原地复写-双指针):

如果【从前向后】进行原地复写操作的话,由于0的出现会复写两次,导致没有复写的数【被覆盖掉】。因此我们选择【从后往前】的复写策略。但是【从后向前】复写的时候,我们需要找到【最后一个复写的数】,因此我们的大体流程分为两步:

i.先找到最后一个复写的数;

ii.然后从后向前进行复写操作。

算法流程:

a.初始化两个指针cur=0,dest=-1;

b.找到最后一个复写的数:

i.当cur<n的时候,一直执行下面循环:

●​​​判断cur位置的元素:

若为0,dest往后移动两位;否则dest往后移动一位。

●判断dest时候已经到结束位置,若结束就终止循环;

●如果没有结束,cur++,继续判断。

c.判断dest是否越界到n的位置:

i.若越界,执行下面三步:

●n-1的位置值直接修改为0;

●cur向前移动一步;

●dest向前移动两步。

d.从cur位置开始往前遍历原数组,依次还原出复写后的结果数组:

i.判断cur位置的值:

●若为0:dest及dest-1的值修改为0,dest-=2;

●若非0,dest位置修改为cur位置的值,dest--;

ii.cur--,复写下一个位置。

代码:

2.3快乐数

题目分析:

为了方便叙述,将【对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和】这一个操作记为x操作;

题目告诉我们,当我们不断重复x操作的时候,计算一定会【死循环】,死的方式有两种:

●情况一:一直在1中死循环,即1->1->1->......->1

●情况二:在历史的数据中死循环,但始终变不到1

由于上述两种情况只会出现一种,因此,只要我们能确定循环时在【情况一】中进行,还是在【情况二】中进行,就能得到结果。

简单证明:

a.经过一次变化之后的最大值9^2*10=810(2^31-1=2147483647。选一个最大数9999999999),即变化的区间在[1,810]之间;

b.根据【鸽巢原理】一个数变化811次之后,必然会形成一个循环;

c.因此,变化的过程最终会走到一个圈里面,因此可以用【快慢指针】来解决。

算法思路(快慢指针):

根据上述的题目分析,我们可以知道重复执行x的时候,数据会陷入到一个【循环】之中。而【快慢指针】有一个特性,就是在一个圆圈中,快指针总是会追上慢指针的,也就是说他们总会相遇在一个位置(参考带环链表)。若相遇位置的值是1,那么这个数一定是快乐数;若相遇位置不是1的化,就不是快乐数。

补充知识:如何求一个数每个位置上的数字的平方和

a.把每一位的数提取出来:

循环迭代下面步骤:

i.int t=n%10提取个位;

ii. n/=10除去个位;

直到n的值变为0;

b.提取每一位的时候,用一个变量sum记录这一位的平方与之前提取位数的平方和

●sum+=t*t

代码:

2.4盛最多水的容器

算法思路一(暴力求解)(超时):

枚举出能构成的所有容器,找出其中的最大值。

●容器容积的计算方式:设两指针i,j,分别指向水槽板的最左端和最右端,此时容器的宽度位j-i。由于容器的高度有两板中的短板决定,因此可得容积公式:v=(j-i)*min(height[i],height [j])

class Solution{

public:

int maxArea(vector<int> &height){

int n=height.size();

int ret=0;

//两层for枚举出所有可能的情况

for(int i=0;i<n;i++){

for(int j=i+1;j<n;j++){

//计算容积,找出最大的一个

ret=max(ret,min(height[i],height[j])*(j-i));

}

}

return ret;

}

};

算法思路二(对撞指针):

设两个指针left,right分别指向容器的左右两个端点,此时容器的容积:v=(right-left)*min(height[right],height[left])容器的左边界为height[left],右边界为height[right]。

为了方便叙述,我们假设【左边边界】小于【右边边界】。若此时我们固定一个边界,改变另一个边界,水的容积会有如下变化形式:

●容器的宽度一定变小

●由于左边界较小,决定了水的高度。若改变左边界,新的水面高度不确定,但是一定不会超过右边右边柱子的高度,因此容器的容积可能会增大

●若该改变右边界,无论右边界移动到哪里,新的水面的高度一定不会超过左边界,也就是不会超过现在的水面高度,但是由于容器的宽度减小,因此容器的容积一定会变小

由此可见,左边界和其余边界的组合情况都可以舍去。所有我们可以left++跳过这个边界,继续去判断下一个左右边界。

当我们不断重复上述过程,每次都可以舍去大量不必要的枚举过程,直到left和right相遇。期间产生的所有容积里面的最大值,就是最终答案。

代码:

2.5有效三角形的个数

算法思路一(暴力,超时):

三层for循环枚举出所有的三元组,并且判断是否能构成三角形。

优化情况下的暴力,判断三角形的优化:

●若能构成三角形,需要满足任意两边之和大于第三边。但实际上只需让较小的两条边之和大于第三边

●因此我们可以先将原数组排序,然后从小到大枚举三元组,一方面省去枚举的数量,另一方面方便判断是否能构成三角形

class Solution{

public:

int triangleNumber(vector<int> &nums){

//排序

sort(nums.begin(),nums.end());

int n=nums.size(),ret=0;

//从小到大枚举所有的三元组

for(int i=0;i<n;i++){

for(int j=i+1;j<n;j++){

for(int k=j+1;k<n;k++){

//当最小的两个边之和大于第三边时,统计答案

if(nums[i]+nums[j]>nums[k]) ret++;

}

}

}

return ret;

}

};

算法思路二(排序+双指针):

先将数组排序。根据【思路一】中的优化思想,我们可以固定一个【最长边】,然后在比这条边小的有序数组中找出一个二元组,时这个二元组之和大于这个最长边。由于数组是有序的,我们可以利用【对撞指针】来优化。

设最长边枚举到i位置,区间[left,right]是i位置左边的区间(即比它小的区间):

●若nums[left]+nums[right]>nums[i]:

●说明[left,right-1]区间上的所有元素均可以与nums[right]构成比nums[i]大的二元组

●满足条件的有right-left种

●此时right位置的元素的所有情况相当于全部考虑完毕,right--,进入下一轮判断

●若nums[left]+nums[right]<=nums[i]:

●说明left位置的元素是不可能与[left+1,right]位置上的元素构成满足条件的二元组

●left位置的元素可以舍去,left++进去下一轮循环

代码:

2.6查找总价格为目标值的两个商品

算法思路一(暴力,超时):

两层for循环列出所有两个数字的组合,判断是否等于目标值

算法流程:

两层for循环:

●外层for循环依次枚举第一个数a

●内层for循环依次枚举第二个数b,让它和a匹配;(注:这里有一个细节,挑选第二个数时,不可以从第一个数开始,因为a前面的数我们都已经在之前考虑过了,因此,我们可以从a往后的数开始列举)

●然后将挑选的两个数相加,判断是否符合目标值

class Solution{

public:

vector<int> twoSum(vector<int>& numbers,int target){

int n=nums.size();

for(int i=0;i<n;i++){//第一层循环从前往后列举第一个数

for(int j=i+1;j<n;j++){//第二层从i位置后列举第二个数

if(nums[i]+nums[j]==target)//两数和等于目标值,则找到结果

return {nums[i],nums[j])};

}

}

return {-1,-1};

}

};

算法思路二(双指针-对撞指针):

升序数组,可以用【对撞指针】优化时间复杂度。

算法流程:

a.初始化left,right分别指向数组的左右两端(这里不是真正的指针,而是数组的下标)

b.当left<right的时,一直循环

i.当nums[left]+nums[right]==target时,说明找到结果,记录结果,并且返回;

ii.当nums[left]+nums[right]<target时:

●对于nums[left]而言,此时nums[right]相当于时nums[left]能碰到的最大值。如果此时不符合要求,说明在这个数组里面,没有别的数符合nums[left]的要求了。因此,我们可以大胆舍去这个数,left++,比较下一组数据;

●对于nums[right]而言,由于此时两数之和是小于目标值的,nums[right]还可以选择比nums[left]大的值继续比较,因此right不需要改变

iii.当nums[left]+nums[right]>target时,同理我们可以舍去nums[right]。让right--,继续比较下一组数据,而left指针不变

代码:

2.7三数之和

算法思路:

本题与两数之和类似,是经典的面试题。

与两数之和稍微不同的是,题目中要求找到所有【不重复】的三元组。我们可以利用在两数之和那里用的双指针思想,来对暴力枚举进行优化:

i.先排序;

ii.然后固定一个数a;

iii.在这个数后面的区间内,使用【双指针算法】快速找到两个数之和等于-a即可。

但是要注意到是,这道题里面需要有【去重】操作

i.找到一个结果之和,left和right指针要【跳过重复】的元素;

ii.当使用完一次双指针算法之后,固定的a也要【跳过重复】的元素。

代码:

2.8四数之和

算法思路(排序+双指针):

a.依次固定一个数a;

b.在这个数a后面区间上,利用【三数之和】找到三个数,是这三个数的和等于target-a即可

代码:

相关推荐
努力努力再努力wz2 小时前
【Linux网络系列】:JSON+HTTP,用C++手搓一个web计算器服务器!
java·linux·运维·服务器·c语言·数据结构·c++
魂梦翩跹如雨2 小时前
死磕排序算法:手撕快速排序的四种姿势(Hoare、挖坑、前后指针 + 非递归)
java·数据结构·算法
夏鹏今天学习了吗9 小时前
【LeetCode热题100(87/100)】最小路径和
算法·leetcode·职场和发展
哈哈不让取名字9 小时前
基于C++的爬虫框架
开发语言·c++·算法
Lips61111 小时前
2026.1.20力扣刷题笔记
笔记·算法·leetcode
2501_9413297211 小时前
YOLOv8-LADH马匹检测识别算法详解与实现
算法·yolo·目标跟踪
洛生&11 小时前
Planets Queries II(倍增,基环内向森林)
算法
小郭团队12 小时前
1_6_五段式SVPWM (传统算法反正切+DPWM2)算法理论与 MATLAB 实现详解
嵌入式硬件·算法·matlab·dsp开发
小郭团队12 小时前
1_7_五段式SVPWM (传统算法反正切+DPWM3)算法理论与 MATLAB 实现详解
开发语言·嵌入式硬件·算法·matlab·dsp开发