【算法】专题一:双指针之呈最多水的容器,有效三角型的个数,和为 s 的两个数字,三数之和,四数之和
1.盛最多水的容器:
题⽬链接:11. 盛最多水的容器 - 力扣(LeetCode)
问题描述:
算法原理:
解法⼀(暴⼒求解):
算法思路:
枚举出能构成的所有容器,找出其中容积最⼤的值。 容器容积的计算⽅式:设两指针 i , j ,分别指向⽔槽板的最左端以及最右端,此时容器的宽度为 j - i 。由于容器的⾼度由两板中的短板决定,因此可得容积公式 : v = (j - i) * min(height[i], height[j])
class Solution { public: int maxArea(vector<int>& height) { int n = height.size(); int ret = 0; // 两层 for 枚举出所有可能出现的情况 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { // 计算容积,找出最⼤的那⼀个 ret = max(ret, min(height[i], height[j]) * (j - i)); } } return ret; } };这种方法缺点就是会超时,走不过去,但解法没有错,而且这是一个中档题出题人也不想让你就简简单单的暴力枚举就做出来的。
解法⼆(对撞指针):
算法思路:
设两个指针 left , right 分别指向容器的左右两个端点,此时容器的容积 :
v = (right - left) * min( height[right], height[left])容器的左边界为 height[left] ,右边界为 height[right] 。为了⽅便叙述,我们假设左边边界⼩于右边边界。
如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
- 容器的宽度⼀定变⼩。
- 由于左边界较⼩,决定了⽔的⾼度。如果改变左边界,新的⽔⾯⾼度不确定,但是⼀定不会超过右边的柱⼦⾼度,因此容器的容积可能会增⼤。
- 如果改变右边界,⽆论右边界移动到哪⾥,新的⽔⾯的⾼度⼀定不会超过左边界,也就是不会超过现在的⽔⾯⾼度,但是由于容器的宽度减⼩,因此容器的容积⼀定会变⼩的。
由此可⻅,左边界和其余边界的组合情况都可以舍去。所以我们可以 left++ 跳过这个边界,继续去判断下⼀个左右边界。当我们不断重复上述过程,每次都可以舍去⼤量不必要的枚举过程,直到 left 与 right 相遇。期间产⽣的所有的容积⾥⾯的最⼤值,就是最终答案。
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; } };过程展现:
这道题的时间复杂度是O(n^2)
问题补充:
max(v)为什么不行?
max(v)这种写法在 C++ 中不合法,因为max是一个函数(在<algorithm>中),需要至少两个参数来比较。好比max(3, 5)会返回 5,但max(5)编译报错,因为它不知道和谁比较可以不用
ret而直接取所有v的最大值吗?可以,但必须事先存储所有
v:但这会浪费 O(n) 额外空间,且没必要。
2.有效三角形的个数:
题⽬链接:611. 有效三角形的个数 - 力扣(LeetCode)
问题描述:
算法原理:
解法⼀(暴⼒求解)(会超时):
算法思路:
三层 for 循环枚举出所有的三元组,并且判断是否能构成三⻆形。
虽然说是暴⼒求解,但是还是想优化⼀下:
判断三⻆形的优化:
- 如果能构成三⻆形,需要满⾜任意两边之和要⼤于第三边。但是实际上只需让较⼩的两条边之和⼤于第三边即可。
- 因此我们可以先将原数组排序,然后从⼩到⼤枚举三元组,⼀⽅⾯省去枚举的数量,另⼀⽅⾯⽅便判断是否能构成三⻆形
class Solution { public: int triangleNumber(vector<int>& nums) { // 1. 排序 sort(nums.begin(), nums.end()); int n = nums.size(), ret = 0; // 2. 从⼩到⼤枚举所有的三元组 for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { for (int k = j + 1; k < n; k++) { // 当最⼩的两个边之和⼤于第三边的时候,统计答案 if (nums[i] + nums[j] > nums[k]) ret++; } } } return ret; } };解法⼆(排序 + 双指针)
用单调性,使用双指针算法来解决问题
先固定最大的数
在最大的数的左区间内,使用双指针算法,快速统计出符合要求的三元组的个数
算法思路:
先将数组排序。
根据解法⼀中的优化思想,我们可以固定⼀个最⻓边,然后在⽐这条边⼩的有序数组中找出⼀个⼆元组,使这个⼆元组之和⼤于这个最⻓边。由于数组是有序的,我们可以利⽤对撞指针来优化。设最⻓边枚举到 i 位置,区间 [left, right] 是 i 位置左边的区间(也就是⽐它⼩的区
间):1.如果 nums[left] + nums[right] > nums[i] :
- 说明 [left, right - 1] 区间上的所有元素均可以与 nums[right] 构成⽐nums[i] ⼤的⼆元组满⾜条件的有 right - left 种
- 此时 right 位置的元素的所有情况相当于全部考虑完毕, right-- ,进⼊下⼀轮判断
2.如果 nums[left] + nums[right] <= nums[i]:
- 说明 left 位置的元素是不可能与 [left + 1, right] 位置上的元素构成满⾜条件的⼆元组
- left 位置的元素可以舍去, left++ 进⼊下轮循环
class Solution { public: int triangleNumber(vector<int>& nums) { //优化 sort(nums.begin(),nums.end()); //利用双指针解决问题 int ret = 0; int 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; } };手写展现:
过程展现:
3.和为 s 的两个数字
题⽬链接:
LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)
问题描述:
算法原理:
解法⼀(暴⼒解法,会超时):
算法思路:
两层 for 循环列出所有两个数字的组合,判断是否等于⽬标值。
算法流程:
- 两层 for 循环:
- 外层 for 循环依次枚举第⼀个数 a ;
- 内层 for 循环依次枚举第⼆个数 b ,让它与 a 匹配;
注意细节:我们挑选第⼆个数的时候,可以不从第⼀个数开始选,因为 a 前⾯的数我们都已经在之前考虑过了;因此,我们可以从 a 往后的数开始列举。然后将挑选的两个数相加,判断是否符合⽬标值。
class Solution { public: vector<int> twoSum(vector<int>& price, int target) { int n = price.size(); for (int i = 0; i < n; i++) { // 第⼀层循环从前往后列举第⼀个数 for (int j = i + 1; j < n; j++) { // 第⼆层循环从 i 位置之后列举第⼆个 数 if (price[i] + price[j] == target) // 两个数的和等于⽬标值,说明我们 已经找到结果了 return { price[i], price[j] }; } } return { -1, -1 }; } }解法⼆(双指针 - 对撞指针):
算法思路:
注意到本题是升序的数组,因此可以⽤对撞指针优化时间复杂度。
算法流程
a. 初始化 left , right 分别指向数组的左右两端(这⾥不是我们理解的指针,⽽是数组的下
标)
b. 当 left < right 的时候,⼀直循环
1. 当 price[left] + price[right] == target 时,说明找到结果,记录结果,并且
返回;
2. 当 price[left] + price[right] < target 时:
- 对于 price[left] ⽽⾔,此时 price[right] 相当于是 price[left] 能碰到的最⼤值(别忘了,这⾥是升序数组哈~)。如果此时不符合要求,说明在这个数组⾥⾯,没有别的数符合 price[left] 的要求了(最⼤的数都满⾜不了你,你已经没救了)。因此,我们可以⼤胆舍去这个数,让 left++ ,去⽐较下⼀组数据;
- 那对于 price[right] ⽽⾔,由于此时两数之和是⼩于⽬标值的, price[right]还可以选择⽐ price[left] ⼤的值继续努⼒达到⽬标值,因此 right 指针我们按兵不动;
3.当 price[left] + price[right] > target 时,同理我们可以舍去price[right] (最⼩的数都满⾜不了你,你也没救了)。接着让 right-- ,继续⽐较下⼀组数据,⽽ left 指针不变(因为他还是可以去匹配⽐ price[right] 更⼩的数的
class Solution { public: vector<int> twoSum(vector<int>& price, int target) { int left = 0, right = price.size() - 1; while (left < right) { int sum = price[left] + price[right]; if (sum > target) right--; else if (sum < target) left++; else return { price[left], price[right] }; } // 照顾编译器 return { -4941, -1 }; } };手写分析:
过程展现:
问题补充:
问题1:return {price[left], price[right]}是在初始化变量吗?
答:
return {price[left], price[right]};用的是初始化列表的语法,但在return语句里,它的身份是返回值,不是变量初始化。这是在返回一个值,不是初始化变量。编译器看到return {a, b},会根据函数返回类型自动转成vector<int>{a, b}。
问题2:return {-4191, -1}里的数字是随便写的吗?
答:是的。这只是占位用的,因为函数声明了返回
vector<int>,编译器要求所有执行路径都有返回值。正常情况下while循环内已经 return 了,最后这个 return 不会执行到,所以数字随便写。
问题3:return {}和return {a, b}有什么区别?
答:
return {}:返回一个空vector<int>
return {a, b}:返回一个有两个元素的vector<int>,值是 a 和 b
4.三数之和
题⽬链接:15. 三数之和 - 力扣(LeetCode)
题目描述:
算法原理:
算法思路:
本题与两数之和类似。
与两数之和稍微不同的是,题⽬中要求找到所有不重复的三元组。那我们可以利⽤在两数之和我们先固定一个数,然后剩下的两个数之和变成它的相反数,就可以和为0
步骤如下:
1. 先排序;
2. 然后固定⼀个数 a :
3. 在这个数后⾯的区间内,使⽤双指针算法快速找到两个数之和等于 -a 即可。
但是要注意的是,这道题⾥⾯需要有去重操作
- 找到⼀个结果之后, left 和 right 指针要跳过重复的元素;
- 当使⽤完⼀次双指针算法之后,固定的 a 也要跳过重复的元素。
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; } };手写分析:
![]()
过程展示:![]()
问题补充:
问题1:ret.push_back({nums[i], nums[left], nums[right]})放进二维数组?
是的,放进去的是二维数组的一行。
ret的类型是vector<vector<int>>,里面的每个元素都是vector<int>;
{nums[i], nums[left], nums[right]}用初始化列表构造了一个一维vector<int>,里面有 3 个数;
push_back把这个一维vector塞进ret,成为一行
ret = [ [a1, b1, c1], // 第一行(一组三元组) [a2, b2, c2], // 第二行 [a3, b3, c3] // 第三行 ]那如果ret是vector<int>(一维),push_back({1,2,3})能放进去吗?
不能,会报错。因为vector<int>的push_back只接受一个int,不接受 3 个数。
问题2:i去重的时候,i往后移了,left和right不会没位置吗?
不会,因为每次
i移动后,left和right会重新初始化,每次i变化后,进入下一轮循环,left和right都会重新赋值,不会出现没位置的问题。
5.四数之和
题⽬链接:18. 四数之和 - 力扣(LeetCode)
题目描述:
算法原理:
解法(排序 + 双指针)
算法思路:
a. 依次固定⼀个数 a ;
b. 在这个数 a 的后⾯区间上,利⽤三数之和找到三个数,使这三个数的和等于 target
- a 即可。
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; } };过程展现:
手写分析:
我们会发现这里面的后四道题,都有一个操作那就是排序,升序排序或者降序排序,其实就是用到了双指针的单调性。这个方法很高效,时间复杂度比暴力求解好的多,但就是难想,今天给大家介绍了,后面遇到这种问题可以多了一种解题思路。
第一个专题双指针就已经结束,下一个专题滑动窗口
