须知
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力! 👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力! 🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++算法感兴趣的朋友,让我们一起进步!
接上篇:【优选算法篇】两人赛跑解难题:双指针算法的妙用(上篇)-CSDN博客
引言:通过上篇文章带大家简单了解“双指针算法”,小试牛刀。接下来将让大家感受一下双指针在解题的妙处。
在算法面试中,双指针是一种非常高效的技巧,能够帮助我们在许多问题中快速定位解。它不仅能大大降低时间复杂度,还能优化空间使用,提升整体程序性能。C++ 作为一种具有高效内存管理和指针操作能力的语言,使得双指针算法得以在众多场景下应用得淋漓尽致。本文将介绍 C++ 双指针算法的进阶技巧,并通过经典问题讲解如何巧妙地运用这一技术。
双指针算法的基本思路是使用两个指针(或索引)同时遍历数据,常常用于有序数组或链表等数据结构中。两个指针可以向相同方向移动,也可以相向而行。通过调整指针的位置,我们能够在复杂度上进行优化。
题目链接:11. 盛最多水的容器 - 力扣(LeetCode)
题目描述:
补充:
采用双指针法,从数组的两端开始,逐步向中间靠拢,通过调整指针位置,找到可以容纳最多水的两个柱子。
思考过程
left
和 right
。height[left] < height[right]
,左指针右移(left++
)。right--
)。left
和 right
相遇时,遍历结束。关键点
输入:
height = [1,8,6,2,5,4,8,3,7]
输出:
49
步骤:
left = 0
, right = 8
,容量: volume=min(1,7)×(8−0)=7×8=49left = 1
,此时 height[left] = 8
。height[left]
和 height[right]
,更新 max_volume
,直至 left == right
。49
。class Solution {
public:
int maxArea(vector<int>& height) {
// 初始化双指针,一个指向数组最左边(left),一个指向数组最右边(right)
// 初始化变量 ret 用于存储最大容积,初始值为 0
int left = 0, right = height.size() - 1, ret = 0;
// 当左右指针未相遇时,继续遍历
while (left < right) {
// 计算当前容器的容量,取两根柱子中较短的一根作为高度
// 容积公式为:短板高度 * 两指针之间的距离
ret = max(ret, min(height[left], height[right]) * (right - left));
// 根据较短的一根柱子移动指针,尝试寻找更大的容量
if (height[left] < height[right]) {
// 如果左边的柱子较短,移动左指针
left++;
} else {
// 如果右边的柱子较短,移动右指针
right--;
}
}
// 返回最终找到的最大容积
return ret;
}
};
双指针通过从两端向中间收缩并动态调整计算条件,避免了暴力解法的多次重复计算,能够在最短时间内找到结果。这种思路同样适用于很多类似的优化问题,比如“接雨水”、“最小覆盖子串”等,掌握它会让你的算法能力更上一层楼。
题目链接:611. 有效三角形的个数 - 力扣(LeetCode)
题目描述:
将数组 nums
按升序排序,使得对于任意 i<j<k,我们有:nums[i]≤nums[j]≤nums[k]
这样只需验证 nums[i]+nums[j]>nums[k],即可保证满足三角形条件。
对于每个 k(即三角形中最长的边),尝试找到符合条件的 i , j:
class Solution {
public:
int triangleNumber(vector<int>& nums) {
// 对数组进行排序
sort(nums.begin(), nums.end());
int count = 0;
// 遍历每个可能的最长边
for (int k = nums.size() - 1; k >= 2; --k) {
int i = 0, j = k - 1;
// 双指针查找符合条件的边
while (i < j) {
if (nums[i] + nums[j] > nums[k]) {
// 符合条件的三角形个数
count += (j - i);
--j; // 移动右指针
} else {
++i; // 移动左指针
}
}
}
return count;
}
};
该算法通过排序和双指针的结合,有效地减少了重复计算,是解决三角形个数问题的经典方法。排序后的双指针遍历不仅逻辑简单,而且复杂度低,非常适合处理大规模数据。
题目链接:LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)
题目描述:
4.1 算法思路:
具体步骤:
left
指向数组的开始,right
指向数组的末尾。price[left] + price[right]
。target
,说明左指针的元素过小,可以向右移动左指针(left++
)。target
,说明右指针的元素过大,可以向左移动右指针(right--
)。left
指针与 right
指针重合时,说明没有找到符合条件的商品,返回 {-1, -1}
。class Solution
{
public:
vector<int> twoSum(vector<int>& price, int target)
{
vector<int> ret;
// 双指针算法
int left = 0, right = price.size() - 1;
while (left < right) {
int sum = price[left] + price[right]; // 计算当前两个指针所指向元素的和
if (sum < target) {
left++; // 如果和小于目标值,左指针右移
} else if (sum > target) {
right--; // 如果和大于目标值,右指针左移
} else {
return {price[left], price[right]}; // 如果找到目标和,返回结果
}
}
return {-1, -1}; // 如果没有找到符合条件的商品,返回{-1, -1}
}
};
n
是数组 price
的长度。双指针法适合于在已排序的数组中查找满足特定条件的元素对。它利用了数组的顺序性,通过调整指针来高效地减少计算量,从而在 O(n) 时间内找到目标结果。对于本题,双指针法是一个非常高效的解决方案。
题目描述:
这个问题可以通过排序 + 双指针法来高效解决,时间复杂度为 O(n^2),因为每个元素都会遍历一次,同时使用双指针来寻找满足条件的两数。
nums
进行排序,以便可以利用双指针法。排序后的数组有助于后续检查重复的三元组,避免重复计算。nums[i]
,然后使用双指针法在 nums[i+1]
到 nums[n-1]
的区间内寻找其他两个数,使得这三个数之和为零。left
和 right
,分别指向当前区间的两端。根据 nums[i]
和 nums[left] + nums[right]
的和与零的关系来决定如何移动指针: nums[left] + nums[right] > -nums[i]
,则将 right
向左移动,减小和。nums[left] + nums[right] < -nums[i]
,则将 left
向右移动,增加和。nums[left] + nums[right] == -nums[i]
,则找到一个三元组,将其加入结果,并移动两个指针,跳过相同的元素以避免重复三元组。nums[i]
后,若 nums[i]
和前一个元素相同,则跳过当前元素,避免重复。nums[left]
与 nums[left-1]
相同,或者 nums[right]
与 nums[right+1]
相同,跳过这些元素以避免重复。ret
向量中,返回该向量。class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ret;
sort(nums.begin(), nums.end()); // 先对数组进行排序
int n = nums.size();
for (int i = 0; i < n - 2;) { // 固定一个数
int left = i + 1, right = n - 1;
// 双指针寻找其余两个数
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--; // 总和大于0,右指针左移
} else if (sum < 0) {
left++; // 总和小于0,左指针右移
} else {
// 找到一个和为0的三元组
ret.push_back({nums[i], nums[left], nums[right]});
left++;
right--;
// 跳过重复的元素
while (left < right && nums[left] == nums[left - 1]) left++;
while (left < right && nums[right] == nums[right + 1]) right--;
}
}
// 跳过重复的元素
i++;
while (i < n && nums[i] == nums[i - 1]) i++;
}
return ret;
}
};
n
是数组 nums
的长度。n
次,而内层双指针部分最多会执行 n
次。最终的总时间复杂度是 O(n^2)。ret
,我们只使用了常量空间来存储指针和临时变量。因此空间复杂度是 O(1),不过需要注意返回的结果所占的空间是与结果大小相关的。题目描述:
为了高效解决此问题,我们可以利用排序和双指针法来避免暴力解法的高时间复杂度。
nums
进行排序。这有助于后续使用双指针法,并且排序后的数组有助于跳过重复的元素,避免重复的四元组。nums[i]
和 nums[j]
,接下来使用双指针法来找出剩下两个数使得四个数的和等于 target
。nums[i]
和 nums[j]
,在剩下的元素中使用双指针法来寻找另外两个数 nums[left]
和 nums[right]
,使得四个数的和为 target
。sum = nums[i] + nums[j] + nums[left] + nums[right]
。sum
小于 target
,则需要增大 sum
,所以将左指针 left
向右移动;如果 sum
大于 target
,则需要减小 sum
,所以将右指针 right
向左移动。sum
等于 target
,则找到一个符合条件的四元组,添加到结果列表 ret
中,并且移动指针以跳过重复的元素。left
和右指针 right
指向相同的元素时,也应该跳过,以避免重复。class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ret;
sort(nums.begin(), nums.end()); // 对数组进行排序
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;
while (left < right) {
long long sum = (long long)nums[i] + nums[j]; // 当前两个数的和
long long t = (long long)target - nums[left] - nums[right]; // 剩余的目标和
if (sum > t) {
right--; // 总和大于目标值,右指针左移
} else if (sum < t) {
left++; // 总和小于目标值,左指针右移
} else {
ret.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--;
}
}
j++;
while (j < n && nums[j] == nums[j - 1]) j++; // 跳过重复的元素
}
i++;
while (i < n && nums[i] == nums[i - 1]) i++; // 跳过重复的元素
}
return ret;
}
};
n
是数组 nums
的长度。n
次。i
和 j
,在最坏情况下,双指针遍历剩余部分的时间复杂度是 O(n)。因此,双指针的总时间复杂度是 O(n^2)。ret
,我们只使用了常数空间来存储指针和临时变量。因此,空间复杂度为 O(1),但结果数组的空间占用与结果大小有关。通过上述「盛最多水的容器」、「有效三角形个数」、「查找目标值的两个商品」、「三数和」以及「四数和」的例子,可以总结出双指针算法的核心思想、应用场景和优化技巧。双指针算法高效、灵活,特别适合处理有序数组中的查找、优化与统计问题。通过上述例子,我们可以看到双指针的优势在于减少搜索空间,同时通过排序和跳过重复优化结果。掌握双指针方法是解决数组与组合类问题的利器。
路虽远,行则将至;事虽难,做则必成
亲爱的读者们,下一篇文章再会!!!