盛水最多的容器
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
**说明:**你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
提示:
n == height.length
2 <= n <= 10^5
0 <= height[i] <= 10^4
超时做法思路
开始的想法很简单,根据题意,定义盛水量为 tmp,双指针为 i 和 j,那么 tmp = min(height[i], height[j]) * (j - i)。直接从数组第一个元素开始枚举,最终更新最大答案即可,代码如下:
class Solution {
public:
int maxArea(vector<int>& height) {
int len = height.size(); // 数组长度
int i, j; // 定义双指针
int ans = 0; // 记录答案
for (i = 0; i < len - 1; ++i) {
for (j = i + 1; j < len; ++j) {
if (min(height[i], height[j]) * (j - i) > ans)
ans = min(height[i], height[j]) * (j - i);
}
}
return ans;
}
};
但是我们再看数据范围,数组元素个数最多是105,以上暴力算法的时间复杂度是O(n2),显然会超时,因此需要进一步优化。
优化双指针思路
我们对这个过程进行分析,容器的盛水量和双指针指向的元素中的较小者,以及双指针之间的距离有关。为了优化上面算法的时间复杂度,我们必须减少循环嵌套。因此我们可以先将两个指针分别指向数组的头和尾,将两个指针向内移动,直到两个指针相遇,找出最大容量,这样做就可以只用一层循环解决。
为了更低的时间复杂度,我们需要每次移动指针时保证盛水量尽量不减。我们把双指针指向的元素当作两个木板:
当我们向内移动短板时,(i - j) 一定减小,但是 min(height[i], height[j]) 可能增大,所以最终结果可能增大;
当我们向内移动长板时,(i - j) 一定减小,min(height[i], height[j]) 要么不变,要么减小,所以最终结果一定减小。
根据以上分析,我们每次向内移动短板,并更新答案即可。具体代码如下:
class Solution {
public:
int maxArea(vector<int>& height) {
int len = height.size(); // 数组长度
int i = 0, j = len - 1; // 定义双指针,指向起点与终点
int ans = 0; // 记录答案
while (i < j) {
int tmp = min(height[i], height[j]) * (j - i);
if (tmp > ans) ans = tmp;
// 每次移动短板
if (height[i] > height[j]) {
j--;
} else {
i++;
}
}
return ans;
}
};
三数之和
给你一个整数数组 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
-10^5 <= nums[i] <= 10^5
思路
因为要找一个三元组,所以开始的想法是用三个for循环进行枚举,当找到满足和为0的三元组就将其压入答案vector中。但这样会有问题,比如示例1中,我们用三重for循环枚举,前两重循环分别指向第一个元素-1和第二个元素0,第三重循环往后遍历时,会找到满足条件的1,此时得到满足条件的三元组[-1,0,1]。而当前两重循环遍历到第二个元素0和第三个元素1时,第三重循环会遍历到-1,也满足条件,但这个三元组与前面的三元组[-1,0,1]重复了。若直接在三重循环的基础上进行去重,还要用到哈希表,这样时间复杂度与空间复杂度都会比较高,因此考虑其他做法。
如何避免重复问题
因为上述做法出现了三元组重复的问题,即[-1,0,1]、[0,-1,1]、[1,0,-1]。那么有没有一种策略能够避免这种重复问题呢?答案当时是可以。我们只需要对于给定的数组进行排序,再通过以上三重循环思路进行枚举,还需要注意的是枚举时需要判断当前循环指向的元素与上一个元素是否相同,若相同则直接跳过。
时间复杂度的进一步优化
即使采用了上述做法避免了重复问题,我们仍然没有跳出三重循环的束缚,时间复杂度仍然为O(N^3)。我们要对其进行优化,可以考虑内层的两个循环。当我们确定最外层循环的数字 a 时,需要对内两层循环 b 和 c 进行枚举。
由于此时数组已经经过排序,所以对 b 和 c 的变化情况进行分析:首先 c 一定始终在 b 右侧,当 b 向右移动(b 增大)时,c 一定得变小,这是因为 a 不变,我们要找到 (b + c = -a) 的情况。所以说内两层循环可以进行合并操作,b 从左往右遍历的同时,c 从右往左遍历。具体看以下代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int len = nums.size();
// 先对数组进行排序,避免后续枚举时计算重复三元组
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
// 第一重循环枚举a
for (int first = 0; first < len; ++first) {
// 先判断当前枚举到的元素是否与上一个元素相同,避免重复
if (first > 0 && nums[first] == nums[first - 1]) continue;
// 指向c的指针,开始初始化在最右端
int third = len - 1;
int target = -nums[first];
// 第二重循环枚举b
for (int second = first + 1; second < len; ++second) {
// 先判断当前枚举到的元素是否与上一个元素相同,避免重复
if (second > first + 1 && nums[second] == nums[second - 1]) continue;
// 保证b始终在c左侧
while (second < third && nums[second] + nums[third] > target)
--third;
// 指针相遇就枚举完了
if (second == third) break;
// 找到一组解
if (nums[second] + nums[third] == target)
ans.push_back({nums[first], nums[second], nums[third]});
}
}
return ans;
}
};
if (nums[second] + nums[third] == target)
ans.push_back({nums[first], nums[second], nums[third]});
}
}
return ans;
}
};