图解算法:为什么一定要移动那个短板?| LeetCode 11. 盛最多水的容器
前言:在面试中,有一类题目看似简单,暴力解法也能做,但面试官真正想看的是你如何将
O(N2)的复杂度优化到
O(N)。LeetCode 11 题“盛最多水的容器”就是这类题目的典范。今天我们不背代码,而是深入探讨背后的贪心策略与双指针思维。
一、 题目直觉与“木桶效应”
题目的目标非常直观:在一个数组中找到两条垂线,使得它们与 X 轴围成的容器能盛最多的水。
我们要计算的是矩形面积:
Area=Width×HeightArea=Width×Height
这里有一个物理常识至关重要,那就是木桶效应 (Short Board Effect) :
一个木桶能装多少水,取决于最短的那块木板。
映射到题目中:
- 宽度 (Width) :两条垂线在 X 轴上的距离 right - left。
- 高度 (Height) :两条垂线中较矮的那一条,即 Math.min(height[left], height[right])。
二、 痛点:为什么暴力解法不行?
最容易想到的思路是双重循环:计算所有两两组合的面积,然后取最大值。
然而以我的经验,当你写下双循环的时候,你自己心中的无奈,没有人会比你更了解
面试官在了解到你的解题思路时,就已经将你pass掉了
任何算法题,写双循环的结果只有死路一条(因为他会认为你对空间与时间复杂度没有概念,或者你的实力就这么多)
JavaScript
// 暴力解法
let max = 0;
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
// 计算每一对组合...
}
}
这种解法的时间复杂度是
O(N2)
。
题目提示中数组长度
NN
可达
105105
。这意味着计算量高达
10101010
次。在通常的算法竞赛或面试标准中,这绝对会触发 TLE (Time Limit Exceeded) 超时错误。
我们需要一种更聪明的做法,将复杂度降维打击到
O(N)
。
三、 核心:双指针法与贪心策略
我们要优化的核心是:如何尽可能少地遍历,却能保证不漏掉最大值?
1. 初始布局:拉满宽度
既然面积 = 宽 × 高,我们不妨先让宽度最大。
我们在数组的头尾各放置一个指针:left 指向开头,right 指向结尾。
此时,容器的底宽是最大的。接下来的每一步移动,宽度必然减小。为了弥补宽度的损失,我们必须寻找更高的垂线。
2. 决策困境:移动哪一根?
这是本题最难理解的点。假设现在的状况是:
-
左边柱子高度 left_h = 2
-
右边柱子高度 right_h = 8
-
当前宽度 w = 10
-
当前面积 =
2×10=202×10=20
现在我们需要向内移动一个指针,是移左边的(矮的),还是移右边的(高的)?
假设我们移动高的那一边(右边):
宽度肯定变小了(变成 9)。
而水位高度取决于谁?依然是左边那个不动的短板(高度 2)。
无论右边新遇到的柱子是高耸入云还是矮小不堪,容器的有效高度最高只能是 2。
新面积=9×min(2,新高度)≤18新面积=9×min(2,新高度)≤18
结论: 移动高板,宽度减小,高度受限于不动的短板(无法增加)。面积只会变小,绝对不可能变大。 这是一条死路。
贪心策略:移动矮的那一边(左边):
虽然宽度变小了(变成 9),但我们抛弃了当前的短板(高度 2)。
如果运气好,左边新遇到的柱子高度是 10,那么新的有效高度就变成了 8(受限于右边)。
新面积=9×8=72新面积=9×8=72
结论: 只有移动短板,我们才有可能找到更高的柱子来弥补宽度的损失。
这就是本题的贪心逻辑: 每一步我们都排除掉那个“导致当前高度受限”的短板,因为它已经发挥了它的最大潜力(在当前最宽的情况下),保留它没有任何意义。
四、 代码实现
理解了上述逻辑,代码实现就非常简单了。
JavaScript
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
// 1. 定义双指针,分别指向头尾
let left = 0;
let right = height.length - 1;
let maxWater = 0;
// 2. 当指针未相遇时循环
while (left < right) {
// 3. 计算当前面积
// 高度取决于短板 (木桶效应)
const currentHeight = Math.min(height[left], height[right]);
const currentWidth = right - left;
// 更新历史最大值
maxWater = Math.max(maxWater, currentHeight * currentWidth);
// 4. 核心决策:移动较矮的一侧
// 如果左边是短板,那左边这块板子在当前宽度下已经发挥了最大价值,
// 再往里缩宽度只会变小,保留左边没意义,不如向右移试试看有没有更高的。
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxWater;
};
五、 复杂度分析
-
时间复杂度:
O(N)双指针 left 和 right 总共遍历整个数组一次。相比于暴力解法的
O(N2),效率提升是巨大的。
-
空间复杂度:
O(1)O(1)我们只需要存储指针索引和 maxWater 几个变量,不需要额外的数组空间。
六、 总结
所谓算法优化,往往不是代码写得有多复杂,而是思维模型的转换。
LeetCode 11 题通过观察“木桶效应”,让我们明白:保留长板、抛弃短板是唯一可能获得更大收益的路径。这种通过排除法将搜索空间从二维矩阵(所有组合)压缩到一维线性扫描(双指针)的过程,就是算法中的降维打击。
希望这篇文章能帮你彻底搞懂双指针解法!