按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
464 lines
12 KiB
Markdown
464 lines
12 KiB
Markdown
# 盛最多水的容器 (Container With Most Water)
|
||
|
||
## 题目描述
|
||
|
||
给定一个长度为 `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:**
|
||
```
|
||
输入:[1,1]
|
||
输出:1
|
||
```
|
||
|
||
### 约束条件
|
||
|
||
- `n == height.length`
|
||
- `2 <= n <= 10^5`
|
||
- `0 <= height[i] <= 10^4`
|
||
|
||
## 思路推导
|
||
|
||
### 暴力解法分析
|
||
|
||
**最直观的思路**:枚举所有可能的线对,计算每对线构成的容器容量。
|
||
|
||
```python
|
||
def maxArea(height):
|
||
max_water = 0
|
||
n = len(height)
|
||
|
||
for i in range(n):
|
||
for j in range(i+1, n):
|
||
# 容量 = 宽度 × 高度
|
||
width = j - i
|
||
h = min(height[i], height[j])
|
||
max_water = max(max_water, width * h)
|
||
|
||
return max_water
|
||
```
|
||
|
||
**时间复杂度**:O(n²)
|
||
- 外层循环:O(n)
|
||
- 内层循环:O(n)
|
||
- 总计:O(n) × O(n) = O(n²)
|
||
|
||
**空间复杂度**:O(1)
|
||
|
||
**问题分析**:
|
||
1. 效率低:n=10⁵ 时,n² 不可接受
|
||
2. 重复计算:很多组合明显不可能最优
|
||
3. 无法利用单调性优化
|
||
|
||
### 优化思考 - 第一步:双指针策略
|
||
|
||
**观察**:容器的容量由 `min(height[left], height[right]) × (right - left)` 决定
|
||
|
||
**关键问题**:如何移动指针才能找到更大的容量?
|
||
|
||
**直觉思考**:
|
||
- 如果移动较高的指针,宽度减小,高度只能保持不变或减小
|
||
- 如果移动较短的指针,虽然宽度减小,但可能会找到更高的线
|
||
|
||
**为什么这样思考?**
|
||
- 容量受限于较短的线
|
||
- 移动较短的线才有可能增加容量
|
||
- 移动较高的线不可能增加容量
|
||
|
||
**优化后的思路**:
|
||
```python
|
||
left, right = 0, len(height) - 1
|
||
max_water = 0
|
||
|
||
while left < right:
|
||
width = right - left
|
||
h = min(height[left], height[right])
|
||
max_water = max(max_water, width * h)
|
||
|
||
# 移动较短的指针
|
||
if height[left] < height[right]:
|
||
left += 1
|
||
else:
|
||
right -= 1
|
||
```
|
||
|
||
**时间复杂度**:O(n)
|
||
- 每次移动一个指针
|
||
- 最多移动 n 次
|
||
- 总计:O(n)
|
||
|
||
### 优化思考 - 第二步:数学证明
|
||
|
||
**问题**:双指针法一定能找到最优解吗?
|
||
|
||
**证明**:
|
||
|
||
假设当前指针在 `left` 和 `right`,且 `height[left] < height[right]`。
|
||
|
||
**当前容量**:`S = height[left] × (right - left)`
|
||
|
||
**如果移动 right 指针**:
|
||
- 新容量:`S' = min(height[left], height[right-1]) × (right - 1 - left)`
|
||
- 由于 `height[left] < height[right]`,且 `right-1 < right`
|
||
- 所以 `S' <= height[left] × (right - 1 - left) < height[left] × (right - left) = S`
|
||
|
||
**结论**:移动较高的指针不会得到更大的容量。
|
||
|
||
**因此**:每次移动较短的指针,可以保证不遗漏最优解。
|
||
|
||
## 解题思路
|
||
|
||
### 方法一:双指针法(最优解)
|
||
|
||
**核心思想:**使用两个指针,一个在数组开头,一个在数组末尾。每次移动较短的指针向中间靠拢。
|
||
|
||
**为什么这样做?**
|
||
- 容器的容量由 `min(height[left], height[right]) * (right - left)` 决定
|
||
- 如果移动较高的指针,宽度减小,高度只能保持不变或减小,容量一定不会增大
|
||
- 如果移动较短的指针,虽然宽度减小,但可能会找到更高的线,从而增大容量
|
||
|
||
**算法步骤:**
|
||
1. 初始化 `left = 0`,`right = len(height) - 1`,`maxArea = 0`
|
||
2. 当 `left < right` 时:
|
||
- 计算当前面积:`area = min(height[left], height[right]) * (right - left)`
|
||
- 更新 `maxArea = max(maxArea, area)`
|
||
- 如果 `height[left] < height[right]`,则 `left++`,否则 `right--`
|
||
3. 返回 `maxArea`
|
||
|
||
### 方法二:暴力枚举(不推荐)
|
||
|
||
枚举所有可能的线对,计算每对线构成的容器容量,取最大值。时间复杂度 O(n²),会超时。
|
||
|
||
## 代码实现
|
||
|
||
### Go 实现
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
)
|
||
|
||
func maxArea(height []int) int {
|
||
left, right := 0, len(height)-1
|
||
maxArea := 0
|
||
|
||
for left < right {
|
||
// 计算当前面积
|
||
width := right - left
|
||
h := height[left]
|
||
if height[right] < h {
|
||
h = height[right]
|
||
}
|
||
area := width * h
|
||
|
||
// 更新最大面积
|
||
if area > maxArea {
|
||
maxArea = area
|
||
}
|
||
|
||
// 移动较短的指针
|
||
if height[left] < height[right] {
|
||
left++
|
||
} else {
|
||
right--
|
||
}
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
|
||
// 测试用例
|
||
func main() {
|
||
// 测试用例1
|
||
height1 := []int{1, 8, 6, 2, 5, 4, 8, 3, 7}
|
||
fmt.Printf("输入: %v\n", height1)
|
||
fmt.Printf("输出: %d\n", maxArea(height1)) // 期望输出: 49
|
||
|
||
// 测试用例2
|
||
height2 := []int{1, 1}
|
||
fmt.Printf("\n输入: %v\n", height2)
|
||
fmt.Printf("输出: %d\n", maxArea(height2)) // 期望输出: 1
|
||
|
||
// 测试用例3: 递增序列
|
||
height3 := []int{1, 2, 3, 4, 5}
|
||
fmt.Printf("\n输入: %v\n", height3)
|
||
fmt.Printf("输出: %d\n", maxArea(height3)) // 期望输出: 6
|
||
|
||
// 测试用例4: 递减序列
|
||
height4 := []int{5, 4, 3, 2, 1}
|
||
fmt.Printf("\n输入: %v\n", height4)
|
||
fmt.Printf("输出: %d\n", maxArea(height4)) // 期望输出: 6
|
||
|
||
// 测试用例5: 包含0
|
||
height5 := []int{0, 2}
|
||
fmt.Printf("\n输入: %v\n", height5)
|
||
fmt.Printf("输出: %d\n", maxArea(height5)) // 期望输出: 0
|
||
}
|
||
```
|
||
|
||
- **时间复杂度:** O(n)
|
||
- 只需遍历数组一次,每次移动一个指针
|
||
- 指针最多移动 n 次
|
||
|
||
- **空间复杂度:** O(1)
|
||
- 只使用了常数级别的额外空间
|
||
- 只需要几个变量存储指针和最大面积
|
||
|
||
### 暴力枚举
|
||
- **时间复杂度:** O(n²)
|
||
- 需要枚举所有可能的线对
|
||
- 共有 n(n-1)/2 种组合
|
||
|
||
- **空间复杂度:** O(1)
|
||
- 只需要常数级别的额外空间
|
||
|
||
## 进阶问题
|
||
|
||
### Q1: 如果可以倾斜容器,问题会如何变化?
|
||
**A:** 如果可以倾斜容器,问题会变得复杂得多。需要考虑水的倾斜角度和容器的几何形状,这涉及到更多的物理和几何计算。
|
||
|
||
### Q2: 如果需要返回构成最大容器的两条线的索引,应该如何修改?
|
||
**A:** 在更新 `maxArea` 的同时,记录当前的 `left` 和 `right` 索引。
|
||
|
||
```go
|
||
// Go 版本
|
||
func maxAreaWithIndex(height []int) (int, int, int) {
|
||
left, right := 0, len(height)-1
|
||
maxArea, bestL, bestR := 0, 0, 0
|
||
|
||
for left < right {
|
||
h := height[left]
|
||
if height[right] < h {
|
||
h = height[right]
|
||
}
|
||
area := (right - left) * h
|
||
|
||
if area > maxArea {
|
||
maxArea = area
|
||
bestL, bestR = left, right
|
||
}
|
||
|
||
if height[left] < height[right] {
|
||
left++
|
||
} else {
|
||
right--
|
||
}
|
||
}
|
||
|
||
return maxArea, bestL, bestR
|
||
}
|
||
```
|
||
|
||
### Q3: 如果数组中有负数,应该如何处理?
|
||
**A:** 如果高度可以为负数,需要先过滤掉负数或取绝对值。通常物理意义上的高度不应为负,但如果题目允许,可以这样处理:
|
||
|
||
```go
|
||
// 处理负数:取绝对值
|
||
func maxAreaWithNegative(height []int) int {
|
||
left, right := 0, len(height)-1
|
||
maxArea := 0
|
||
|
||
for left < right {
|
||
h := height[left]
|
||
if height[right] < h {
|
||
h = height[right]
|
||
}
|
||
h = abs(h) // 取绝对值
|
||
area := (right - left) * h
|
||
maxArea = max(maxArea, area)
|
||
|
||
if abs(height[left]) < abs(height[right]) {
|
||
left++
|
||
} else {
|
||
right--
|
||
}
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
|
||
func abs(x int) int {
|
||
if x < 0 {
|
||
return -x
|
||
}
|
||
return x
|
||
}
|
||
```
|
||
|
||
## P7 加分项
|
||
|
||
### 1. 深度理解:为什么双指针法一定正确?
|
||
|
||
**数学证明:**
|
||
|
||
假设当前指针在 `left` 和 `right`,且 `height[left] < height[right]`。
|
||
|
||
我们要证明:移动 `right` 指针一定不会得到更大的面积。
|
||
|
||
- 当前面积:`S1 = height[left] * (right - left)`
|
||
- 移动 `right` 后的面积:`S2 = min(height[left], height[right-1]) * (right - 1 - left)`
|
||
- 由于 `height[left] < height[right]`,且 `right-1 < right`
|
||
- 所以 `S2 <= height[left] * (right - 1 - left) < height[left] * (right - left) = S1`
|
||
|
||
因此,移动较高的指针不会得到更大的面积。
|
||
|
||
### 2. 实战扩展:接雨水问题 (Trapping Rain Water)
|
||
|
||
**LeetCode 42:** 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
|
||
|
||
```go
|
||
func trap(height []int) int {
|
||
if len(height) < 3 {
|
||
return 0
|
||
}
|
||
|
||
left, right := 0, len(height)-1
|
||
leftMax, rightMax := 0, 0
|
||
water := 0
|
||
|
||
for left < right {
|
||
if height[left] < height[right] {
|
||
if height[left] >= leftMax {
|
||
leftMax = height[left]
|
||
} else {
|
||
water += leftMax - height[left]
|
||
}
|
||
left++
|
||
} else {
|
||
if height[right] >= rightMax {
|
||
rightMax = height[right]
|
||
} else {
|
||
water += rightMax - height[right]
|
||
}
|
||
right--
|
||
}
|
||
}
|
||
|
||
return water
|
||
}
|
||
```
|
||
|
||
**核心区别:**
|
||
- 盛水容器:找两条线构成最大面积
|
||
- 接雨水:计算所有能接的雨水总量
|
||
|
||
### 3. 变形题目
|
||
|
||
#### 变形1:盛最多水的容器 II(允许倾斜)
|
||
|
||
如果允许容器倾斜,最大水量取决于两条线之间的最小距离和角度。
|
||
|
||
#### 变形2:三维盛水
|
||
|
||
给定一个 m × n 的矩阵,每个格子表示高度,找出能盛最多水的四个角构成的容器。
|
||
|
||
#### 变形3:动态盛水
|
||
|
||
容器的高度会随时间变化,求某个时间段内能盛的最大水量。
|
||
|
||
### 4. 优化技巧
|
||
|
||
#### 优化1:提前终止
|
||
|
||
如果当前可能的面积(即使宽度最大)已经小于 `maxArea`,可以提前终止。
|
||
|
||
```go
|
||
func maxAreaOptimized(height []int) int {
|
||
left, right := 0, len(height)-1
|
||
maxArea := 0
|
||
|
||
for left < right {
|
||
area := (right - left) * min(height[left], height[right])
|
||
maxArea = max(maxArea, area)
|
||
|
||
// 提前终止:如果当前宽度已经很窄,可能无法超过maxArea
|
||
if right-left <= maxArea/max(height[left], height[right]) {
|
||
break
|
||
}
|
||
|
||
if height[left] < height[right] {
|
||
left++
|
||
} else {
|
||
right--
|
||
}
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
```
|
||
|
||
#### 优化2:跳过明显不可能的线
|
||
|
||
如果移动后的线高度比移动前还低,可以继续移动直到找到更高的线。
|
||
|
||
```go
|
||
func maxAreaSkip(height []int) int {
|
||
left, right := 0, len(height)-1
|
||
maxArea := 0
|
||
|
||
for left < right {
|
||
area := (right - left) * min(height[left], height[right])
|
||
maxArea = max(maxArea, area)
|
||
|
||
if height[left] < height[right] {
|
||
oldLeft := left
|
||
left++
|
||
// 跳过比oldLeft还低的线
|
||
for left < right && height[left] <= height[oldLeft] {
|
||
left++
|
||
}
|
||
} else {
|
||
oldRight := right
|
||
right--
|
||
// 跳过比oldRight还低的线
|
||
for left < right && height[right] <= height[oldRight] {
|
||
right--
|
||
}
|
||
}
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
```
|
||
|
||
### 5. 实际应用场景
|
||
|
||
- **水库设计:** 计算水库的最大蓄水量
|
||
- **城市规划:** 确定建筑物之间的最佳距离以最大化绿化面积
|
||
- **数据压缩:** 在某些压缩算法中寻找最优的分段点
|
||
|
||
### 6. 面试技巧
|
||
|
||
**面试官可能会问:**
|
||
1. "为什么双指针法一定能找到最优解?"
|
||
2. "如果数组有 10^8 个元素,你的算法还能用吗?"
|
||
3. "如何证明你的算法是正确的?"
|
||
|
||
**回答要点:**
|
||
1. 给出数学证明(如上所述)
|
||
2. 讨论算法的局限性,考虑分布式处理
|
||
3. 提供正确的证明思路
|
||
|
||
### 7. 相关题目推荐
|
||
|
||
- LeetCode 42: 接雨水
|
||
- LeetCode 11: 盛最多水的容器(本题)
|
||
- LeetCode 84: 柱状图中最大的矩形
|
||
- LeetCode 85: 最大矩形
|