📕作者简介:非科班在读,纯技术博客分享,致力于C/C++,涉及Python、C/C++、Linux,数据结构,git企业级开发,Mysql等。
📗本文收录于算法指南,旨在帮助读者应对各大互联网大厂笔试题,构建完整的算法体系!
前言:双指针简介
常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
。对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
。对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
◦ left == right (两个指针指向同⼀个位置)
◦ left > right (两个指针错开)
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
。 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
话不多说,现在来看看大厂们比较有代表性的面试题吧!
一、283.移动零
「数组分两块」是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部
分。这种类型的题,⼀般就是使⽤「双指针」来解决。
题目描述:
1.1 算法思想(快排的思想:数组划分区间 - 数组分两块)
在本题中,我们可以⽤⼀个 cur 指针来扫描整个数组,另⼀个 dest 指针⽤来记录⾮零数序列的最后⼀个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。在 cur 遍历期间,使 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零。
1.2 算法流程
- 初始化 cur = 0 (⽤来遍历数组), dest = -1 (指向⾮零元素序列的最后⼀个位置。因为刚开始我们不知道最后⼀个⾮零元素在什么位置,因此初始化为 -1 )
- cur 依次往后遍历每个元素,遍历到的元素会有下⾯两种情况:
①遇到的元素是 0 , cur 直接 ++ 。因为我们的⽬标是让 [dest + 1, cur - 1] 内的元素全都是零,因此当 cur 遇到 0 的时候,直接 ++ ,就可以让 0 在 cur - 1的位置上,从⽽在 [dest + 1, cur - 1] 内;
② 遇到的元素不是 0 , dest++ ,并且交换 cur 位置和 dest 位置的元素,之后让cur++ ,扫描下⼀个元素。
。因为 dest 指向的位置是⾮零元素区间的最后⼀个位置,如果扫描到⼀个新的⾮零元素,那么它的位置应该在 dest + 1 的位置上,因此 dest 先⾃增 1 ;
。dest++ 之后,指向的元素就是 0 元素(因为⾮零元素区间末尾的后⼀个元素就是0 ),因此可以交换到 cur 所处的位置上,实现 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零。
1.3 代码实现
class Solution { public: void moveZeroes(vector<int>& nums) { for(int cur = 0, dest = -1; cur < nums.size(); cur++) if(nums[cur]) // 处理⾮零元素 swap(nums[++dest], nums[cur]); } };
二、1089. 复写零
2.1 算法思路
如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」。因此我们选择「从后往前」的复写策略。
但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两
步:
- 先找到最后⼀个复写的数;
- 然后从后向前进⾏复写操作
2.2 算法流程
- ①初始化两个指针 cur = 0 , dest = -1 ;
- ② 找到最后⼀个复写的数:
- 当 cur < n 的时候,⼀直执⾏下⾯循环:
- 如果是 0 的话, dest 往后移动两位;否则, dest 往后移动⼀位。
- 判断 dest 时候已经到结束位置,如果结束就终⽌循环;
- 如果没有结束, cur++ ,继续判断。
- ③判断 dest 是否越界到 n 的位置:
- 如果越界,执⾏下⾯三步:
- n - 1 位置的值修改成 0 ;2. cur 向移动⼀步;3. dest 向前移动两步。
- ④从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:
- 判断 cur 位置的值:
- 如果是 0 : dest 以及 dest - 1 位置修改成 0 , dest -= 2 ;
- 如果⾮零: dest 位置修改成 0 , dest -= 1 ;
- cur-- ,复写下⼀个位置。
2.3 代码实现
class Solution { public: void duplicateZeros(vector<int>& arr) { int cur=0, dest=-1, n=arr.size(); //1. 先找到最后一个数 while(cur<n) { if(arr[cur]) dest++; else dest+=2; if(dest+1>=n) break; cur++; } //2. 边界处理(dest可能超出范围) if(dest==n) { arr[n-1]=0; cur--, dest-=2; } //从后向前完成复学 while(cur>=0) { if(arr[cur]) arr[dest--]=arr[cur--]; else { arr[dest--]=0; arr[dest--]=0; cur--; } } } };
三、202. 快乐数
3.1 算法思路(快慢指针)
根据上述的题⽬我们可以知道,当重复执⾏ x 的时候,数据会陷⼊到⼀个「循环」之中。
⽽「快慢指针」有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会
相遇在⼀个位置上。如果相遇位置的值是 1 ,那么这个数⼀定是快乐数;如果相遇位置不是 1
的话,那么就不是快乐数。
3.2 代码实现
class Solution { public: //用于求每个位置的平方和 int BitSum(int x) { int sum=0; while(x) { int t = x % 10;//余数 sum += t * t; x /= 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; } };
四、11. 盛最多水的容器
4.1 算法思路(对撞指针)
设两个指针 left , right 分别指向容器的左右两个端点,此时容器的容积 :
v = (right - left) * min( height[right], height[left])
容器的左边界为 height[left] ,右边界为 height[right] 。
为了⽅便叙述,我们假设「左边边界」⼩于「右边边界」。
- 如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
- 容器的宽度⼀定变⼩。
- 由于左边界较⼩,决定了⽔的⾼度。如果改变左边界,新的⽔⾯⾼度不确定,但是⼀定不会超过右边的柱⼦⾼度,因此容器的容积可能会增⼤。
- 如果改变右边界,⽆论右边界移动到哪⾥,新的⽔⾯的⾼度⼀定不会超过左边界,也就是不会超过现在的⽔⾯⾼度,但是由于容器的宽度减⼩,因此容器的容积⼀定会变⼩的。
由此可⻅,左边界和其余边界的组合情况都可以舍去。所以我们可以 left++ 跳过这个边界,继续去判断下⼀个左右边界。当我们不断重复上述过程,每次都可以舍去⼤量不必要的枚举过程,直到 left 与 right 相遇。期间产⽣的所有的容积⾥⾯的最⼤值,就是最终答案。
代码实现
class Solution { public: int maxArea(vector<int>& height) { int ret=0; int left=0, right=height.size()-1; while(left < right) { int capacity = min(height[left], height[right])*(right - left); ret =max(ret, capacity); if(height[left] < height[right]) left++; else right--; } return ret; } };