算法修炼 chapter 2 双指针进阶
摘要
本章学习双指针技法在容器盛水、三角形判断、两数/三数/四数之和等问题中的应用,涵盖5道经典题。
题目 来源 核心考点 11. 盛最多水的容器 LeetCode 对撞指针求最大面积 611. 有效三角形的个数 LeetCode 排序 + 对撞指针计数 LCR 179. 查找总价格为目标值的两个商品 LeetCode 有序数组两数之和 15. 三数之和 LeetCode 排序 + 对撞指针去重 18. 四数之和 LeetCode 三数之和扩展,注意 long long 防溢出 关键技法:
- 对撞指针:两端向中间逼近,每次收缩较小(或较大)的一侧
- 排序预处理:有序是双指针的前提,也是去重的基础
- 去重:找到一组解后,跳过所有相邻重复值,避免结果重复
易错点:
if/else多行语句必须加花括号,否则只有第一行受条件控制- 变量名不要用
max,与std::max冲突,可能编译报错- 四数之和中三数之和的 aim 要用
long long,防止nums[i]+nums[j]+target溢出- 去重时注意比较的是同侧的前驱元素:
nums[right] == nums[right+1],不是nums[left+1]- 三数之和/四数之和固定第一个数后,若该数 > 0 可直接提前终止(全正数不可能凑出 0)
1. LeetCode 11 --- 盛最多水的容器
题目链接: 力扣链接
题解
cpp
class Solution {
public:
int maxArea(vector<int>& h) {
int left = 0;
int right = h.size() - 1;
int retMax = 0;
while (left < right) {
if (h[left] < h[right]) {
int ret = (right - left) * h[left++];
if (ret > retMax) retMax = ret;
} else {
int ret = (right - left) * h[right--];
if (ret > retMax) retMax = ret;
}
}
return retMax;
}
};
解题思路
暴力枚举
枚举出所有能构成的容器,找出其中的最大值。设两指针 i、j 分别指向水槽板的最左端和最右端,此时容器的宽度为 j - i。由于容器的高度由两板中的短板决定,因此可得容积公式:
V = (j - i) * min(height[i], height[j])。暴力法时间复杂度 O(n²),会超时。
对撞指针设两个指针 left、right 分别指向容器的左右两个端点,此时容器的容积:
V = (right - left) * min(height[right], height[left])。容器的左边界为height[left],右边界为height[right]。用ret记录这个容积,与retMax(初始值为 0)作比较,如果ret > retMax,则retMax = ret。然后移动较短的那一边向中间靠拢:如果左边较短,
left++;如果右边较短,right--。因为宽度一定在减小,只有移动到更高的板才有可能获得更大的面积。继续这个过程直到两指针相遇,返回retMax。
2. LeetCode 611 --- 有效三角形的个数
题目链接: 力扣链接
题解
cpp
class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
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]) {
// [left, right-1] 中的所有元素与 nums[right] 都满足三角不等式
ret += right - left;
right--;
} else {
left++;
}
}
}
return ret;
}
};
解题思路
- 根据「解法一」中的优化思想,我们可以固定一个「最长边」,然后在比这条边小的有序数组中找出一个二元组,使这个二元组之和大于这条最长边。由于数组已排序,可以利用「对撞指针」来优化。
- 设最长边枚举到 i 位置,区间
[left, right]是 i 位置左边的区间(也就是比它小的区间):- 如果
nums[left] + nums[right] > nums[i]:说明[left, right-1]区间上的所有元素均可以与nums[right]构成满足条件的二元组,满足条件的有right - left种。此时right位置的所有情况已考虑完毕,right--,进入下一轮判断。 - 如果
nums[left] + nums[right] <= nums[i]:说明left位置的元素不可能与[left+1, right]位置上的元素构成满足条件的二元组(因为nums[left]已经是当前最小的了),left++进入下一轮循环。
- 如果
3. LeetCode LCR 179 --- 查找总价格为目标值的两个商品
题目链接: 力扣链接
题解
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& p, int target) {
int left = 0;
int right = p.size() - 1;
while (left < right) {
int sum = p[left] + p[right];
if (sum > target)
right--;
else if (sum < target)
left++;
else
break;
}
return {p[left], p[right]};
}
};
解题思路
- 题目已保证数组有序(单调递增)。设左右两个指针 left、right,用
p[left] + p[right]和 target 比较:- 如果和大于 target,说明右边的数太大,
right--缩小右边界; - 如果和小于 target,说明左边的数太小,
left++扩大左边界; - 如果相等,直接跳出循环。
- 如果和大于 target,说明右边的数太大,
- 返回
{p[left], p[right]}即可。
注意 :本题数组已有序,无需额外排序。如果输入无序,需先
sort再使用对撞指针。
4. LeetCode 15 --- 三数之和
题目链接: 力扣链接
题解
cpp
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;) {
if (nums[i] > 0) break; // 最小的数已经大于0,不可能有三数之和为0
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
while (left < right && nums[left] == nums[left - 1]) left++;
// 跳过重复的 right
while (left < right && nums[right] == nums[right + 1]) right--;
}
}
i++;
while (i < n && nums[i] == nums[i - 1]) i++; // 跳过重复的 i
}
return ret;
}
};
解题思路
- 为了提高效率,先给数组从小到大排序。与两数之和不同的是,题目要求找到所有「不重复」的三元组。
- 固定一个数
a = nums[i],问题转化为在i+1 ~ n-1区间内找两数之和等于-a,利用对撞指针实现。 - 去重关键 :
- 固定 i 时,如果
nums[i] == nums[i-1]则跳过,避免重复三元组。 - 找到一组解后,left 和 right 都需要跳过所有相邻重复值。
- 固定 i 时,如果
- 如果
nums[i] > 0,由于数组已排序,后续所有数都为正,三数之和不可能为 0,直接break。
5. LeetCode 18 --- 四数之和
题目链接: 力扣链接
题解
cpp
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;) {
for (int j = i + 1; j < n;) {
int left = j + 1, right = n - 1;
long long aim = (long long)target - nums[i] - nums[j];
while (left < right) {
long long sum = (long long)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--]});
// 跳过重复的 left
while (left < right && nums[left] == nums[left - 1]) left++;
// 跳过重复的 right
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;
}
};
解题思路
- 与三数之和的思想类似,固定左边的两个数
nums[i]和nums[j],剩下的用对撞指针找两数之和等于target - nums[i] - nums[j]。 - 注意
long long防溢出 :aim和sum都使用long long,防止nums[i] + nums[j] + target或两数相加时整型溢出。 - 去重逻辑与三数之和完全一致,三层循环每一层都要跳过相邻重复值。
- 四数之和的时间复杂度为 O(n³),空间复杂度 O(1)(不计返回值)。
任何的批评和建议欢迎指出,我们共同进步!