目录
双指针算法是一种常用的算法技巧,通过使用两个指针在数组或链表中进行迭代,从而解决一些问题。下面是几种常见的双指针算法:
常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
• 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
• 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
left == right (两个指针指向同⼀个位置)
left > right (两个指针错开)
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。 其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
• 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
双指针算法通常能够在O(n)的时间复杂度内解决问题,是一种非常高效的算法技巧。在解决问题时,需要根据具体情况调整指针的移动条件和顺序。
注意:双指针算法又分为异地双指针和就地双指针两种,现在的双指针也是根据异地操作优化成就地双指针的。我们所说的双指针都是就地双指针。双指针的异地就地区别,详见:链表数组OJ题汇总:删除数组中值为val的元素
相关博客:链表数组OJ题
。。。
其实快速排序的区间划分算法(数组分块)也是一种双指针算法。(详见博客:八大排序)
前后指针法对区间元素进行划分时,我们设置了cur指针和prev指针,cur负责找小,找到小后prev先++然后交换a[prev]和a[cur],cur走完后再交换a[prev]和key值,区间划分完成。
为什么这样能保证区间成功划分呢?其实是双指针算法的使用:
在这个过程中:
cur指针:负责遍历数组中元素
prev指针:已处理区间内,小于key的最后一个元素的指针。prev指针和cur指针之间的就是大于key的元素。
数组分块是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部分。这种类型的题,⼀般就是使⽤「双指针」来解决。这和快排的核心思想一致。
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]
示例 2:
输入: nums = [0] 输出: [0]
设置两个指针:
cur指针:负责遍历整个数组,初始设置为0
dst指针:指向已处理区间中非零元素的最后一个位置,初始设为-1(一开始没有处理,为-1)
cur遇到0,cur++;cur遇到非0,dst++, a[dst]和a[cur]和交换。
数组被分为三块,详见下图:
- class Solution {
- public:
- void moveZeroes(vector<int>& nums)
- {
- //1、src遇到0,src++
- //2、src遇到非0,++dst和src交换,src++
- int dst = -1, src = 0;
- while(src < nums.size())
- {
- if(nums[src] == 0)
- {
- src++;
- }
- else
- {
- swap(nums[++dst], nums[src++]);
- }
- }
- }
- };
给你一个长度固定的整数数组 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]
这题和上一题有所不同,我们指针能从前往后走,否则会导致dst指针在cur指针的前面,导致一些cur未遍历的数据提前被dst覆盖。
所以我们可以考虑从后往前走:
先找到最后一个要复写的数。
如何找?这里又要用到双指针算法:
设置两个指针,cur指向要复写的数,初始设为0;dst指向复写的最后一个位置,初始设为-1(初始没有进行复写)。
判断a[cur]是否为0,决定dst走几步。
如果a[cur] != 0, dst++;如果a[cur] == 0 ,dst+=2。
判断一下dst是否结束(这个相当关键)。
cur++。
需要注意的点:dst结束时有两种情况,一种是dst == n-1,正好复写完成;另一种是dst == n,这种明显是复写0时出现的越界情况,需要特殊处理一下。
如何处理?:a[n-1] = 0; dst-=2;cur--;
利用双指针从后往前完成复写操作。
- class Solution {
- public:
- void duplicateZeros(vector<int>& arr)
- {
- //1.找到最后一个复写的数
- /*
- 1.根据src对应的值来判断dst走几步
- 2.dst走完后判断是否结束(dst >= n - 1时结束,循环break)
- 3.src++
- */
- int dst = -1, src = 0, n = arr.size();
- //变量dst:最后一个复写的位置;一开始没有复写,设置为-1。
- while(src < n)
- {
- if(arr[src] != 0)
- dst++;
-
- if(arr[src] == 0)
- dst += 2;
-
- if(dst >= n - 1)
- break;
-
- src++;
- }
-
- //2.控制一下边界,因为dst可能走到了n的位置
- if(dst == n)
- {
- arr[n - 1] = 0;
- dst -= 2;
- src--;
- }
-
- //3.利用双指针从后往前复写
- while(src >= 0)
- {
- if(arr[src] != 0)
- {
- arr[dst--] = arr[src--];
- }
- else
- {
- arr[dst--] = arr[src];
- arr[dst--] = arr[src];
- src--;
- }
- }
- }
- };
快慢指针其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
示例 1:
输入:n = 19 输出:true 解释: 12 + 92 = 82 82 + 22 = 68 62 + 82 = 100 12 + 02 + 02 = 1
示例 2:
输入:n = 2 输出:false
经过上图分析:快乐数最后为1,非快乐数也会陷入一个循环中,比如2,最后陷入4~20的往复循环中。
类比以前的带环链表OJ题,我们可以使用快慢指针的思想来解决这种带环问题:
设置快指针和慢指针。
快指针一次走两步,慢指针一次走一步。
判断两个指针是否相遇,相遇即是非快乐数;如果在相遇之前快指针提前走到1的位置,说明是快乐数。
- class Solution {
- public:
- //将一个数替换为它每个位置上的数字的平方和
- int SumOfSquares(int num)
- {
- int ret = 0;
- while(num)
- {
- ret += (num % 10) * (num % 10);
- num /= 10;
- }
- return ret;
- }
-
- //快慢指针解决循环往复的问题
- bool isHappy(int n)
- {
- int num1 = n, num2 = n;
- while(num1 != 1 && SumOfSquares(num1) != 1)
- {
- num1 = SumOfSquares(SumOfSquares(num1));
- num2 = SumOfSquares(num2);
-
- if(num1 == num2)
- return false;
- }
- return true;
- }
- };
对撞指针⼀般⽤于顺序结构中,也称左右指针。 • 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。 • 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是: left == right (两个指针指向同⼀个位置) left > right (两个指针错开)
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
解法一:暴力遍历,O(N*N)
解法二:双指针(左右对撞指针),O(N)
设置左右指针left == 0 ,right == n -1;
利用单调性+短板原理,
如果height[left] < height[right],left++
,V = w * h,因为right左边的元素比left大没用(短板效应短的决定height),比left小或者相等V一定减小(w减小,h非增)
;同理,如果height[left] >= height[right],right--;
left == right后,V == 0,结束。
- //单调性+短板效应
- class Solution {
- public:
- int maxArea(vector<int>& height)
- {
- priority_queue<int, vector<int>> volume;
- int n = height.size();
- int left = 0, right = n - 1 ;
- while(left < right)
- {
- //V = w * h
- //如果height[left] < height[right],left++
- //因为right左边的元素比left大没用(短板效应短的决定height),比left小或者相等V一定减小(w减小,h非增)
- if(height[left] < height[right])
- {
- volume.push(height[left] * (right - left));
- left++;
- }
- else
- {
- volume.push(height[right] * (right - left));
- right--;
- }
- }
-
- return volume.top();
- }
- };
给定一个包含非负整数的数组 nums
,返回其中可以组成三角形三条边的三元组个数。
示例 1:
输入: nums = [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使用第二个 2) 2,2,3
示例 2:
输入: nums = [4,2,3,4] 输出: 4
解法一:暴力遍历 O(N^3)
解法二:双指针 O(N^2)
排序;
固定最大的数;
在最大数的左区间里使用双指针算法,找到符合条件的三元组。
假设i是固定的最大数,
如果left + right > i ,left右边所有的数和right相加都大于i,能组成三角形,count += right - left,right--;
如果left + right <= i ,C所有的数和left相加都不大于i,不能组成三角形,left++;
需要注意固定数的下标满足 i >= 2,保证由三个以上的数。
- class Solution {
- public:
- int triangleNumber(vector<int>& nums)
- {
- int count = 0;
- int n = nums.size();
-
- //1.排序
- sort(nums.begin(), nums.end());
- //2.固定最大的数
- int i = n - 1;
- while(i >= 2)
- {
- //3.双指针寻找符合的三元组
- int left = 0, right = i - 1;
- while(left < right)
- {
- if(nums[left] + nums[right] > nums[i])
- {
- count += right - left;
- right--;
- }
- else
- {
- left++;
- }
- }
- i--;
- }
- return count;
- }
- };
购物车内的商品价格按照升序记录于数组 price
。请在购物车中找到两个商品的价格总和刚好是 target
。若存在多种情况,返回任一结果即可。
示例 1:
输入:price = [3, 9, 12, 15], target = 18 输出:[3,15] 或者 [15,3]
示例 2:
输入:price = [8, 21, 27, 34, 52, 66], target = 61 输出:[27,34] 或者 [34,27]
解法一:暴力遍历 O(N^2)
解法二:双指针算法 O(N)
排序;
设置两个指针l == 0、r == n-1;
如果left + right > target ,left右边所有的数和right相加都大于target,right--;
如果left + right < target ,right左边所有的数和left相加都小于target,left++;
如果left + right == target ,返回。
- class Solution {
- public:
- vector<int> twoSum(vector<int>& price, int target)
- {
- vector<int> retV;
- int n = price.size();
- //1.排序
- sort(price.begin(), price.end());
- //2.利用双指针算法快速找出合为target
- int left = 0, right = n - 1;
- while(left < right)
- {
- if(price[left] + price[right] > target)
- {
- right--;
- }
- else if(price[left] + price[right] < target)
- {
- left++;
- }
- else
- {
- retV.push_back(price[left]);
- retV.push_back(price[right]);
- break;
- }
- }
- return retV;
- }
- };
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。
解法一:暴力遍历 O(N^3)
解法二:双指针算法 O(N^2)
排序。
左边固定一个数nums[i]。
双指针算法再右区间中快速找出合为-nums[i]的两个数。
优化:nums[i] > 0时,不可能找出三数和为0的情况,这种情况不讨论。
题目中有一个关键提示点:答案中不可以包含重复的三元组,比如[-4,-1,-1,0,1,2]会找出重复的三元组,这里要去重。
那么如何去重呢?两种解决办法:
set去重,但这样额外消耗空间。
- 找到一种结果后,left和right跳过重复元素;比如[-4,0,0,2,2],排序后相同元素都在一起,连续的相同元素会造成重复;
- i在完成一次双指针算法后,也要跳过重复元素。比如[-4,-1,-1,0,1,2],两个-1分别会和0、1匹配成一个三元组造成重复;
- 还需要注意的是,面对极端数据[0,0,0,0,0,0],跳过重复元素会造成越界,我们需要检查越界。
- class Solution {
- public:
- vector
int>> threeSum(vector<int>& nums) - {
- vector
int>> retV; - int n = nums.size();
- //1.排序
- sort(nums.begin(), nums.end());
- //2.左边固定一个数a
- int i = 0;
- while(i <= n - 3)
- {
- //3.双指针算法快速找出合为-a的两个数
- //3.5 小优化:如果a > 0, 右边不可能找出合为负数的两个数,这种不谈论
-
- //细节问题:
- //1.不漏:找到后不要停,继续找
- //2.不重:找到一种结果后left,right要跳过重复元素,
- // i使用完一次双指针后也要跳过重复元素;
- // 同时还要注意越界问题
- int a = nums[i];
- if(a > 0)
- break;
-
- int left = i + 1, right = n - 1;
- while(left < right)
- {
- int sum = nums[left] + nums[right];
- if(sum < -a)
- {
- left++;
- }
- else if(sum > -a)
- {
- right--;
- }
- else
- {
- retV.push_back({nums[i], nums[left], nums[right]});
- //不漏:找到后不要停,继续找
- left++;
- right--;
-
- //找到一种结果后,left、right去重
- while(nums[left] == nums[left - 1] && left < right)
- {
- left++;
- }
- while(nums[right] == nums[right + 1] && left < right)
- {
- right--;
- }
- }
- }
- i++;
- //使用完一次双指针后,i去重
- while(nums[i] == nums[i - 1] && i <= n - 3)
- {
- i++;
- }
- }
- return retV;
- }
- };
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和 d
互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8 输出:[[2,2,2,2]]
解法一:暴力遍历 O(N^4)
解法二:双指针算法 O(N^3)
排序
左边固定一个数a
再固定一个数b
利用双指针算法快速找出右区间中和为target - a - b的两个元素
关于优化和去重,和三数之和相同。
- class Solution {
- public:
- vector
int>> fourSum(vector<int>& nums, int target) - {
- vector
int>> retV; - int n = nums.size();
-
- //1.排序
- sort(nums.begin(), nums.end());
-
- //2.左边固定一个数a
- int i = 0;
- while(i <= n - 4)
- {
- int a = nums[i];
- //3.再固定一个数b
- int j = i + 1;
- while(j <= n - 3)
- {
- int b = nums[j];
- long long tmp = (long long)target - a - b;//面对极端数据int可能溢出,使用long long
- //4.利用双指针算法快速找出右区间中和为tmp的两个元素
- int left = j + 1, right = n - 1;
- while(left < right)
- {
- int sum = nums[left] + nums[right];
- if(sum < tmp)
- {
- left++;
- }
- else if(sum > tmp)
- {
- right--;
- }
- else
- {
- retV.push_back({nums[i], nums[j], nums[left], nums[right]});
- left++;
- right--;
-
- //left、right去重
- while(nums[left] == nums[left - 1] && left < right)
- {
- left++;
- }
- while(nums[right] == nums[right + 1] && left < right)
- {
- right--;
- }
- }
- }
- j++;
- //j去重
- while(nums[j] == nums[j - 1] && j <= n - 3)
- {
- j++;
- }
- }
- i++;
- //i去重
- while(nums[i] == nums[i - 1] && i <= n - 4)
- {
- i++;
- }
- }
- return retV;
- }
- };
总结: