【Algorithm】Day-2
本篇文章主要进行双指针与滑动窗口算法练习题的讲解
1 有效三角形的个数
leetcode链接:https://leetcode.cn/problems/valid-triangle-number/description/?envType=problem-list-v2&envId=two-pointers
题目描述:
给定一个包含非负整数的数组
nums
,返回其中可以组成三角形三条边的三元组个数。示例 1:
输入: nums = [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使用第二个 2) 2,2,3示例 2:
输入: nums = [4,2,3,4] 输出: 4提示:
1 <= nums.length <= 1000
0 <= nums[i] <= 1000
题目解析:
题目中会给你一个整数数组 nums,其中的元素都是非负整数,该题目要求返回 nums 中所有能够构成三角形三条边的三元组的个数。比如 nums = [2, 2, 3, 4],能够构成三角形的三元组有三个,分别是[2, 3, 4],[2, 3, 4],[2, 2, 3],虽然构成的三角形有两组是相同的,但是只要使用的是不同的元素,就算是两个不同的三元组。
算法讲解:
该题目涉及到了三角形的相关数学知识,假设三条边长度为 a, b, c,那么要想构成三角形,三条边必须满足 a + b > c,a + c > b,,b + c > a,a - b < c,c - a < b,c - b < a,即两边之和大于第三边,两边之差小于第三边。但是还有一个理论就是如果两条较小边的和大于那条最大的边,那么这三条边就可以构成三角形。比如三条边 a, b, c(a < b < c),如果 a + b > c,那么 a, b, c 就可以构成三角形。因为 c 为最大边,那么 a + c > b 、b + c > a、b - a < c 都是成立的,又 a + b > c ==> c - a < b 与 c -b < a,这样就得到了构成三角形的所有条件,所以只要 a + b > c,那么就可以构成三角形。
基于上述理论,我们只需要找到最大边,然后再找到两条较小的边,且满足这两条较小的边的和大于最大边就可以找到一个构成三角形的三元组。为了方便寻找最大边,我们先对数组按升序进行排序,那么从最后一个元素开始向前枚举,就依次为最大的边,我们可以在前面暴力枚举出所有的两条边进行判断,如果两条边的和大于最大边,那就可以构成三角形:
class Solution
{
public:int triangleNumber(vector<int>& nums) {//先对数组进行排序sort(nums.begin(), nums.end());//用 max 来标记最大边int max = nums.size() - 1;int count = 0;//count 记录最终结果while (max > 1){for (int i = 0; i < max - 1; i++){for (int j = i + 1; j < max; j++){if (nums[i] + nums[j] > nums[max])++count;}}--max;}return count;}
};
显然,这个算法的时间复杂度是 O(n^2) 的。
那么我们怎么可以进行优化呢?当我们在中间选取两条边的时候并没有利用上其有序的特性,我们可以先选取 0 下标和 max - 1 下标两条边作为三角形的另外两条边,如果 nums[0] + nums[max - 1] > nums[max],那么 [0, max - 2] 的所有边都能够与 max - 1 和 max 这两条边构成三角形,因为有序性, [1, max - 2] 都会 0 大,既然 nums[0] + nums[max - 1] > nums[max],那么 nums[i] + nums[max - 1] 也必然会大于 nums[max](0 < i < max - 1),如果 nums[0] + nums[max - 1] <= nums[max],那就继续判断 nums[1] + nums[max - 1] 是否大于 nums[max],就重复以上逻辑,就可以找出当 nums.size() - 1 为最大边的时候的三角形的三元组;之后,再选取 nums.size() - 2 为最大边,一次类推,就可以得到所有的能够构成三角形的三元组的个数了。所以,这道题目的解法就是双指针解法。
代码:
class Solution
{
public:int triangleNumber(vector<int>& nums) {//先对数组进行排序sort(nums.begin(), nums.end());//用 max 来标记最大边int max = nums.size() - 1;int count = 0;//count 记录最终结果while (max > 1){int left = 0, right = max - 1;while (left < right){if (nums[left] + nums[right] > nums[max]){count += (right - left);--right;}else ++left;}--max;}return count;}
};
显然,时间复杂度是 O(n^2) 的。
2 三数之和
leetcode链接:https://leetcode.cn/problems/3sum/?envType=problem-list-v2&envId=two-pointers
题目描述:
给你一个整数数组
nums
,判断是否存在三元组[nums[i], nums[j], nums[k]]
满足i != j
、i != k
且j != k
,同时还满足nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为0
且不重复的三元组。注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。示例 2:
输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。示例 3:
输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
题目解析:
该题目会给你一个整数数组 nums,其中 nums 数组中既可能有负数,也可能有正数与 0,该题目的要求就是让你找出三个和为 0 数字组合,并将所有满足该条件的三元组放到 vector<vector<int>> 中并返回该 vector<vector<int>>,要注意的是返回的所有的三元组不能有重复的三元组,这里的不重复就是指三元组的三个数字不能完全相同。例如 nums = [-2, -1, -1, 0, 0, 1, 2, 2, 3],那么返回的 vector<vector<int>> = [[-1, 0 ,1], [-2, 0, 2], [-1, -1, 2], [-2, -1, 3]]。
算法解析:
该题目的算法比较复杂,我们对题目要求进行变形,nums[i] + nums[j] + nums[k] == 0 变为 nums[i] + nums[j] == -nums[k],这样其实就很想我们之前做过的一个题目,就是在数组中寻找一个和为 target 的两个元素,所以这道题目我们可以采用先排序,如果最小的数字是正数,那就说明没有三个数字的和为0,那就直接返回空数组就可以了;如果最小的数字为负数,那就依次将最小的数字的负数作为那个目标 target,然后在 target 的后面寻找两个元素 left 与 right,如果 nums[left] + nums[right] == -target,那就将 nums[left]、nums[right] 与 target 一起尾插到 vector 中,这样就找到了一个三元组。如果 nums[left] + nums[right] > -target,说明是大了,由于数组已经有序了,所以直接 --right 就可以了;如果 nusm[left] + nums[right] < -target,那就 ++left。然后选取到一个满足条件的 left 与 right 之后,我们需要继续 ++left 与 --right。
题目中还有一个条件,那就是去重。这里的去重也很简单,由于排完序之后,相同的元素都会排在一起,所以在我们判断完 nums[left] + nums[right] 是否等于 -target 之后,再判断,如果 nums[left] ==nums[left - 1],那就 ++left;如果 nums[right] == nums[right + 1],那就 --right。
代码:
class Solution
{
public:vector<vector<int>> threeSum(vector<int>& nums) {//先对数组进行排序sort(nums.begin(), nums.end());vector<vector<int>> v;//然后依次选取最小的元素作为目标 targetfor (int i = 0; i < nums.size() - 2; i++){//如果最小的元素大于0,那就直接退出循环if (nums[i] > 0) break;//这里也需要去重if (i > 0 && nums[i] == nums[i - 1]) continue;//右边选取两个元素,使得他们的和等于 -targetint left = i + 1, right = nums.size() - 1;int target = -nums[i];while (left < right){int sum = nums[left] + nums[right];if (sum > target) --right;else if (sum < target) ++left;else{v.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;}}}return v;}
};
3 将 x 减到 0 的最小操作数
leetcode链接:https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/description/?envType=problem-list-v2&envId=sliding-window
题目描述:
给你一个整数数组
nums
和一个整数x
。每一次操作时,你应当移除数组nums
最左边或最右边的元素,然后从x
中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。如果可以将
x
恰好 减到0
,返回 最小操作数 ;否则,返回-1
。示例 1:
输入:nums = [1,1,4,2,3], x = 5 输出:2 解释:最佳解决方案是移除后两个元素,将 x 减到 0 。示例 2:
输入:nums = [5,6,7,8,9], x = 4 输出:-1示例 3:
输入:nums = [3,2,20,1,1,3], x = 10 输出:5 解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。提示:
1 <= nums.length <= 105
1 <= nums[i] <= 104
1 <= x <= 109
题目解析:
这道题目会给你一个整数数组 nums 和一个整数 x,可以进行的操作是,从数组的最左边或者最右边选择一个数字,让 x 减去该数字,之后重复此操作直到 x 减为了 0,返回所减数字的最小次数,如果 x 不能减为 0,那就返回 -1。需要注意一点的是,题目中的需要修改数组以供接下来的操作使用并不是真正的将元素删去,而是使用过最左边或者最右边的那个数字之后,那个数字从逻辑上就相当于消失了,如果用的是最左边的元素,那么他的下一个元素就成为了最左边的元素;如果用的是最右边的元素,那么前一个元素就成为了最右边的元素。例如示例1:nums = [1, 1, 4, 2, 3],x = 5,我们可以先用 x 减去最左边的 1,此时 nums = [1, 4, 2, 3],x = 4,再减去最左边的 1,nums = [4, 2, 3],x = 3,再减去最右边的 3,nums = [4, 2],x = 0,这次的操作数为 3;当然,最少次数的操作数应该是依次减去最右边的 3,2,nums = [1, 1, 4],x = 0,操作数为2。
算法讲解:
这道题目正着比较难解,我们可以反着想。原题是要我们在左边找一段区间,右边找一段区间,使得这两个区间的和正好等于 x,求出这两个区间长度的最小值;那么我们可以反着想,不就是在中间找一段区间,使得中间的和为 sum - x(sum 为整个数组元素的和),求满足条件的区间长度的最大值,如果我们把 sum - x 看成 target,这不就转化成了在数组中找一段最长的区间,使得该区间的和为 target 吗?之前我们做过一个长度最小的子数组,这道题目不就相当于长度最大的子数组吗,所以这道题目就是采用滑动窗口算法。
既然采用滑动窗口算法,那么就按照那四步来分析:
(1) 先求出数组的和 arr_sum,定义 left = 0, right = 0, target = arr_sum - x, len = -1, sum = 0,sum 为中间区域元素的和
(2) 进窗口:sum += nums[right],++right
(3) 判断:当 sum > target 时,出窗口,也就是 sum -= nums[left], ++left
(4) 更新结果:由于只有在 sum == target 的时候才会更新结果,所以我们需要判断一下,如果相等,那才更新结果
最后,需要注意一点,该题目最终返回的是最小操作数,当 len = -1 时,说明中间没有区域的和是等于 target 的,直接返回 -1;当 len 不等于 -1 的时候,需要返回 nums.size() - len。
代码:
class Solution
{
public:int minOperations(vector<int>& nums, int x) {//先求整个数组的和int arr_sum = 0;for (auto& e : nums)arr_sum += e;//定义变量int target = arr_sum - x;int left = 0, right = 0, sum = 0, len = -1;//由于数组中都是正整数,所以不可能加出小于0的数if (target < 0) return -1;while (right < nums.size()){//进窗口sum += nums[right];//判断while (sum > target){//出窗口sum -= nums[left++];}//更新结果if (sum == target) len = max(len, right - left + 1);++right;}return len == -1 ? -1 : nums.size() - len;}
};