【ps】本篇有 8 道 LeetCode OJ
目录
[.1- 题目解析](#.1- 题目解析)
[.2- 代码编写](#.2- 代码编写)
[.1- 题目解析](#.1- 题目解析)
[.2- 代码编写](#.2- 代码编写)
[.1- 题目解析](#.1- 题目解析)
[.2- 代码编写](#.2- 代码编写)
[.1- 题目解析](#.1- 题目解析)
[.2- 代码编写](#.2- 代码编写)
[.1- 题目解析](#.1- 题目解析)
[.2- 代码编写](#.2- 代码编写)
[.1- 题目解析](#.1- 题目解析)
[.2- 代码编写](#.2- 代码编写)
[.1- 题目解析](#.1- 题目解析)
[.2- 代码编写](#.2- 代码编写)
[.1- 题目解析](#.1- 题目解析)
[.2- 代码编写](#.2- 代码编写)
一、算法简介
双指针(或多指针)是一种针对于线性结构的方法,适用于任何包含顺序存储的线性数据结构中,通过分别控制位于序列不同位置的两个指针,在序列中的移动,逐步简化待解决问题,以求解问题或满足特定条件。
二、相关例题
1)移动零
.1- 题目解析
这是一道典型的数组划分问题,可以选用双指针方法求解。
题目要求,将一个数组中的所有 0
移动到数组末尾,如此,数组就被划分为了非 0 和 0 两部分。也就是说,我们的目标是将非 0 元素集中放在数组左边,将 0 集中放在数组右边。
现定义两个指针 cur 和 dest,其含义和功能如下:
- cur:负责从左往右扫描数组,即遍历数组。
- dest:负责处理数组元素,dest 指向已处理区间内,非 0 元素的最后一个位置。
在实际处理元素的过程中,dest 处理元素的速度一定要慢于 cur 遍历数组的速度,也就是说,dest 基本在 cur 之前。如此,cur 和 dest 又将数组分为了三个区间:
- [ 0, dest ]:非 0 区间(已处理区间)。
- [ dest + 1, cur - 1 ]:0 区间(已处理区间)。
- [ cur, 数组长度 - 1 ]:待处理区间。
在数组中保持这三个区间,直到 cur 遍历至数组末尾,那么整个数组就是已处理的区间,被分为了非 0 和 0 两部分。
至于元素的处理方式,不难想到是交换非 0 元素和 0 元素。
.2- 代码编写
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
//初始时,初始化dest为-1,使其在cur左侧,保持三个区间的划分
int dest=-1,cur=0;
while(cur<nums.size())
{
if(nums[cur]==0)
{
cur++; //cur负责遍历数组,找到数组中的非0元素
}
else
{
swap(nums[dest+1],nums[cur]); //dest负责处理0元素,方式是与cur交换非0元素
cur++;
dest++;
}
}
}
};
将以上代码优化为:
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
for(int dest=-1,cur=0;cur<nums.size();cur++)
{
if(nums[cur])
{
swap(nums[++dest],nums[cur]);
}
}
}
};
2)复写零
.1- 题目解析
这道题最简单的解决方式,就是创建一个规模相同的新数组,按题目要求,通过双指针将原数组的元素按顺序抄入新数组中,更具体为,一个指针指向原数组待拷贝的元素,另一个指针指向新数组要拷贝的位置。但这是异地操作,题目要求的是就地操作,那么,我们其实可以在原数组上,用双指针模拟异地操作。
现定义两个指针 cur 和 dest,其含义和功能如下:
- cur:负责遍历数组。
- dest:负责处理数组元素,进行 0 的复写。
不难发现,如果 cur 是从左往右遍历数组的,那 dest 在进行 0 的复写时,会覆盖非 0 元素,即将其从数组中删除了,这显然会得到错误答案。因此,cur 不能从左往右遍历数组。
观察题目示例,在进行复写 0 后,数组的规模并未发生变化,这就意味着会有一些元素在进行复写 0 后丢失。由此,可以先让 cur 找到最后一个不会被丢弃的非 0 元素,然后从右往左遍历数组;而 dest 在 cur 的右侧,也从右往左遍历数组,进行复写 0。
要让 cur 找到最后一个不会被丢弃的非 0 元素,首先要从左往右遍历数组,在原数组上模拟一遍异地操作的过程:
- 判断 cur 位置的值,若非 0 则 dest 向后移动一步,若为 0 则 dest 向后移动两步;
- cur 始终向后移动一步,直到 dest 先到达数组末尾,此时 cur 就找了最后一个不会被丢弃的非 0 元素。
但是这样存在越界的可能,若 dest 距离数组末尾还有一步,但 cur 遇到了 0 ,dest 向后移动两步就越界了,因此需要处理这种边界问题。具体的方式是,当 dest 越界时,将数组末尾的元素改为 0 ,让 cur 向左移动一步,让 dest 向左移动两步。
.2- 代码编写
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int cur=0,dest=-1,n=arr.size();
// 1. 先找到最后⼀个非0的数
while(cur<n)
{
if(arr[cur])
dest++;
else
dest+=2;
if(dest>=n-1)
break;//防越界
cur++;
}
// 2. 处理⼀下边界情况
if(dest==n)
{
arr[n-1]=0;
dest-=2;
cur--;
}
// 3. 从后向前完成复写操作
while(cur>=0)
{
if(arr[cur])
arr[dest--]=arr[cur--];
else
{
arr[dest--]=0;
arr[dest--]=0;
cur--;
}
}
}
};
3)快乐数
.1- 题目解析
示例 1 的推导过程如下:
示例 2 的推导过程如下:
由此可见,每个数的推导过程都会形成一个环,满足题目条件的数最终会在"1"上成环,而不满足题目条件的数虽然也会成环,但环里不会有"1"。
那么,本题的求解过程就从判断一个数是否是快乐数,转化成了一个环中是否有"1",类似于判断一个链表是否有环。如此,就可以使用快慢双指针。
现定义两个指针 slow 和 fast,其含义和功能如下:
- slow:每次向后移动一步。
- fast:每次向后移动两步。
当 slow 和 fast 在一个环上有先有后,但最终一定会相遇,相遇时,判断当前的值是否为 1 即可。
.2- 代码编写
cpp
class Solution {
public:
int bitSum(int n) //按题目要求推导出下一个数
{
int sum=0;
while(n)
{
int t=n%10;
sum+=t*t;
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; //最终判断相遇时的值是否为1即可
}
};
4)盛最多水的容器
.1- 题目解析
由题,要找到盛最多水的容器,实际是要找到数组中的两个位置,使其所构成的矩形的高宽之积最大。
首先不难想到暴力枚举,用两次循环将所有数的组合枚举出来,很容易就找到最大的高宽之积,但这样显然会超时。
不过,寻找最大的高宽之积,这个思路肯定是对的。要高宽之积最大,也就是要高尽量大、宽也尽量大。
如图,在示例 1 中截取一小段数来尝试找最大的高宽之积,不难发现,在向内枚举寻找高宽之积的过程中,高宽之积明显是不断在变小的,因为向内枚举时宽始终在变小,而高不论是在变小还是不变,这使得高宽之积都会变小。
由此,就可以利用这种单调性,将暴力枚举的方式优化。具体方式是,让双指针指向数组左右两边,让它们向中间靠近,逐个枚举出高宽之积,每次枚举完,其中一个值更小的指针就向中间移动一步;两个指针相遇后,就在已经枚举出的高宽之积中,找出一个最大值即可。
.2- 代码编写
cpp
class Solution {
public:
int maxArea(vector<int>& height) {
int left=0,right=height.size()-1;
int sum=0;
while(left<right)
{
int v=min(height[left],height[right])*(right-left); //计算出当前双指针所找出的高宽之积
sum=max(sum,v); //统计最大的高宽之积
if(height[left]<height[right]) //移动指针
left++;
else
right--;
}
return sum;
}
};
5)有效三角形的个数
.1- 题目解析
题目要求,从数组中随机选出三个数,以此枚举出可以组成三角形的所有组合。不难想到暴力枚举 + 判断的方式,但要枚举三个数就要用到三层循环,这就一定会超时,因此需要对此种方法进行优化。
三个数能够构成一个三角形,一定满足"任意两边之和大于第三边"这个条件。假设三角形的三边分别为 a、b、c,满足"任意两边之和大于第三边",即满足"a + b > c && b + c > a && a + c > b"。不过,我们可以对此做一个小优化,如果"任意两边之和大于第三边"的前提是,a <= b <= c,那么 a、b、c 仅需满足"a + b > c"即可构成一个三角形,因为此时 c 是最大数,不论加上 a 还是 b 都会大于另一个数。
由此,在对三个数进行枚举之前,只需对数组进行升序排序,即可完成优化,然后利用数组的单调性和双指针来进行解题。具体的方式是,将数组末尾的数,即当前升序数组中最大的数设为 c,将数组头部和倒数第二的数分别设为 a 和 b,去与 c 进行比较,以找出满足条件的三数组合;每次比较完,都让 a 的指针向数组右侧移动一位,直到 a 和 b 相遇,就将数组的倒数第二个数设为 c,进行下一轮比较;以此类推,直到将 c 数组的第三个数设为 c,判断完此轮的"a + b > c"即完成了全部题解过程。
而在判断"a + b > c"的过程中,也存在优化的可能。当" a + b > c "不满足时,由于数组是升序的,a 当前的值就是这一轮中最小的,a 的指针再向数组右侧移动一位,也可以使" a + b > c "成立,因此,此时只需统计 a 到 b 之间数的个数,然后直接进入下一轮判断即可。
.2- 代码编写
cpp
class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(),nums.end());
int sum=0;
int n=nums.size();
for(int i=n-1;i>1;i--)
{
int left=0,right=i-1;
while(left<right)
{
if(nums[left]+nums[right]>nums[i])
{
sum+=right-left;
right--;
}
else
{
left++;
}
}
}
return sum;
}
};
6)查找总价格为目标值的两个商品
LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)
.1- 题目解析
尽管这道题用暴力枚举也会超时,但可以和上文的题目一样,通过"双指针 + 单调性"来求解。
题目中的数组已经是升序的了,只需用双指针分别指向数组的头部和尾部。数组的头部是数组中最小的数,尾部是数组中最大的数,如果双指针的值之和小于 target,则让指向数组头部的指针向右移动一步,使和增大,反之,则让让指向数组尾部的指针向左移动一步,使和减小,直至找出满足条件的数组元素。
.2- 代码编写
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
int left=0,right=price.size()-1;
while(left<right)
{
if(price[left]+price[right]>target)
{
right--;
}
else if(price[left]+price[right]<target)
{
left++;
}
else
{
return {price[left],price[right]};
}
}
return {-1,-1};
}
};
7)三数之和
.1- 题目解析
题目要求从一个数组中,找出不重复的、和为 0 的无序三元组。
我们可以通过暴力枚举,找出所有符合条件的三元组,然后进行去重。但直接去重并不方便,如果先对数组进行升序排序,再去找出所有符合条件的三元组,再进行去重,就要方便得多。不过,这显然要用到三层循环,也是要超时的。
三个数的和为 0 ,其实可以将其中一个数作为基准,去找另外两个数,使它们的和为这个数的相反数。
如图,将数组头部的元素 -4 作为基准,只需在剩下元素构成的区间中,寻找两个和为 4 的数即可。
由此,就得到了该题的解题步骤:
- 对数组进行升序排序;
- 将一个数作为基准值;
- 在剩下的区间里,找到两数之和为基准值的相反数。
其中,还涉及一些细节,例如要在剩下的区间里找完所有满足条件的两个数,不能遗漏;此外,还需要对找到的结果进行去重。
去重的具体方式是,在区间中找到一个结果后,如果 left 或 right 在移动时遇到了重复的元素,就要将其跳过,因为这个重复元素已经被统计过了;找完了当前的区间,如果 i 在移动时遇到了重复的元素,也要将其跳过;i、left、right不能越界。
.2- 代码编写
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ret;
//1.排序
sort(nums.begin(),nums.end());
int i=0,n=nums.size();
while(i<n)
{
if(nums[i]>0) //小优化
{
break;
}
//2.选基准值
int left=i+1,right=n-1,target=-nums[i];
//3.在剩余区间中查找两个数
while(left<right)
{
int sum=nums[left]+nums[right];
if(sum>target)
{
right--;
}
else if(sum<target)
{
left++;
}
else
{
ret.push_back({nums[i],nums[left],nums[right]});
left++;right--;
//去重 left
while(left<right&&nums[left]==nums[left-1])
{
left++;
}
//去重 right
while(left<right&&nums[right]==nums[right+1])
{
right--;
}
}
}
i++;
//去重 i
while(i<n&&nums[i]==nums[i-1])
{
i++;
}
}
return ret;
}
};
8)四数之和
.1- 题目解析
这道题与上一道《三数之和》类似,也可以通过排序 + 双指针来进行求解,具体的方式是,
先确定一个基准值 a ,然后在剩余的区间里找到和为"目标值 - a"的三个数即可;而对于和为目标值 - a 的三个数,可以套用先前的思路,在该区间里确定一个基准值 b,然后在剩余的区间里找到和为"目标值 - a - b"的两个数即可。余下的细节问题,也与上一道《三数之和》类似,也要注意去重和不漏。
.2- 代码编写
cpp
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ret;
//1.排序
sort(nums.begin(),nums.end());
//2.选基准值 a
int a=0,n=nums.size();
while(a<n)
{
//3.选基准值 b
int b=a+1;
while(b<n)
{
//4.在剩余区间里找和为target-a-b的两个数
int left=b+1,right=n-1;
long long aim=(long long)target-nums[a]-nums[b];//防溢出
while(left<right)
{
int sum=nums[left]+nums[right];
if(sum<aim)
{
left++;
}
else if(sum>aim)
{
right--;
}
else
{
ret.push_back({nums[a],nums[b],nums[left++],nums[right--]});
//去重left
while(left<right&&nums[left]==nums[left-1])
{
left++;
}
//去重right
while(left<right&&nums[right]==nums[right+1])
{
right--;
}
}
}
b++;
//去重b
while(b<n&&nums[b]==nums[b-1])
{
b++;
}
}
a++;
//去重a
while(a<n&&nums[a]==nums[a-1])
{
a++;
}
}
return ret;
}
};