想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第8课:盛最多水的容器
模块:双指针 | 难度:Medium ⭐⭐⭐ LeetCode 链接:leetcode.cn/problems/co… 前置知识:第7课(移动零-快慢指针基础) 预计学习时间:20分钟
🎯 题目描述
给你一个长度为 n 的整数数组 height,其中每个元素代表一条竖线的高度。这些竖线与 x 轴共同构成容器的边界。你需要找到两条线,使它们与 x 轴围成的容器能够容纳最多的水。
返回容器可以储存的最大水量。
注意:容器不能倾斜,水的高度由两条线中较短的那条决定,容器的宽度由两条线的水平距离决定。
示例:
输入:height = [1,8,6,2,5,4,8,3,7]
输出:49
解释:选择下标 1 和 8 的两条线(高度分别为 8 和 7),宽度为 8-1=7
面积 = min(8, 7) × 7 = 7 × 7 = 49
输入:height = [1,1]
输出:1
解释:只有两条线,高度都是1,宽度为1,面积为 1×1 = 1
约束条件:
2 <= height.length <= 10^5(至少2条线,最多10万条)0 <= height[i] <= 10^4(高度可以为0)
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 最小输入 | [1, 1] | 1 | 只有2条线的基本情况 |
| 含零高度 | [0, 2, 0, 3] | 3 | 高度为0的线不贡献面积 |
| 递增序列 | [1, 2, 3, 4, 5] | 6 | 最优解在两端(1×4 vs 2×3 vs 3×2) |
| 递减序列 | [5, 4, 3, 2, 1] | 6 | 同样,两端可能是最优 |
| 相同高度 | [4, 4, 4, 4] | 12 | 高度相同时,宽度最大即最优 |
| 大规模 | n=10^5 | — | 暴力法 O(n²) 会超时,必须 O(n) |
💡 思路引导
生活化比喻
想象你在一个停车场,两侧有很多不同高度的围栏。现在下大雨了,你想知道:如果选两块围栏作为"挡板",中间能积多少水?
🐌 笨办法:你从第一块围栏开始,依次和后面每一块围栏都试一遍——"我和第2块能积多少水?和第3块呢?和第4块呢?……" 测完第一块,再换第二块重复。这样两两组合全试一遍,累得要命,还要算 n×(n-1)/2 次!
🚀 聪明办法:你站在停车场中间,左手指着最左边的围栏,右手指着最右边的围栏(这样宽度最大!)。然后你发现:"咦,左边这块围栏比较矮,那水的高度就被它限制了。如果我把左手往右移一格(放弃这块矮围栏),也许能找到更高的围栏,虽然宽度少了1,但高度增加可能让总面积更大!" 就这样,每次移动较矮的那一侧,直到两手碰到一起。这样只需要扫一遍,就能找到最优解!
这就是对撞双指针 + 贪心的精髓:从最宽开始,逐步缩小宽度的同时,尽量提升高度。
关键洞察
面积 = min(height[left], height[right]) × (right - left)。移动较高的边只会让高度不变或更低且宽度减小,所以必然变差;只有移动较矮的边,才有可能找到更高的边来弥补宽度的损失。
🧠 解题思维链
这一节模拟你在面试中"从零开始思考"的过程。
Step 1:理解题目 → 锁定输入输出
- 输入:整数数组
height,每个元素代表一条竖线的高度 - 输出:一个整数,表示最大水量(面积)
- 核心公式:面积 = min(左边高度, 右边高度) × 宽度
- 限制:线段本身不占宽度,宽度 = 下标差
Step 2:先想笨办法(暴力法)
最直觉的想法:枚举所有可能的两条线组合,计算每对的面积,取最大值。
- 双层循环:
i从 0 到 n-2,j从 i+1 到 n-1 - 对每对
(i, j),计算area = min(height[i], height[j]) × (j - i) - 时间复杂度:O(n²)
- 瓶颈在哪:需要检查 n×(n-1)/2 对组合,当 n=10^5 时,约 50 亿次计算,会超时
Step 3:瓶颈分析 → 优化方向
暴力法的问题:很多组合根本不需要检查!比如,如果当前左边界很矮,那它和右边任何一条线组合,面积都被这个矮边限制住了。继续用这条矮边去试更近的线(宽度更小),只会让面积更小——这是在浪费时间!
- 核心问题:我们不需要试遍所有组合,因为大部分组合注定不是最优解
- 优化思路:从宽度最大的两端开始(这样至少宽度有优势),然后每次放弃较矮的边(因为它已经没有潜力了),向内收缩,直到两指针相遇
Step 4:选择武器
- 选用:对撞双指针
- 理由:
- 初始时宽度最大,面积有竞争力
- 每次移动矮边,是唯一可能让面积变大的策略(移动高边必然变差)
- 只需一次遍历,O(n) 时间
🔑 模式识别提示:当题目涉及"两端向中间"、"寻找最优配对"、"有序/单调性"(这里宽度单调递减),优先考虑"对撞双指针"模式
🔑 解法一:暴力双循环(直觉法)
思路
枚举所有可能的 (i, j) 组合,计算每对的容水量,取最大值。虽然直观,但时间复杂度 O(n²),大数据会超时。
图解过程
示例:height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
索引: 0 1 2 3 4 5 6 7 8
暴力法:依次计算所有组合
(0,1): min(1,8)×1 = 1
(0,2): min(1,6)×2 = 2
(0,3): min(1,2)×3 = 3
...
(1,6): min(8,8)×5 = 40 ← 不错,但还不是最优
(1,8): min(8,7)×7 = 49 ← 最优解!
...
总共需要计算 9×8/2 = 36 次
Python代码
from typing import List
def max_area_brute(height: List[int]) -> int:
"""
解法一:暴力双循环
思路:枚举所有 (i,j) 对,计算面积,取最大
"""
n = len(height)
max_water = 0 # 记录最大面积
for i in range(n): # 左边界
for j in range(i + 1, n): # 右边界(j > i)
width = j - i # 宽度 = 下标差
h = min(height[i], height[j]) # 高度 = 较矮的那条线
area = h * width # 面积 = 高×宽
max_water = max(max_water, area) # 更新最大值
return max_water
# ✅ 测试
print(max_area_brute([1, 8, 6, 2, 5, 4, 8, 3, 7])) # 期望输出:49
print(max_area_brute([1, 1])) # 期望输出:1
print(max_area_brute([4, 3, 2, 1, 4])) # 期望输出:16
print(max_area_brute([1, 2, 1])) # 期望输出:2
复杂度分析
- 时间复杂度:O(n²) — 双层循环,外层 n 次,内层平均 n/2 次
- 具体地说:如果 n=1000,需要约 500,000 次计算;如果 n=10^5,需要约 50 亿次,会超时
- 空间复杂度:O(1) — 只用了几个变量
优缺点
- ✅ 思路简单,容易理解和实现
- ✅ 一定能找到正确答案
- ❌ 时间复杂度 O(n²),大数据会超时,不符合面试要求
⚡ 解法二:对撞双指针 + 贪心(最优解)
优化思路
暴力法浪费在:很多明显不优的组合也被计算了。我们可以用贪心策略:从最宽的两端开始,每次移动较矮的边。
为什么这样做是对的?
- 初始状态:
left=0, right=n-1,宽度最大 - 贪心抉择:当前面积 =
min(h[left], h[right]) × (right-left)- 如果移动较高的边,宽度减1,高度最多不变(可能更矮) → 面积必然≤当前
- 如果移动较矮的边,宽度减1,但高度有可能变大 → 这是唯一有希望超越当前面积的选择
- 终止条件:
left和right相遇时,所有有潜力的组合都已考察过
💡 关键想法:每次放弃的那条矮边,和它对面所有线的组合都不可能比当前更优(因为高度被它限制,宽度只会更小),所以可以安全丢弃。
图解过程
示例:height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
索引: 0 1 2 3 4 5 6 7 8
初始状态:
left=0 (h=1), right=8 (h=7)
面积 = min(1,7) × 8 = 1×8 = 8
max_area = 8
[1] ←left 8 6 2 5 4 8 3 [7]← right
↑ ↑
矮边(1 < 7),移动 left++
Step 1: left=1 (h=8), right=8 (h=7)
面积 = min(8,7) × 7 = 7×7 = 49 ✅ 更新最大值!
max_area = 49
1 [8]←left 6 2 5 4 8 3 [7]← right
↑ ↑
高边(8 > 7),移动 right--
Step 2: left=1 (h=8), right=7 (h=3)
面积 = min(8,3) × 6 = 3×6 = 18 < 49
1 [8]←left 6 2 5 4 8 [3] 7 ← right
↑ ↑
高边(8 > 3),移动 right--
Step 3: left=1 (h=8), right=6 (h=8)
面积 = min(8,8) × 5 = 8×5 = 40 < 49
1 [8]←left 6 2 5 4 [8] 3 7 ← right
↑ ↑
相等(8 == 8),移动任意一个,这里选 left++
Step 4: left=2 (h=6), right=6 (h=8)
面积 = min(6,8) × 4 = 6×4 = 24 < 49
1 8 [6]←left 2 5 4 [8] 3 7 ← right
↑ ↑
矮边(6 < 8),移动 left++
Step 5~7: 继续收缩...直到 left >= right
最终答案:49
Python代码
from typing import List
def max_area_two_pointer(height: List[int]) -> int:
"""
解法二:对撞双指针
思路:从两端开始,每次移动较矮的边,更新最大面积
"""
left, right = 0, len(height) - 1 # 左右指针从两端开始
max_water = 0 # 记录最大面积
while left < right: # 当两指针未相遇
width = right - left # 宽度
h = min(height[left], height[right]) # 高度(取较矮的)
area = h * width # 当前面积
max_water = max(max_water, area) # 更新最大值
# 贪心:移动较矮的边(这是唯一可能变大的选择)
if height[left] < height[right]:
left += 1 # 左边矮,右移左指针
else:
right -= 1 # 右边矮(或相等),左移右指针
return max_water
# ✅ 测试
print(max_area_two_pointer([1, 8, 6, 2, 5, 4, 8, 3, 7])) # 期望输出:49
print(max_area_two_pointer([1, 1])) # 期望输出:1
print(max_area_two_pointer([4, 3, 2, 1, 4])) # 期望输出:16
print(max_area_two_pointer([1, 2, 1])) # 期望输出:2
print(max_area_two_pointer([2, 3, 4, 5, 18, 17, 6])) # 期望输出:17
复杂度分析
- 时间复杂度:O(n) — 每个元素最多被访问一次,左右指针共移动 n-1 次
- 具体地说:如果 n=10^5,只需要约 10 万次操作,比暴力法快 50,000 倍!
- 空间复杂度:O(1) — 只用了 3 个变量(left, right, max_water)
🐍 Pythonic 写法
利用 Python 的三元表达式可以让代码更简洁:
def max_area_pythonic(height: List[int]) -> int:
"""一行流写法(不推荐面试用,但展示 Python 技巧)"""
left, right, max_water = 0, len(height) - 1, 0
while left < right:
max_water = max(max_water, min(height[left], height[right]) * (right - left))
left, right = (left + 1, right) if height[left] < height[right] else (left, right - 1)
return max_water
或者用更极致的函数式风格(仅供欣赏,实际不推荐):
from functools import reduce
def max_area_functional(height: List[int]) -> int:
"""函数式编程风格 — 仅用于学习,面试别用!"""
def step(state, _):
left, right, max_area = state
if left >= right:
return state
area = min(height[left], height[right]) * (right - left)
new_max = max(max_area, area)
if height[left] < height[right]:
return (left + 1, right, new_max)
else:
return (left, right - 1, new_max)
final_state = reduce(step, range(len(height)), (0, len(height) - 1, 0))
return final_state[2]
⚠️ 面试建议:先写清晰版本(解法二)展示思路,代码通过后可以说:"我可以用 Python 的三元表达式优化一下",但千万别一上来就写晦涩代码。面试官更看重你的思考过程和沟通能力,而非炫技。
📊 解法对比
| 维度 | 解法一:暴力双循环 | 解法二:对撞双指针 |
|---|---|---|
| 时间复杂度 | O(n²) | O(n) |
| 空间复杂度 | O(1) | O(1) |
| 代码难度 | 简单 | 中等(需要理解贪心正确性) |
| 面试推荐 | ⭐ | ⭐⭐⭐ |
| 适用场景 | n ≤ 1000 的小数据 | 所有情况,尤其 n > 10^4 |
| 正确性证明 | 显然(枚举所有) | 需要证明贪心不漏最优解 |
面试建议:
- 先说暴力法:"最直接的想法是枚举所有组合,O(n²),但这对 10 万数据会超时"
- 引出优化:"我注意到,如果左边很矮,那它和右边所有线的组合都被限制了,可以直接放弃它。这启发我用双指针从两端开始,每次移动矮边,这样能保证不漏掉最优解,且只需 O(n)"
- 写代码 + 讲解:边写边说"这里移动矮边是因为…"
- 测试边界:自己举例
[1,1],[1,2,1]验证
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你解决一下这道题(给出题目)。
你:(审题30秒)好的,这道题是要找两条线,使它们与 x 轴围成的容器容水量最大。容水量 = min(两条线的高度) × 宽度。
面试官:对的,有什么想法吗?
你:我的第一个想法是暴力枚举所有 (i, j) 对,计算每对的面积,取最大值。这样时间复杂度是 O(n²),对小数据可行,但题目约束 n 可以到 10^5,暴力法会超时。
面试官:嗯,那怎么优化?
你:我注意到一个规律:假如当前左边界很矮,那它和右边任何一条线的组合,面积都被它限制住了,而且往右移(宽度变小)只会更差。所以可以直接放弃这条矮边。这启发我用对撞双指针:从两端开始,每次移动较矮的边,这样既不会漏掉最优解,又只需遍历一次,O(n)。
面试官:很好,为什么移动矮边是对的?移动高边不行吗?
你:因为面积 = min(左,右) × 宽度。如果我移动高边,宽度减 1,而高度最多保持不变(因为 min 取决于矮边),所以面积必然 ≤ 当前值。但如果移动矮边,虽然宽度也减 1,但高度有可能变大(如果新边更高),这是唯一可能超越当前面积的选择。所以贪心策略是:总是移动矮边。
面试官:好的,请写一下代码。
你:(边写边说)我用 left 和 right 两个指针从两端开始,每次计算当前面积,更新最大值。然后比较 height[left] 和 height[right],移动较矮的那个。循环直到两指针相遇。(写完)
面试官:测试一下?
你:好的,用 [1, 8, 6, 2, 5, 4, 8, 3, 7]。初始 left=0(h=1), right=8(h=7),面积 1×8=8。左边矮,右移左指针到 left=1(h=8),面积 7×7=49,这是最大值。继续收缩...最终返回 49。再测一个边界 [1,1]:只有两条线,面积 1×1=1,正确。
面试官:不错!
高频追问
| 追问 | 应答策略 |
|---|---|
| "为什么对撞指针不会漏掉最优解?" | "每次放弃的矮边,和它对面所有线的组合都不如当前(高度被限制,宽度只会更小),所以不会漏。最优解要么在当前,要么在剩余区间,递归必能找到。" |
| "如果有多个最优解怎么办?" | "题目只要求返回最大值,不需要返回下标。如果要返回所有最优对,可以在更新 max 时用列表记录所有达到最大值的 (i,j)。" |
| "空间能不能更优?" | "已经是 O(1) 了,只用了 3 个变量,无法再优化。" |
| "这个算法能推广到其他问题吗?" | "能!对撞指针 + 贪心在很多问题中适用,比如三数之和(LeetCode 15)、接雨水(LeetCode 42,虽然也可用单调栈)。核心是:从两端开始,根据某个规则(这里是高度)决定移动哪一端。" |
🎓 知识点总结
Python技巧卡片 🐍
# 技巧1:同时更新多个变量 — 避免中间变量
left, right = (left + 1, right) if height[left] < height[right] else (left, right - 1)
# 等价于:
# if height[left] < height[right]:
# left += 1
# else:
# right -= 1
# 技巧2:max() 的链式调用 — 多个值中取最大
max_area = max(max_area, min(height[left], height[right]) * (right - left))
# 技巧3:enumerate() 虽然本题用不上,但在类似问题中很有用
for i, h in enumerate(height):
print(f"索引 {i} 的高度是 {h}")
💡 底层原理(选读)
为什么对撞指针的贪心策略是正确的?
数学证明(反证法):
- 假设最优解是
(i*, j*),面积为A* - 我们的算法从
(0, n-1)开始,假设某一步跳过了(i*, j*) - 算法跳过
(i*, j*)只有一种可能:在到达i*或j*之前,就让另一个指针越过了它 - 不失一般性,假设是在
left < i*时,right就从j*越过了(即right移到了j*-1) - 这说明在某个状态
(left, j*)时,height[left] > height[j*],所以移动了right - 但此时
left < i*,所以height[left] ≤ height[i*](否则(left, j*)面积更大,矛盾) - 因此
min(height[left], height[j*]) ≤ min(height[i*], height[j*])且宽度更大 - 这意味着
(left, j*)的面积 ≥A*,与A*是最优解矛盾!
结论:算法不会漏掉最优解。
Python 的 min/max 是怎么实现的?
- Python 内置的
min(a, b)是 C 语言实现的,时间复杂度 O(1)- 如果传入的是可迭代对象
min([1,2,3]),则是 O(n) 遍历- 本题中
min(height[left], height[right])只比较两个数,所以是 O(1)
算法模式卡片 📐
- 模式名称:对撞双指针 + 贪心
- 适用条件:
- 问题涉及"两端"或"区间"
- 存在某种单调性或优化方向(这里是:移动矮边才可能变优)
- 可以通过局部决策(移动哪个指针)逐步逼近最优解
- 识别关键词:"最大面积"、"两端"、"容器"、"配对"、"有序/单调"
- 模板代码:
def two_pointer_converge(arr):
left, right = 0, len(arr) - 1
result = 初始值
while left < right:
# 计算当前状态的值
current = 某个函数(arr[left], arr[right])
result = max/min(result, current)
# 根据贪心策略移动指针
if 某个条件(arr[left], arr[right]):
left += 1
else:
right -= 1
return result
易错点 ⚠️
-
错误:移动错了指针
- 常见错误:
if height[left] > height[right]: left += 1(应该是<时移动 left!) - 原因:逻辑搞反了,记住:移动矮边
- 正确做法:画图理解,矮边限制了高度,继续用它只会更差
- 常见错误:
-
错误:宽度计算错了
- 常见错误:
width = right - left + 1(以为包含端点) - 原因:混淆了"线段数量"和"间距"。两条线之间的间距是下标差,不需要 +1
- 正确做法:
width = right - left
- 常见错误:
-
错误:初始化 max_area 为负数
- 常见错误:
max_area = float('-inf')(高度可以是 0,但面积 ≥ 0) - 原因:题目约束
height[i] ≥ 0,所以最小面积是 0,不是负无穷 - 正确做法:
max_area = 0
- 常见错误:
-
错误:两指针相等时还在计算
- 常见错误:
while left <= right(应该是<) - 原因:
left == right时只有一条线,无法构成容器 - 正确做法:
while left < right
- 常见错误:
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
-
场景1:数据库查询优化 — 区间查询 在时序数据库中,经常需要查询某个时间范围内的最大值。如果数据按时间排序,可以用类似的双指针策略优化查询。
-
场景2:图像处理 — 最大矩形检测 在计算机视觉中,检测图像中的最大矩形区域(如文档边框检测)时,需要找两条边界线使围成的矩形面积最大,可以用对撞指针加速。
-
场景3:股票交易策略 — 买卖时机 虽然本题不直接对应股票,但双指针思想在"买卖股票的最佳时机"(LeetCode 121)中也有应用:左指针记录最低价,右指针扫描计算最大利润。
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 42. 接雨水 | Hard | 对撞指针/单调栈/DP | 和本题类似,但高度是"凹陷处能接多少水",也可用双指针 |
| LeetCode 15. 三数之和 | Medium | 对撞指针+去重 | 固定一个数,对剩余数组用双指针找另外两个数 |
| LeetCode 167. 两数之和 II | Easy | 对撞指针(有序数组) | 本题的简化版,数组已排序,直接双指针 |
| LeetCode 611. 有效三角形的个数 | Medium | 对撞指针 | 排序后,固定最长边,用双指针找满足 a+b>c 的对数 |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目:给定一个整数数组 height,你可以选择三条线与 x 轴围成一个区域(类似本题,但是三条线)。计算这三条线能围成的最大面积。注意:三条线必须形成一个封闭区域,且不能重叠。
💡 提示(实在想不出来再点开)
可以先固定中间的一条线,然后对剩余两侧用对撞双指针找最优的两条线。或者用三重循环暴力(如果数据量小)。
✅ 参考答案
def max_area_three_lines(height: List[int]) -> int:
"""
三条线围成最大面积 — 暴力法
思路:枚举所有 (i, j, k) 三元组,计算面积
注意:三条线需要满足 i < j < k,面积计算比较复杂,取决于如何定义"围成"
"""
# 简化版:假设面积 = min(h[i], h[j], h[k]) × (k - i)
n = len(height)
max_area = 0
for i in range(n - 2):
for j in range(i + 1, n - 1):
for k in range(j + 1, n):
h = min(height[i], height[j], height[k])
width = k - i
area = h * width
max_area = max(max_area, area)
return max_area
复杂度:O(n³),只适用于小数据。如果要优化到 O(n²),需要更复杂的策略(固定中间线 + 双指针)。
如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。