1. 什么是双指针算法?
双指针算法是一种常用于解决数组或链表中的问题的技巧。它涉及使用两个指针(索引或引用),通常分别称为“快指针”和“慢指针”或“左指针”和“右指针”,以协同进行遍历或搜索。
该算法的核心思想是通过移动这两个指针来实现特定的目标,例如寻找一对元素的和、判断是否存在某种关系或在特定条件下移动其中一个指针。双指针算法通常能够在O(n)的时间复杂度内解决问题,具有较好的效率。
2. 双指针算法的常见形式
常见的双指针算法有以下几种类型:
对撞指针: 在数组两端分别设立左右指针,通过向中间移动这两个指针来解决问题。例如,寻找数组中的两个元素,它们的和等于给定值。
快慢指针: 一个指针移动速度较快,另一个移动速度较慢。这常用于解决链表中的问题,如判断链表是否有环,找到链表的中间节点等。
滑动窗口(有专题描述:算法——滑动窗口): 使用两个指针维护一个窗口,通过移动窗口的左右边界解决问题。这类问题常见于字符串和数组处理,例如找到最短的包含所有字符的子串。
同向双指针: 两个指针方向相同,通过控制其中一个指针的位置来处理问题。例如,在一个有序数组中查找两个数,使它们的和等于给定值。
3. 应用实例
1. 移动零
解析:看到这道题我们这样解决,即定一个左端点在第一个0处,右指针向右寻找第一个非0元素,找到后交换这两个元素,直到遍历整个数组,代码如下
class Solution
{
public:
void moveZeroes(vector<int>& nums)
{
for (int left = 0, right = 0; right < nums.size(); right++)
if (nums[right] != 0)
swap(nums[left++], nums[right]);
}
};
2. 复写零
解析:对于这道题,我们分析一下可以发现,要想在原数组上做到复写0,我们需要从右向左复写,不然会覆盖到后面的数字,那么我们如何找到最后一个修改后数组的位置呢?那么在这里我们可以定义一个快指针dest表示慢指针所指元素是否到达数组结尾,定义一个慢指针cur表示修改后数组中末尾元素,代码如下
class Solution
{
public:
void duplicateZeros(vector<int>& arr)
{
int n = arr.size();
int cur = 0;
int dest = -1;
while (dest < n-1)
{
// 若cur对应元素为0则表示需要复写,dest+=2
if (arr[cur] != 0)
{
dest++;
}
else
{
dest += 2;
}
if (dest < n-1)
cur++;
}
// 从右向左开始复写
while (cur >= 0)
{
// 若dest超出数组范围,预处理末尾元素即可
if (dest > n-1)
{
arr[n-1] = 0;
dest-=2;
cur--;
continue;
}
// 非0元素复写一次,0元素复写2次
if (arr[cur] != 0)
{
arr[dest] = arr[cur];
dest--;
cur--;
}
else
{
arr[dest] = 0;
arr[dest-1] = 0;
dest -= 2;
cur--;
}
}
}
};
3. 快乐数
解析:我们首先分析一下19是如何变为1的,即
随后我们再来分析一下2是为何变成不了1的,即
可以看到,最后整个生成快乐数的过程形成了一个环,因此我们可以使用快慢指针判断链表是否有环的思想来解决这道题(参考:OR36链表的回文结构),代码如下
class Solution {
public:
int transform(int n)
{
int ret = 0;
while (n)
{
int num = (n % 10) * (n % 10);
ret += num;
n /= 10;
}
return ret;
}
bool isHappy(int n)
{
int slow = transform(n);
int fast = transform(transform(n));
while (slow != fast)
{
slow = transform(slow);
fast = transform(transform(fast));
}
if (fast == 1) return true;
else return false;
}
};
对于这道题来说,我们应该还有一个疑问,为什么不会到达1的时候一定会形成一个环呢?因为int类型最大值为为2 147 483 647, 所以平方和最大的数是1 999 999 999,平方和为1 + 81*9 = 724。任何数的平方和都在1到724之间,724次循环之内一定有重复的,即一定会进入循环区间,从而形成环。
4. 盛多最水的容器
题目链接:11. 盛最多水的容器 - 力扣(LeetCode)
解析:分析一下题意我们可以发现,我们可以定义左右指针,分别从两端向中间遍历,哪一端低就像反方向移动一位(若左边低则left++,右边低则right--),每一次将本次计算出的面积与上一次相比较,直到遍历到最后得到最终结果,代码如下
class Solution
{
public:
int maxArea(vector<int>& h)
{
int left = 0;
int right = h.size()-1;
int max = 0;
while (left < right)
{
if (h[left] <= h[right])
{
int tmp = h[left] * (right - left);
if (tmp > max) max = tmp;
left++;
}
else
{
int tmp = h[right] * (right - left);
if (tmp > max) max = tmp;
right--;
}
}
return max;
}
};
5. 有效三角形的个数
题目链接:611. 有效三角形的个数 - 力扣(LeetCode)
解析:要想判断三个数是否能组成三角形,我们知道需要满足以下条件:
1. 任意两边之和大于第三边
2. 任意两边之差小于第三边
那么我们先将数组排序,可以固定一条边(此边最小),将这个数右边的数作为一个区间,借助双指针来判断这三个数是否能够组成三角形,代码如下
class Solution {
public:
int triangleNumber(vector<int>& nums)
{
sort(nums.begin(), nums.end());
int count = 0;
int n = nums.size();
for (int i = n-1; i >= 2; i--)
{
int left = 0;
int right = i-1;
while (left < right)
{
if (nums[left] + nums[right] > nums[i])
{
count += right - left;
right--;
}
else
{
left++;
}
}
}
return count;
}
};
6. 和为s的两个数
题目链接:LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)
解析:分析题目后,我们可以使用双指针从数组的两端向中间寻找,由于数组是升序的,因此当结果 < target时,left++即可,当结果> target时,right--即可,代码如下
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target)
{
int n = price.size();
int left = 0, right = n-1;
while (left < right)
{
if (price[left] + price[right] < target) left++;
else if (price[left] + price[right] > target) right--;
else break;
}
return {price[left], price[right]};
}
};
7. 三数之和
解析:这道题要求找到三数之和为0的三元组,那么我们可以先将数组排序,每次固定一个最小的数(最左端),在它右边的剩余区间中借助双指针找到符合nums[left] + nums[ight] == nums[min]的数,代码如下
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
// 借助set去重
set<vector<int>> res;
sort(nums.begin(), nums.end());
int n = nums.size();
for (int i = 0; i < n-2; i++)
{
int left = i+1;
int right = n-1;
while (left < right)
{
if (nums[left] + nums[right] < -nums[i]) left++;
else if (nums[left] + nums[right] > -nums[i]) right--;
else
{
vector<int> tmp = {nums[i], nums[left++], nums[right--]};
res.insert(tmp);
}
}
}
vector<vector<int>> ret;
for (auto& e : res) ret.push_back(e);
return ret;
}
};
8. 四数之和
解析:对于四数之和我们可以采取和三数之和类似的处理方法,即先将数组排序,固定一个最小的数(最左端数),在剩下的区间内再去固定另一个最小值,再借助双指针寻找并判断四数是否满足对应关系,代码如下
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target)
{
sort(nums.begin(), nums.end());
set<vector<int>> res;
int n = nums.size();
for (int i = 0; i < n - 3; i++)
{
for (int j = i+1; j < n-2; j++)
{
int left = j+1;
int right = n-1;
while (left < right)
{
int a = nums[i];
int b = nums[j];
int c = nums[left];
int d = nums[right];
long long t_left = nums[left] + nums[right];
long long t_right = target;
t_right -= nums[i];
t_right -= nums[j];
if (t_left < t_right) left++;
else if (t_left > t_right) right--;
else
{
vector<int> tmp = {a,b,c,d};
res.insert(tmp);
left++;
right--;
}
}
}
}
vector<vector<int>> ret;
for (auto& e : res) ret.push_back(e);
return ret;
}
};