Files
interview/16-LeetCode Hot 100/盛最多水的容器.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

12 KiB
Raw Blame History

盛最多水的容器 (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

思路推导

暴力解法分析

最直观的思路:枚举所有可能的线对,计算每对线构成的容器容量。

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) 决定

关键问题:如何移动指针才能找到更大的容量?

直觉思考

  • 如果移动较高的指针,宽度减小,高度只能保持不变或减小
  • 如果移动较短的指针,虽然宽度减小,但可能会找到更高的线

为什么这样思考?

  • 容量受限于较短的线
  • 移动较短的线才有可能增加容量
  • 移动较高的线不可能增加容量

优化后的思路

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)

优化思考 - 第二步:数学证明

问题:双指针法一定能找到最优解吗?

证明

假设当前指针在 leftright,且 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 = 0right = len(height) - 1maxArea = 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 实现

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 的同时,记录当前的 leftright 索引。

// 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: 如果高度可以为负数,需要先过滤掉负数或取绝对值。通常物理意义上的高度不应为负,但如果题目允许,可以这样处理:

// 处理负数:取绝对值
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. 深度理解:为什么双指针法一定正确?

数学证明:

假设当前指针在 leftright,且 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 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

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,可以提前终止。

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跳过明显不可能的线

如果移动后的线高度比移动前还低,可以继续移动直到找到更高的线。

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: 最大矩形