—— 从快慢指针到对撞指针,刷题效率提升 200%!
常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。
⼀般⽤于顺序结构中,也称左右指针。
对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
left == right (两个指针指向同⼀个位置)
left > right (两个指针错开)
又称为龟兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
• 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。
「数组分两块」是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部分。这种类型的题,⼀般就是使⽤「双指针」来解决。
题⽬链接:
283. 移动零
要点:
老师代码:
class Solution
{
public:
void moveZeroes(vector& nums)
{
for(int cur = 0, dest = -1; cur < nums.size(); cur++)
if(nums[cur]) // 处理⾮零元素
swap(nums[++dest], nums[cur]);
}
}
老师思路:
我的代码:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int dest = -1;//指向已经移动完成的区域的最后一个位置
int cur = 0;//指向当前需要操作的位置
while(cur < nums.size())
{
if(nums[cur] == 0)
{
cur++;
}
else
{
std::swap(nums[dest + 1], nums[cur]);
dest++;
cur++;
}
}
}
};
我的思路:
使用 dest
指针标记已处理非零元素的末尾,cur
指针遍历数组。当 cur
遇到非零元素时,与 dest+1
位置交换,dest
后移。此操作保证 [0, dest]
始终为非零元素,时间复杂度 O(n)。
我的笔记:
dest
初始化为 -1,巧妙处理初始边界
交换操作后只需 dest++
,无需重复检查已处理区域
对比老师的代码,发现 for
循环比 while
更简洁,但逻辑等价
题⽬链接:
复写零
要点:
老师代码:
class Solution
{
public:
void duplicateZeros(vector<int>& arr)
{
// 1. 先找到最后⼀个数
int cur = 0, dest = -1, n = arr.size();
while(cur < n)
{
if(arr[cur]) dest++;
else dest += 2;
if(dest >= n - 1) break;
cur++;
}
// 2. 处理⼀下边界情况
if(dest == n)
{
arr[n - 1] = 0;
cur--; dest -=2;
}
// 3. 从后向前完成复写操作
while(cur >= 0)
{
if(arr[cur]) arr[dest--] = arr[cur--];
else
{
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
}
老师思路:
如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」。因此我们选择「从后往前」的复写策略。但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两步:i. 先找到最后⼀个复写的数;ii. 然后从后向前进⾏复写操作
我的代码:
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int dest = -1, cur = 0, n = arr.size();
while(cur < n)//用快慢双指针来找到最后一个可以复写到的元素,标记为cur
{
if(arr[cur]) dest++;
else dest +=2;
if(dest >= n - 1) break;
cur++;
}
if(dest == n)//如果dest出现越界,则cur指向的位置一定是0
{
arr[n - 1] = 0;//复写后的最后一个数字为0
dest -= 2;//回退到n-2
cur--;//回退一格
}
while(cur >= 0)//从后往前遍历复写
{
if(arr[cur]) arr[dest--] = arr[cur--];
else
{
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
};
我的思路:
dest
越界,需手动处理末尾零我的笔记:
dest
的移动规则:遇到非零+1,遇到零+2cur
指针的起始位置需通过模拟填充确定dest == n
表示最后一个有效元素是零且导致越界题⽬链接:
快乐数
要点:
老师代码:
class Solution
{
public:
int bitSum(int n) // 返回 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;
}
}
老师思路:
为了⽅便叙述,将「对于⼀个正整数,每⼀次将该数替换为它每个位置上的数字的平⽅和」这⼀个操作记为 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 的话,那么就不是快乐数
我的代码:
class Solution {
public:
int fun(int x)
{
int a = 1, sum = 0;
while(x / a >= 10)
{
sum += (x % (a * 10) / a) * (x % (a * 10) / a);
a *= 10;
}
sum += (x / a) * (x / a);
return sum;
}
bool isHappy(int n) {
int fast = n, slow = n;
do
{
fast = fun(fast);
fast = fun(fast);
slow = fun(slow);
}while(fast != slow);
if(fast == 1) return true;
else return false;
}
};
我的思路:
fast
和 slow
指针,fast
每次计算两次平方和,slow
一次我的笔记:
要学会老师取一个整数的每一位数的方法
简化代码
当提到无限循环的时候就要想到能不能用快慢双指针的方法解决问题,就像链表成环问题一样
平方和函数可用取余法优化:while(n) { t = n%10; sum += t*t; n /=10; }
快慢指针相遇后只需判断是否为1,无需继续计算
时间复杂度从 O(n) 降至 O(logn)
题⽬链接:
盛水最多的容器
要点:
min(height[left], height[right]) * (right - left)
老师代码:
class Solution
{
public:
int maxArea(vector<int>& height)
{
int left = 0, right = height.size() - 1, ret = 0;
while(left < right)
{
int v = min(height[left], height[right]) * (right - left);
ret = max(ret, v);
// 移动指针
if(height[left] < height[right]) left++;
else right--;
}
return ret;
}
}
老师思路:
我的代码:
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int max = 0, maxi = 0;
while(left < right)
{
maxi = (right - left) * (height[left] < height[right] ? height[left] : height[right]);
max = max > maxi ? max : maxi;
if(height[left] < height[right])
{
left++;
}
else
{
right--;
}
}
return max;
}
};
我的思路:
从两端往中间靠拢的过程中,长度一直在减少,因此要想找到比现在还大的容积只有当高度比现在指针指向的高度还要高的情况才有可能出现,因此才有了上面的判断条件
我的笔记:
题⽬链接:
有效三角形个数
要点:
老师代码:
class Solution
{
public:
int triangleNumber(vector<int>& nums)
{
// 1. 优化
sort(nums.begin(), nums.end());
// 2. 利⽤双指针解决问题
int ret = 0, n = nums.size();
for(int i = n - 1; i >= 2; i--) // 先固定最⼤的数
{
// 利⽤双指针快速统计符合要求的三元组的个数
int left = 0, right = i - 1;
while(left < right)
{
if(nums[left] + nums[right] > nums[i])
{
ret += right - left;
right--;
}
else
{
left++;
}
}
}
return ret;
}
};
老师思路:
我的代码:
class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
int num = 0;
int hight = nums.size() - 1, left = 0, right = hight - 1;
while(hight >= 2)
{
while(left < right)
{
if(nums[left] + nums[right] > nums[hight])
{
num += right - left;
right--;
}
else
{
left++;
}
}
hight--;
left = 0, right = hight - 1;
}
return num;
}
};
我的思路:
nums[i]
left=0
和 right=i-1
搜索满足 nums[left] + nums[right] > nums[i]
的组合right-left
个组合均有效我的笔记:
if (sum > target) ret += right - left;
题⽬链接:
和为 s 的两个数字
要点:
老师代码:
class Solution
{
public:
vector<int> twoSum(vector<int>& nums, int target)
{
int left = 0, right = nums.size() - 1;
while(left < right)
{
int sum = nums[left] + nums[right];
if(sum > target) right--;
else if(sum < target) left++;
else return {nums[left], nums[right]};
}
// 照顾编译器
return {-4941, -1};
}
}
我的代码:
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
//vector nums = {0, 0};
int left = 0, right = price.size() - 1;
while(left < right)
{
if(price[left] + price[right] == target)
{
//nums[0] = price[left], nums[1] = price[right];
return {price[left], price[right]};
}
else if(price[left] + price[right] > target)
{
right--;
}
else
{
left++;
}
}
//return nums;
return {0, 0};
}
};
我的思路:
我的笔记:
题⽬链接:
三数之和
要点:
老师代码:
class Solution
{
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
vector<vector<int>> ret;
// 1. 排序
sort(nums.begin(), nums.end());
// 2. 利⽤双指针解决问题
int n = nums.size();
for(int i = 0; i < n; ) // 固定数 a
{
if(nums[i] > 0) break; // ⼩优化
int left = i + 1, right = n - 1, target = -nums[i];
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 和 right
while(left < right && nums[left] == nums[left - 1]) left++;
while(left < right && nums[right] == nums[right + 1]) right--;
}
}
// 去重 i
i++;
while(i < n && nums[i] == nums[i - 1]) i++;
}
return ret;
}
};
我的代码:
错误一
#include
class Solution {
private:
vector<vector<int>> arr;
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
int heigh = nums.size() - 1, left = 0, right = heigh - 1;
while(heigh >= 2)
{
while(left < right)
{
if(nums[left] + nums[right] + nums[heigh] > 0)
{
while(right - 1 > 0 && nums[right - 1] == nums[right])
{
right--;
}
right--;
}
else if(nums[left] + nums[right] + nums[heigh] < 0)
{
while(left + 1 < heigh && nums[left + 1] == nums[left])
{
left++;
}
left++;
}
else
{
arr.push_back({nums[left], nums[right], nums[heigh]});
left++, right--;
}
}
left = 0, right = heigh - 1;
while(heigh >= 2 && nums[heigh - 1] == nums[heigh])
heigh--;
heigh--;
}
return arr;
}
};
错误二:
class Solution {
private:
vector<vector<int>> arr;
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
for(int a = 0; a < nums.size();)
{
int left = a + 1, right = nums.size() - 1;
while(left < right)
{
if(nums[left] + nums[right] + nums[a] == 0)
{
arr.push_back({nums[a], nums[left], nums[right]});
left++;//这里同样需要判断是否越界,这是错误原因
right--;
}
else if(nums[left] + nums[right] + nums[a] > 0)
{
right--;
while(left < right && nums[right + 1] == nums[right]) right--;//这段代码写到上面批注部分
}
else
{
left++;
while(left < right && nums[left - 1] == nums[left]) left++;//这段代码也写到上面批注部分
}
}
a++;
while(a < nums.size() && nums[a - 1] == nums[a]) a++;
}
return arr;
}
};
我的思路:
要点:
算法思:双指针思想
先排序
然后固定⼀个数 a
在这个数后⾯的区间内,使⽤「双指针算法」快速找到两个数之和等于 -a 即可 (这一条只要满足题意都可以)
注意:
找到⼀个结果之后, left 和 right 指针要「跳过重复」的元素;
使⽤完⼀次双指针算法之后,固定的 a 也要「跳过重复」的元素
越界判断
固定第一个数 nums[i]
,转化为两数之和问题
双指针 left=i+1
和 right=n-1
搜索 target = -nums[i]
去重操作需在找到解后立即执行
我的笔记:
我提交的代码是有一些无法通过的,其实就是边界问题没有处理好,如:
while(left < right && nums[right - 1] == nums[right])//这里是吧现在这个nums[right],与数组的下一个nums[right - 1]对比
{
right--;
}
right--;//如果使用这一种方法,这一条代码执行后就不能判断right是否越界了
将这一串代码的判断条件换一个方法:
right--;
while(left < right && nums[right + 1] == nums[right])//这里是吧现在这个nums[right],与数组的上一个nums[right + 1]对比
{
right--;
}
就可以完美解决无法判断后面right–;是否越界的情况
注意去重与越界访问问题,以后写代码都要记得考虑一下这个问题
去重代码模板:
while (i < n && nums[i] == nums[i-1]) i++; // 外层去重
while (left < right && nums[left] == nums[left-1]) left++; // 内层去重
边界检查 left < right
必须放在条件首位,防止越界
题⽬链接:
四数之和
要点:
long long
防止整数溢出老师代码:
class Solution
{
public:
vector<vector<int>> fourSum(vector<int>& nums, int target)
{
vector<vector<int>> ret;
// 1. 排序
sort(nums.begin(), nums.end());
// 2. 利⽤双指针解决问题
int n = nums.size();
for(int i = 0; i < n; ) // 固定数 a
{
// 利⽤ 三数之和
for(int j = i + 1; j < n; ) // 固定数 b
{
// 双指针
int left = j + 1, right = n - 1;
long long aim = (long long)target - nums[i] - nums[j];
while(left < right)
{
int sum = nums[left] + nums[right];
if(sum < aim) left++;
else if(sum > aim) right--;
else
{
ret.push_back({nums[i], nums[j], nums[left++],
nums[right--]});
// 去重⼀
while(left < right && nums[left] == nums[left - 1])
left++;
while(left < right && nums[right] == nums[right + 1])
right--;
}
}
// 去重⼆
j++;
while(j < n && nums[j] == nums[j - 1]) j++;
}
// 去重三
i++;
while(i < n && nums[i] == nums[i - 1]) i++;
}
return ret;
}
};
我的代码:
class Solution {
private:
vector<vector<int>> arr;
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size();)
{
for(int j = i + 1; j < nums.size();)
{
int left = j + 1, right = nums.size() - 1;
while(left < right)
{
long long num = (long long)nums[i] + nums[j] + nums[left] + nums[right];
if(num == target)
{
arr.push_back({nums[i], nums[j], nums[left], nums[right]});
left++, right--;
//去重一
while(left < right && nums[left] == nums[left - 1]) left++;
while(left < right && nums[right] == nums[right + 1]) right--;
}
else if(num > target) right--;
else left++;
}
//去重二
j++;
while(j <nums.size() && nums[j] == nums[j - 1]) j++;
}
//去重三
i++;
while(i < nums.size() && nums[i] == nums[i - 1]) i++;
}
return arr;
}
};
我的思路:
nums[i]
和 nums[j]
left=j+1
和 right=n-1
搜索剩余两数
我的笔记:
long long aim = (long long)target - nums[i] - nums[j];