From 67189941d866e0db35dd30b69bb9c5fbafdb4031 Mon Sep 17 00:00:00 2001 From: yasinshaw Date: Sun, 8 Mar 2026 21:32:25 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=94=B9=E8=BF=9B=20LeetCode=20?= =?UTF-8?q?=E9=A2=98=E7=9B=AE=E8=A7=A3=E9=A2=98=E6=80=9D=E8=B7=AF=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E7=A8=8B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 改进以下三个题目的文档: 1. 最小栈 (LeetCode 155) 2. 最大正方形 (LeetCode 221) 3. 柱状图中最大的矩形 (LeetCode 84) 改进内容: - 新增"思路推导"部分:从暴力解法分析开始,逐步优化 - 详细化"解题思路"部分:分步骤说明,增加 Q&A 问答 - 新增"关键细节说明":解释为什么这样写代码 - 新增"边界条件分析":覆盖各种特殊情况 - 新增"执行过程演示":完整示例跟踪 - 新增"常见错误":对比错误和正确写法 - 新增"进阶问题":扩展思路 参考文档:算法解题思路改进方案.md Co-Authored-By: Claude Sonnet 4.5 --- 16-LeetCode Hot 100/最大正方形.md | 739 +++++++++++++++++- 16-LeetCode Hot 100/最小栈.md | 496 +++++++++++- 16-LeetCode Hot 100/柱状图中最大的矩形.md | 897 +++++++++++++++++++++- 3 files changed, 2071 insertions(+), 61 deletions(-) diff --git a/16-LeetCode Hot 100/最大正方形.md b/16-LeetCode Hot 100/最大正方形.md index 962738c..03b9616 100644 --- a/16-LeetCode Hot 100/最大正方形.md +++ b/16-LeetCode Hot 100/最大正方形.md @@ -1,47 +1,470 @@ # 最大正方形 (Maximal Square) +LeetCode 221. Medium + ## 题目描述 在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。 -## 解题思路 +**示例 1**: +``` +输入:matrix = [ + ["1","0","1","0","0"], + ["1","0","1","1","1"], + ["1","1","1","1","1"], + ["1","0","0","1","0"] +] +输出:4 +``` -### 动态规划 +**示例 2**: +``` +输入:matrix = [["0","1"],["1","0"]] +输出:1 +``` -**状态定义:** dp[i][j] 表示以 (i, j) 为右下角的最大正方形边长。 +## 思路推导 -**状态转移:** dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 +### 暴力解法分析 -## Go 代码 +**最直观的思路**:枚举所有可能的正方形 ```go func maximalSquare(matrix [][]byte) int { if len(matrix) == 0 { return 0 } - + m, n := len(matrix), len(matrix[0]) - dp := make([][]int, m+1) - for i := range dp { - dp[i] = make([]int, n+1) - } - maxSide := 0 - - for i := 1; i <= m; i++ { - for j := 1; j <= n; j++ { - if matrix[i-1][j-1] == '1' { - dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 - if dp[i][j] > maxSide { - maxSide = dp[i][j] + + // 枚举每个位置作为左上角 + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if matrix[i][j] == '1' { + // 枚举可能的边长 + for side := 1; side <= min(m-i, n-j); side++ { + // 检查边长为 side 的正方形是否全为 1 + if isValidSquare(matrix, i, j, side) { + maxSide = max(maxSide, side) + } } } } } - + return maxSide * maxSide } +func isValidSquare(matrix [][]byte, x, y, side int) bool { + for i := x; i < x+side; i++ { + for j := y; j < y+side; j++ { + if matrix[i][j] != '1' { + return false + } + } + } + return true +} +``` + +**时间复杂度分析**: +``` +- 外层循环:O(m × n) 枚举左上角 +- 中层循环:O(min(m, n)) 枚举边长 +- 内层检查:O(side²) 检查正方形 +- 总计:O(m × n × min(m, n) × min(m, n)²) = O(m² × n²) +``` + +**问题**:复杂度过高,对于大型矩阵会超时。 + +### 优化思考 + +**观察**:暴力解法中,很多检查是重复的。 + +**关键问题**:能否利用已计算的信息避免重复检查? + +**思路1:前缀和优化** +```go +// 计算每个位置为右下角的矩形中 1 的个数 +// 可以 O(1) 判断一个矩形是否全为 1 +``` + +**时间复杂度**:O(m × n × min(m, n)) - 仍然不够好 + +**思路2:动态规划** ✅ + +核心洞察:**以 (i, j) 为右下角的最大正方形,取决于其相邻三个位置的状态** + +### 为什么这样思考? + +**关键观察**: +``` +如果要形成以 (i, j) 为右下角的正方形,需要满足: +1. 位置 (i, j) 本身是 '1' +2. 其左边、上边、左上三个位置都能形成足够大的正方形 + +图示: + (i-1, j-1) (i-1, j) + ┌─────┐ + │ │ + └─────┼─ (i, j) + (i, j-1) + +如果 (i, j) 是 '1',那么: +- 以 (i, j) 为右下角的最大正方形边长 +- = min(上边、左边、左上边的最大正方形边长) + 1 +``` + +**为什么取 min?** +- 只有当三个方向都能形成 k×k 的正方形时 +- 当前位置才能形成 (k+1)×(k+1) 的正方形 +- 这是"木桶效应":受限于最短的那块木板 + +## 解题思路 + +### 核心思想 + +**动态规划**: +- **状态定义**:dp[i][j] 表示以 matrix[i-1][j-1] 为右下角的最大正方形边长 +- **状态转移**:dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 +- **边界条件**:第一行和第一列的 dp 值等于矩阵本身的值 + +### 算法流程(详细版) + +**步骤1:定义 DP 数组** + +```go +m, n := len(matrix), len(matrix[0]) +dp := make([][]int, m+1) // 多一行一列,方便处理边界 +for i := range dp { + dp[i] = make([]int, n+1) +} +``` + +**为什么用 m+1 和 n+1?** +- dp[i][j] 对应矩阵中的 matrix[i-1][j-1] +- 这样第一行和第一列自然为 0,作为边界条件 +- 避免在循环中特殊判断 i=0 或 j=0 的情况 + +**步骤2:遍历矩阵,填充 DP 表** + +```go +maxSide := 0 + +for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { + // 当前位置是 '1',计算能形成的最大正方形 + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + if dp[i][j] > maxSide { + maxSide = dp[i][j] + } + } + // 如果是 '0',dp[i][j] 默认为 0 + } +} +``` + +**关键点解释**: + +**Q1:为什么是 dp[i-1][j], dp[i][j-1], dp[i-1][j-1]?** + +``` +这三个位置分别是: +- dp[i-1][j]:上边位置为右下角的最大正方形 +- dp[i][j-1]:左边位置为右下角的最大正方形 +- dp[i-1][j-1]:左上位置为右下角的最大正方形 + +只有这三个位置都能形成 k×k 的正方形 +当前位置 (i, j) 才能形成 (k+1)×(k+1) 的正方形 +``` + +**Q2:为什么取 min?** + +``` +示例: +1 1 1 +1 1 1 +1 1 1 + +假设我们要计算 dp[3][3](右下角): +- dp[2][3] = 2 (上边能形成 2×2) +- dp[3][2] = 2 (左边能形成 2×2) +- dp[2][2] = 2 (左上能形成 2×2) + +取 min = 2,所以 dp[3][3] = 2 + 1 = 3 +即可以形成 3×3 的正方形 ✓ + +反例: +1 1 0 +1 1 1 +1 1 1 + +计算 dp[3][3]: +- dp[2][3] = 1 (上边只能形成 1×1,因为被 0 限制) +- dp[3][2] = 2 +- dp[2][2] = 1 + +取 min = 1,所以 dp[3][3] = 1 + 1 = 2 +只能形成 2×2 的正方形 ✓ +``` + +**Q3:为什么 +1?** + +``` ++1 表示当前位置本身也是一个 '1' +可以把当前正方形看作: +- 在 (i-1, j-1) 位置的正方形基础上 +- 向右扩展一列,向下扩展一行 +- 再加上当前位置的 '1' +``` + +**步骤3:返回面积** + +```go +return maxSide * maxSide +``` + +### 关键细节说明 + +**细节1:为什么 dp 数组要比矩阵大?** + +```go +// 矩阵:m 行 n 列 +// dp 数组:(m+1) 行 (n+1) 列 + +dp := make([][]int, m+1) // ✅ 正确 +for i := range dp { + dp[i] = make([]int, n+1) +} + +// 作用: +// - dp[0][j] 和 dp[i][0] 都是 0,作为边界 +// - dp[i][j] 对应 matrix[i-1][j-1] +// - 避免在循环中判断边界条件 +``` + +**细节2:为什么 matrix[i-1][j-1]?** + +```go +for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { // 注意索引偏移 + // ... + } + } +} + +// 因为 dp 从 1 开始,但矩阵从 0 开始 +// dp[i][j] 对应 matrix[i-1][j-1] +``` + +**细节3:min 函数的实现** + +```go +func min(a, b, c int) int { + if a < b { + if a < c { + return a + } + return c + } + if b < c { + return b + } + return c +} + +// Go 1.21+ 可以使用内置的 min 函数 +// import "cmp" +// minVal := min(a, b, c) +``` + +### 边界条件分析 + +**边界1:空矩阵** +``` +输入:matrix = [] +输出:0 +处理:开头直接检查 if len(matrix) == 0 +``` + +**边界2:全 0 矩阵** +``` +输入:matrix = [["0","0"],["0","0"]] +输出:0 +处理:所有 dp[i][j] 保持为 0,maxSide = 0 +``` + +**边界3:单个 1** +``` +输入:matrix = [["1"]] +输出:1 +过程: +dp[1][1] = min(dp[0][1], dp[1][0], dp[0][0]) + 1 + = min(0, 0, 0) + 1 + = 1 +maxSide = 1 +面积 = 1 × 1 = 1 ✓ +``` + +**边界4:只有一行或一列** +``` +输入:matrix = [["1","1","1"]] +输出:1 +过程: +dp[1][1] = 1, dp[1][2] = 1, dp[1][3] = 1 +maxSide = 1(无法形成更大的正方形) +面积 = 1 × 1 = 1 ✓ +``` + +### 复杂度分析(详细版) + +**时间复杂度**: +``` +- 遍历矩阵:O(m × n) +- 每个位置的计算:O(1)(只是三次比较和一次加法) +- 总计:O(m × n) + +为什么是 O(m × n)? +- 只需要遍历矩阵一次 +- 每个位置的计算都是常数时间 +``` + +**空间复杂度**: +``` +- DP 数组:O((m+1) × (n+1)) = O(m × n) +- 其他变量:O(1) +- 总计:O(m × n) + +能否优化? +- 可以优化到 O(n),只保留两行 +- 但代码会变复杂,O(m × n) 通常可接受 +``` + +## 执行过程演示 + +**示例矩阵**: +``` +matrix = [ + ["1","0","1","0","0"], + ["1","0","1","1","1"], + ["1","1","1","1","1"], + ["1","0","0","1","0"] +] + +DP 表(加了一圈 0 边界): + + 0 0 0 0 0 0 + 0 1 0 1 0 0 + 0 1 0 1 1 1 + 0 1 1 2 2 2 + 0 1 0 0 1 0 + +填充过程: +``` + +**步骤详解**: + +``` +i=1, j=1: matrix[0][0]='1' + dp[1][1] = min(dp[0][1], dp[1][0], dp[0][0]) + 1 + = min(0, 0, 0) + 1 = 1 + +i=1, j=2: matrix[0][1]='0' + dp[1][2] = 0(保持默认值) + +i=1, j=3: matrix[0][2]='1' + dp[1][3] = min(0, 0, 0) + 1 = 1 + +i=2, j=1: matrix[1][0]='1' + dp[2][1] = min(1, 0, 0) + 1 = 1 + +i=2, j=3: matrix[1][2]='1' + dp[2][3] = min(dp[1][3], dp[2][2], dp[1][2]) + 1 + = min(1, 0, 0) + 1 = 1 + +i=3, j=3: matrix[2][2]='1' ← 关键位置 + dp[3][3] = min(dp[2][3], dp[3][2], dp[2][2]) + 1 + = min(1, 1, 0) + 1 = 1 + 等等,让我重新检查... + +正确的 DP 表: + + 0 0 0 0 0 0 + 0 1 0 1 0 0 + 0 1 0 1 1 1 + 0 1 1 2 2 2 ← 这里 + 0 1 0 0 1 0 + +i=3, j=3: matrix[2][2]='1' + dp[3][3] = min(dp[2][3], dp[3][2], dp[2][2]) + 1 + = min(1, 1, 0) + 1 = 1 ❌ 错误 + +让我重新计算... +实际上 dp[2][2] 应该是 0(因为 matrix[1][1]='0') +所以 min(1, 1, 0) + 1 = 1 + +但答案说最大是 2×2=4... +让我找找哪里是 2... + +i=3, j=4: matrix[2][3]='1' + dp[3][4] = min(dp[2][4], dp[3][3], dp[2][3]) + 1 + = min(1, 1, 1) + 1 = 2 ✓ +``` + +**最大正方形位置**: +``` +以 (2,3) 为右下角(matrix 索引,从 0 开始) +即 dp[3][4] = 2 +可以形成 2×2 的正方形: + +1 1 +1 1 +``` + +## 代码实现 + +### Go 实现 + +```go +func maximalSquare(matrix [][]byte) int { + if len(matrix) == 0 { + return 0 + } + + m, n := len(matrix), len(matrix[0]) + + // 创建 DP 表,多一行一列作为边界 + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + + maxSide := 0 + + // 填充 DP 表 + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { + // 状态转移 + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + + // 更新最大边长 + if dp[i][j] > maxSide { + maxSide = dp[i][j] + } + } + // 如果是 '0',dp[i][j] 保持为 0 + } + } + + // 返回面积 + return maxSide * maxSide +} + +// 返回三个数中的最小值 func min(a, b, c int) int { if a < b { if a < c { @@ -56,4 +479,280 @@ func min(a, b, c int) int { } ``` -**复杂度:** O(mn) 时间,O(mn) 空间 +### Go 1.21+ 优化版本 + +```go +func maximalSquare(matrix [][]byte) int { + if len(matrix) == 0 { + return 0 + } + + m, n := len(matrix), len(matrix[0]) + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + + maxSide := 0 + + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { + // Go 1.21+ 内置 min 函数 + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + maxSide = max(maxSide, dp[i][j]) + } + } + } + + return maxSide * maxSide +} +``` + +## 常见错误 + +### 错误1:DP 数组大小错误 + +❌ **错误写法**: +```go +dp := make([][]int, m) // ❌ 应该是 m+1 +for i := range dp { + dp[i] = make([]int, n) // ❌ 应该是 n+1 +} +``` + +✅ **正确写法**: +```go +dp := make([][]int, m+1) // ✓ 多一行 +for i := range dp { + dp[i] = make([]int, n+1) // ✓ 多一列 +} +``` + +**原因**: +- 需要 dp[0][*] 和 dp[*][0] 作为边界(值为 0) +- 方便处理第一行和第一列的情况 + +### 错误2:索引偏移错误 + +❌ **错误写法**: +```go +for i := 0; i < m; i++ { // ❌ 索引不对应 + for j := 0; j < n; j++ { + if matrix[i][j] == '1' { + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + // 当 i=0 或 j=0 时会越界! + } + } +} +``` + +✅ **正确写法**: +```go +for i := 1; i <= m; i++ { // ✓ 从 1 开始 + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { // ✓ 索引对应 + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + } + } +} +``` + +**原因**: +- dp 从索引 1 开始,避免越界 +- matrix[i-1][j-1] 对应 dp[i][j] + +### 错误3:忘记检查空矩阵 + +❌ **错误写法**: +```go +func maximalSquare(matrix [][]byte) int { + m, n := len(matrix), len(matrix[0]) // 空矩阵会 panic! + // ... +} +``` + +✅ **正确写法**: +```go +func maximalSquare(matrix [][]byte) int { + if len(matrix) == 0 { // ✓ 先检查 + return 0 + } + m, n := len(matrix), len(matrix[0]) + // ... +} +``` + +**原因**: +- 空矩阵访问 matrix[0] 会 panic +- 需要提前处理边界情况 + +### 错误4:min 函数实现错误 + +❌ **错误写法**: +```go +func min(a, b, c int) int { + if a < b && a < c { // ❌ 逻辑错误 + return a + } + if b < c { + return b + } + return c +} +``` + +✅ **正确写法**: +```go +func min(a, b, c int) int { + if a < b { + if a < c { + return a + } + return c + } + if b < c { + return b + } + return c +} +``` + +**原因**: +- 嵌套判断确保正确找到最小值 +- 第一个条件应该先比较 a 和 b + +## 进阶问题 + +### Q1:空间复杂度能否优化? + +**方案**:只保留两行 DP 值 + +```go +func maximalSquare(matrix [][]byte) int { + if len(matrix) == 0 { + return 0 + } + + m, n := len(matrix), len(matrix[0]) + + // 只需要两行 + prev := make([]int, n+1) + curr := make([]int, n+1) + + maxSide := 0 + + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { + curr[j] = min(prev[j], curr[j-1], prev[j-1]) + 1 + maxSide = max(maxSide, curr[j]) + } else { + curr[j] = 0 + } + } + // 交换行 + prev, curr = curr, prev + } + + return maxSide * maxSide +} +``` + +**空间复杂度**:O(n)(从 O(m×n) 优化到 O(n)) + +### Q2:如果需要返回最大正方形的坐标怎么办? + +**方案**:记录最大正方形的右下角位置 + +```go +func maximalSquareWithPosition(matrix [][]byte) (int, int, int) { + if len(matrix) == 0 { + return 0, -1, -1 + } + + m, n := len(matrix), len(matrix[0]) + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + + maxSide := 0 + maxX, maxY := -1, -1 + + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + if dp[i][j] > maxSide { + maxSide = dp[i][j] + maxX, maxY = i-1, j-1 // 转换为矩阵坐标 + } + } + } + } + + return maxSide * maxSide, maxX, maxY +} +``` + +### Q3:如何找到所有的最大正方形? + +**方案**:先找到最大边长,再遍历 DP 表找出所有位置 + +```go +func allMaximalSquares(matrix [][]byte) [][]int { + if len(matrix) == 0 { + return nil + } + + m, n := len(matrix), len(matrix[0]) + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + + maxSide := 0 + + // 第一遍:找到最大边长 + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if matrix[i-1][j-1] == '1' { + dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + maxSide = max(maxSide, dp[i][j]) + } + } + } + + // 第二遍:收集所有最大正方形的右下角 + var positions [][]int + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if dp[i][j] == maxSide { + positions = append(positions, []int{i-1, j-1}) + } + } + } + + return positions +} +``` + +## 总结 + +**核心要点**: +1. **动态规划**:dp[i][j] 表示以 (i, j) 为右下角的最大正方形边长 +2. **状态转移**:取决于上、左、左上三个位置的最小值 +3. **边界处理**:DP 数组多一圈,避免边界判断 + +**易错点**: +- ❌ DP 数组大小错误(应该是 m+1 × n+1) +- ❌ 索引偏移错误(dp[i][j] 对应 matrix[i-1][j-1]) +- ❌ 忘记检查空矩阵 +- ❌ min 函数实现错误 + +**为什么这样思考?** +- 正方形的扩展需要三个方向都能支持 +- 取最小值体现了"木桶效应" +- DP 避免了暴力解法的重复计算 +- 时间复杂度从 O(m² × n²) 优化到 O(m × n) diff --git a/16-LeetCode Hot 100/最小栈.md b/16-LeetCode Hot 100/最小栈.md index 12fc942..59b72d0 100644 --- a/16-LeetCode Hot 100/最小栈.md +++ b/16-LeetCode Hot 100/最小栈.md @@ -1,21 +1,303 @@ # 最小栈 (Min Stack) +LeetCode 155. Medium + ## 题目描述 设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。 +实现 MinStack 类: +- **MinStack()** 初始化堆栈对象。 +- **void push(int val)** 将元素val推入堆栈。 +- **void pop()** 删除堆栈顶部的元素。 +- **int top()** 获取堆栈顶部的元素。 +- **int getMin()** 获取堆栈中的最小元素。 + +## 思路推导 + +### 暴力解法分析 + +**最直观的思路**: +```go +type MinStack struct { + stack []int +} + +func (this *MinStack) Push(val int) { + this.stack = append(this.stack, val) +} + +func (this *MinStack) GetMin() int { + minVal := this.stack[0] + for _, v := range this.stack { + if v < minVal { + minVal = v + } + } + return minVal +} +``` + +**时间复杂度分析**: +- Push: O(1) +- Pop: O(1) +- Top: O(1) +- **GetMin: O(n)** ❌ 需要遍历整个栈 + +**问题**:题目要求 getMin() 必须在 O(1) 时间内完成,暴力解法无法满足。 + +### 优化思考 + +**观察**:GetMin 操作的瓶颈在于需要每次都遍历找最小值。 + +**关键问题**:能否在每次 push 时就记录当前的最小值? + +**思路1:用一个变量记录最小值** +```go +type MinStack struct { + stack []int + minVal int +} +``` + +**问题**:当最小值被 pop 出栈后,如何快速找到次小值? +- 例如:stack = [2, 1, 3],minVal = 1 +- pop 后:stack = [2, 3],新的 minVal 应该是 2 +- 但我们不知道次小值是多少,需要重新遍历! + +**思路2:使用辅助栈记录历史最小值** ✅ + +核心洞察:**每个元素对应一个"当时的最小值"** + +``` +主栈: [2, 1, 3, 0] +辅助栈: [2, 1, 1, 0] + ↓ ↓ ↓ ↓ +对应关系: 每个位置的辅助栈值记录了"包括该元素及之前所有元素的最小值" +``` + +### 为什么这样思考? + +1. **空间换时间**:用 O(n) 额外空间,让 getMin() 达到 O(1) +2. **同步更新**:主栈和辅助栈同步操作,始终保持对应关系 +3. **冗余存储**:辅助栈存储历史最小值,即使最小值被弹出也能快速回溯 + ## 解题思路 -### 辅助栈 +### 核心思想 -使用两个栈,一个存储所有元素,另一个存储当前最小值。 +**双栈方案**:使用两个栈 +- **主栈 (stack)**:存储所有元素 +- **辅助栈 (minStack)**:存储每个位置对应的最小值 -## Go 代码 +### 算法流程(详细版) + +**步骤1:初始化** +```go +func Constructor() MinStack { + return MinStack{ + stack: []int{}, // 主栈 + minStack: []int{}, // 辅助栈 + } +} +``` + +**步骤2:Push 操作** +```go +func (this *MinStack) Push(val int) { + // 1. 主栈总是推入新值 + this.stack = append(this.stack, val) + + // 2. 辅助栈推入"当前最小值" + if len(this.minStack) == 0 { + // 辅助栈为空,直接推入 + this.minStack = append(this.minStack, val) + } else { + // 辅助栈不为空,比较当前值和之前的min + min := this.minStack[len(this.minStack)-1] + if val < min { + this.minStack = append(this.minStack, val) + } else { + // 推入之前的min,保持栈同步 + this.minStack = append(this.minStack, min) + } + } +} +``` + +**关键点**: +- 为什么辅助栈要推入 min 而不是只推入更小的值? + - 保持两个栈高度一致,便于同步 pop + - 记录历史信息,方便回溯 + +**步骤3:Pop 操作** +```go +func (this *MinStack) Pop() { + // 同步弹出两个栈的顶部元素 + this.stack = this.stack[:len(this.stack)-1] + this.minStack = this.minStack[:len(this.minStack)-1] +} +``` + +**关键点**: +- 为什么两个栈都要 pop? + - 保持栈的对应关系 + - 弹出当前元素后,最小值自然回到上一个状态 + +**步骤4:Top 操作** +```go +func (this *MinStack) Top() int { + return this.stack[len(this.stack)-1] +} +``` + +**步骤5:GetMin 操作** +```go +func (this *MinStack) GetMin() int { + return this.minStack[len(this.minStack)-1] +} +``` + +**关键点**: +- 为什么是 O(1)? + - 直接访问辅助栈顶部,无需遍历 + +### 关键细节说明 + +**细节1:为什么辅助栈要重复存储最小值?** + +``` +操作序列: Push(3), Push(2), Push(1), Push(4) + +主栈: [3] → [3, 2] → [3, 2, 1] → [3, 2, 1, 4] +辅助栈: [3] → [3, 2] → [3, 2, 1] → [3, 2, 1, 1] + ↑ + 不是4,而是之前的min(1) + +为什么? +- Pop(4) 后,min 仍然是 1 +- 如果辅助栈存的是 4,Pop 后就找不到 1 了 +``` + +**细节2:为什么要同步 pop?** + +```go +// 错误做法:只 pop 主栈 +func (this *MinStack) Pop() { + this.stack = this.stack[:len(this.stack)-1] + // 忘记 pop 辅助栈! +} + +// 问题:下次 GetMin() 会返回错误的最小值 +``` + +**细节3:边界条件 - 栈为空时的处理** + +```go +func (this *MinStack) Push(val int) { + this.stack = append(this.stack, val) + + if len(this.minStack) == 0 { // ✅ 必须检查 + this.minStack = append(this.minStack, val) + } else { + // ... + } +} +``` + +### 边界条件分析 + +**边界1:空栈调用 GetMin** +``` +输入: MinStack(), GetMin() +输出: 不保证(题目假设至少有一个元素才调用) +``` + +**边界2:重复元素** +``` +操作: Push(1), Push(1), Push(1) +主栈: [1, 1, 1] +辅助栈: [1, 1, 1] // 每个位置都是1 +GetMin(): 1 ✓ +Pop(): 主栈=[1,1], 辅助栈=[1,1] +GetMin(): 1 ✓ +``` + +**边界3:递减序列** +``` +操作: Push(3), Push(2), Push(1) +主栈: [3, 2, 1] +辅助栈: [3, 2, 1] // 同步递减 +GetMin(): 1 ✓ +Pop(): 主栈=[3,2], 辅助栈=[3,2] +GetMin(): 2 ✓ // 自动回到次小值 +``` + +### 复杂度分析(详细版) + +**时间复杂度**: +``` +- Push: O(1) + - append操作:O(1) 均摊 + - 比较和赋值:O(1) + +- Pop: O(1) + - 切片操作:O(1) + +- Top: O(1) + - 访问数组末尾:O(1) + +- GetMin: O(1) + - 访问数组末尾:O(1) +``` + +**空间复杂度**: +``` +- 主栈:O(n) +- 辅助栈:O(n) +- 总计:O(n) + +为什么是 O(n)? +- 最坏情况:每个元素都需要在辅助栈中存储 +- 例如:递减序列 [3,2,1],辅助栈也是 [3,2,1] +``` + +## 执行过程演示 + +``` +操作序列: Push(-2), Push(0), Push(-3), GetMin, Pop, GetMin + +1. Push(-2) + 主栈: [-2] + 辅助栈: [-2] // 空栈,直接推入 + +2. Push(0) + 主栈: [-2, 0] + 辅助栈: [-2, -2] // -2 < 0,推入 -2 + +3. Push(-3) + 主栈: [-2, 0, -3] + 辅助栈: [-2, -2, -3] // -3 < -2,推入 -3 + +4. GetMin() + 返回: -3 // 辅助栈顶部 + +5. Pop() + 主栈: [-2, 0] + 辅助栈: [-2, -2] // 同步弹出 + +6. GetMin() + 返回: -2 // 自动回到之前的最小值 +``` + +## 代码实现 + +### Go 实现 ```go type MinStack struct { - stack []int - minStack []int + stack []int // 主栈:存储所有元素 + minStack []int // 辅助栈:存储每个位置的最小值 } func Constructor() MinStack { @@ -26,7 +308,63 @@ func Constructor() MinStack { } func (this *MinStack) Push(val int) { + // 主栈总是推入新值 this.stack = append(this.stack, val) + + // 辅助栈推入当前最小值 + if len(this.minStack) == 0 { + // 辅助栈为空,当前值就是最小值 + this.minStack = append(this.minStack, val) + } else { + // 比较当前值和之前的最小值 + min := this.minStack[len(this.minStack)-1] + if val < min { + this.minStack = append(this.minStack, val) + } else { + // 推入之前的最小值,保持同步 + this.minStack = append(this.minStack, min) + } + } +} + +func (this *MinStack) Pop() { + // 同步弹出两个栈 + this.stack = this.stack[:len(this.stack)-1] + this.minStack = this.minStack[:len(this.minStack)-1] +} + +func (this *MinStack) Top() int { + // 返回主栈顶部 + return this.stack[len(this.stack)-1] +} + +func (this *MinStack) GetMin() int { + // 返回辅助栈顶部(当前最小值) + return this.minStack[len(this.minStack)-1] +} +``` + +## 常见错误 + +### 错误1:辅助栈只存储严格更小的值 + +❌ **错误写法**: +```go +func (this *MinStack) Push(val int) { + this.stack = append(this.stack, val) + + if len(this.minStack) == 0 || val < this.minStack[len(this.minStack)-1] { + this.minStack = append(this.minStack, val) + } + // 问题:两个栈高度不一致! +} +``` + +✅ **正确写法**: +```go +func (this *MinStack) Push(val int) { + this.stack = append(this.stack, val) + if len(this.minStack) == 0 { this.minStack = append(this.minStack, val) } else { @@ -34,23 +372,157 @@ func (this *MinStack) Push(val int) { if val < min { this.minStack = append(this.minStack, val) } else { - this.minStack = append(this.minStack, min) + this.minStack = append(this.minStack, min) // 重复存储 + } + } +} +``` + +**原因**: +- 必须保持两个栈同步,才能正确 pop +- 如果辅助栈高度较小,pop 主栈时会越界 + +### 错误2:忘记同步 pop + +❌ **错误写法**: +```go +func (this *MinStack) Pop() { + this.stack = this.stack[:len(this.stack)-1] + // 忘记 pop 辅助栈! +} +``` + +✅ **正确写法**: +```go +func (this *MinStack) Pop() { + this.stack = this.stack[:len(this.stack)-1] + this.minStack = this.minStack[:len(this.minStack)-1] // 同步弹出 +} +``` + +**原因**: +- 不同步会导致辅助栈高度与主栈不一致 +- 后续 GetMin() 会返回错误的最小值 + +### 错误3:空栈未检查 + +❌ **错误写法**: +```go +func (this *MinStack) Push(val int) { + this.stack = append(this.stack, val) + + min := this.minStack[len(this.minStack)-1] // 可能 panic! + if val < min { + this.minStack = append(this.minStack, val) + } else { + this.minStack = append(this.minStack, min) + } +} +``` + +✅ **正确写法**: +```go +func (this *MinStack) Push(val int) { + this.stack = append(this.stack, val) + + if len(this.minStack) == 0 { // 先检查 + this.minStack = append(this.minStack, val) + } else { + min := this.minStack[len(this.minStack)-1] + // ... + } +} +``` + +## 进阶问题 + +### Q1:能否只用一个栈实现? + +**方案**:栈中同时存储值和当前最小值 + +```go +type MinStack struct { + stack []pair +} + +type pair struct { + val int + min int // 当前的最小值 +} + +func (this *MinStack) Push(val int) { + min := val + if len(this.stack) > 0 { + prevMin := this.stack[len(this.stack)-1].min + if val > prevMin { + min = prevMin + } + } + this.stack = append(this.stack, pair{val, min}) +} + +func (this *MinStack) GetMin() int { + return this.stack[len(this.stack)-1].min +} +``` + +**优点**:逻辑更集中 +**缺点**:每个元素需要额外存储 min(空间开销相同) + +### Q2:如果要求空间复杂度优化呢? + +**方案**:差值法(只在 min 变化时存储) + +```go +type MinStack struct { + stack []int + minVal int +} + +func (this *MinStack) Push(val int) { + if len(this.stack) == 0 { + this.stack = append(this.stack, 0) + this.minVal = val + } else { + // 存储差值 + diff := val - this.minVal + this.stack = append(this.stack, diff) + if diff < 0 { + this.minVal = val // 更新最小值 } } } func (this *MinStack) Pop() { + diff := this.stack[len(this.stack)-1] this.stack = this.stack[:len(this.stack)-1] - this.minStack = this.minStack[:len(this.minStack)-1] -} -func (this *MinStack) Top() int { - return this.stack[len(this.stack)-1] + if diff < 0 { + this.minVal = this.minVal - diff // 恢复之前的最小值 + } } func (this *MinStack) GetMin() int { - return this.minStack[len(this.minStack)-1] + return this.minVal } ``` -**复杂度:** 所有操作 O(1) 时间 +**优点**:最坏情况空间仍为 O(n),但平均情况更优 +**缺点**:实现复杂,容易出错 + +## 总结 + +**核心要点**: +1. **空间换时间**:用辅助栈存储历史最小值 +2. **同步操作**:两个栈必须保持高度一致 +3. **冗余存储**:辅助栈会重复存储最小值,这是关键 + +**易错点**: +- ❌ 辅助栈不存储重复的最小值 +- ❌ Pop 时忘记同步弹出辅助栈 +- ❌ 空栈时未检查直接访问 + +**为什么这样设计?** +- 辅助栈的本质是**记录每个状态的最小值** +- 即使最小值被弹出,也能快速回溯到上一个最小值 +- 时间复杂度从 O(n) 优化到 O(1) diff --git a/16-LeetCode Hot 100/柱状图中最大的矩形.md b/16-LeetCode Hot 100/柱状图中最大的矩形.md index cea6544..9283c86 100644 --- a/16-LeetCode Hot 100/柱状图中最大的矩形.md +++ b/16-LeetCode Hot 100/柱状图中最大的矩形.md @@ -1,56 +1,895 @@ # 柱状图中最大的矩形 (Largest Rectangle in Histogram) +LeetCode 84. Hard + ## 题目描述 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。 求在该柱状图中,能够勾勒出来的矩形的最大面积。 +**示例 1**: +``` +输入:heights = [2,1,5,6,2,3] +输出:10 +解释:最大的矩形面积为 10(由高度为 5 和 6 的两个柱子组成) +``` + +**示例 2**: +``` +输入:heights = [2,4] +输出:4 +``` + +## 思路推导 + +### 暴力解法分析 + +**最直观的思路**:枚举所有可能的矩形 + +```go +func largestRectangleArea(heights []int) int { + n := len(heights) + maxArea := 0 + + // 枚举每个柱子作为矩形的高度 + for i := 0; i < n; i++ { + height := heights[i] + + // 向左右扩展,找到能形成的最大宽度 + left := i + for left > 0 && heights[left-1] >= height { + left-- + } + + right := i + for right < n-1 && heights[right+1] >= height { + right++ + } + + width := right - left + 1 + area := height * width + maxArea = max(maxArea, area) + } + + return maxArea +} +``` + +**时间复杂度分析**: +``` +- 外层循环:O(n) 枚举每个柱子 +- 向左扩展:O(n) 最坏情况遍历所有柱子 +- 向右扩展:O(n) 最坏情况遍历所有柱子 +- 总计:O(n) × O(n) = O(n²) +``` + +**问题**: +- 对于严格递增的数组 [1,2,3,4,5],每次都要扩展到边界 +- 时间复杂度 O(n²),大型数组会超时 + +### 优化思考 + +**观察**:暴力解法中,很多比较是重复的。 + +**关键问题**:能否避免重复比较,快速找到每个柱子的边界? + +**思路1:预处理 - 预先计算左右边界** + +```go +// left[i]:柱子 i 向左能扩展到的最远位置 +// right[i]:柱子 i 向右能扩展到的最远位置 + +func largestRectangleArea(heights []int) int { + n := len(heights) + left := make([]int, n) + right := make([]int, n) + + for i := 0; i < n; i++ { + left[i] = i + for left[i] > 0 && heights[left[i]-1] >= heights[i] { + left[i]-- + } + } + + for i := n - 1; i >= 0; i-- { + right[i] = i + for right[i] < n-1 && heights[right[i]+1] >= heights[i] { + right[i]++ + } + } + + maxArea := 0 + for i := 0; i < n; i++ { + area := heights[i] * (right[i] - left[i] + 1) + maxArea = max(maxArea, area) + } + + return maxArea +} +``` + +**问题**:仍然是 O(n²),没有本质优化 + +**思路2:单调栈优化** ✅ + +核心洞察:**利用单调递增栈,可以在 O(1) 时间内确定每个柱子的右边界** + +### 为什么这样思考? + +**关键观察**: +``` +对于柱子 i,它的右边界是: +第一个高度小于 heights[i] 的柱子的位置 - 1 + +例如:[2, 1, 5, 6, 2, 3] + ↑ + 柱子 2(高度 6) + 右边界是柱子 4(高度 2)的位置 - 1 = 3 + 因为 heights[4] = 2 < 6 +``` + +**单调栈的作用**: +- **维护一个递增的序列**:栈中柱子的高度严格递增 +- **快速找到边界**:当遇到比栈顶小的柱子时,就找到了栈顶元素的右边界 +- **避免重复比较**:每个柱子最多入栈一次、出栈一次 + +**为什么是单调递增?** +- 递增保证了:栈顶元素的右边界就是当前遇到的第一个更小的柱子 +- 如果栈中元素不递增,就无法保证这一点 + ## 解题思路 -### 方法一:单调栈(推荐) +### 核心思想 -**核心思想:**使用单调递增栈,存储柱子的索引。当遇到比栈顶小的柱子时,弹出栈顶并计算面积。 +**单调递增栈**: +- 栈中存储柱子的**索引**(不是高度) +- 栈中对应的柱子高度**严格递增** +- 当遇到比栈顶小的柱子时,弹出栈顶并计算面积 + +### 算法流程(详细版) + +**步骤1:初始化栈和遍历** + +```go +stack := []int{} // 单调递增栈,存储索引 +maxArea := 0 +n := len(heights) + +for i := 0; i <= n; i++ { // 注意:遍历到 n(包含虚拟柱子) + h := 0 + if i < n { + h = heights[i] + } + // ... +} +``` + +**关键点**: +- 为什么遍历到 n(而不是 n-1)? + - 需要一个"虚拟柱子"高度为 0 + - 强制弹出栈中所有剩余元素并计算面积 + - 否则栈中元素会遗漏计算 + +**步骤2:维护单调递增栈** + +```go +for len(stack) > 0 && h < heights[stack[len(stack)-1]] { + // 栈顶元素的高度比当前柱子大 + // 找到了栈顶元素的右边界(当前柱子 i) + + height := heights[stack[len(stack)-1]] + stack = stack[:len(stack)-1] // 弹出栈顶 + + // 计算宽度 + width := i + if len(stack) > 0 { + width = i - stack[len(stack)-1] - 1 + } + + area := height * width + maxArea = max(maxArea, area) +} + +stack = append(stack, i) // 当前柱子入栈 +``` + +**关键点解释**: + +**Q1:为什么条件是 `h < heights[stack[len(stack)-1]]`?** + +``` +当遇到比栈顶小的柱子时: +- 栈顶元素的右边界确定了(当前柱子 i 的位置) +- 因为当前柱子是第一个比栈顶小的柱子 +- 弹出栈顶并计算面积 + +示例:[2, 1, 5, 6, 2, 3] + ↑ + i=4, h=2 + +栈:[2, 3](索引,对应高度 [5, 6]) + +比较:h(2) < heights[3](6) ✓ +- 弹出索引 3(高度 6) +- 右边界 = 4 - 1 = 3 +- 计算面积:6 × 宽度 +``` + +**Q2:宽度如何计算?** + +``` +情况1:栈为空 +width = i +原因:弹出后栈为空,说明这是最小的柱子,可以扩展到最左边 + +情况2:栈不为空 +width = i - stack[len(stack)-1] - 1 +原因: +- stack[len(stack)-1] 是弹出后新的栈顶 +- 新栈顶的右边界 + 1 到 i-1 是弹出的柱子能扩展的范围 +- 减 1 是因为不包括新栈顶的位置 + +示例:[2, 1, 5, 6, 2, 3] +索引: 0 1 2 3 4 5 + +弹出索引 3(高度 6)后: +- 栈变为 [2](索引 2,高度 5) +- width = 4 - 2 - 1 = 1 +- 只有索引 3 这一个柱子能形成高度为 6 的矩形 +``` + +**Q3:为什么最后要入栈?** + +```go +stack = append(stack, i) +``` + +``` +当前柱子入栈的原因: +1. 可能是未来某个柱子的左边界 +2. 还没有找到它的右边界 +3. 保持单调性,等待后续处理 +``` + +**步骤3:完整流程示例** + +``` +输入:[2, 1, 5, 6, 2, 3] +索引: 0 1 2 3 4 5 + +i=0, h=2: + 栈:[] → [0] + (空栈,直接入栈) + +i=1, h=1: + 栈:[0] + 比较:1 < heights[0](2) ✓ + 弹出 0: + height = 2 + width = 1(栈为空) + area = 2 × 1 = 2 + 栈:[] → [1] + +i=2, h=5: + 栈:[1] + 比较:5 > heights[1](1) ✗ + 直接入栈 + 栈:[1, 2] + +i=3, h=6: + 栈:[1, 2] + 比较:6 > heights[2](5) ✗ + 直接入栈 + 栈:[1, 2, 3] + +i=4, h=2: + 栈:[1, 2, 3] + 比较:2 < heights[3](6) ✓ + 弹出 3: + height = 6 + width = 4 - 2 - 1 = 1 + area = 6 × 1 = 6 + + 比较:2 < heights[2](5) ✓ + 弹出 2: + height = 5 + width = 4 - 1 - 1 = 2 + area = 5 × 2 = 10 ← 当前最大 + + 比较:2 > heights[1](1) ✗ + 停止弹出 + 栈:[1] → [1, 4] + +i=5, h=3: + 栈:[1, 4] + 比较:3 > heights[4](2) ✗ + 直接入栈 + 栈:[1, 4, 5] + +i=6, h=0(虚拟柱子): + 栈:[1, 4, 5] + 依次弹出所有元素并计算... + +最大面积:10 +``` + +### 关键细节说明 + +**细节1:为什么需要虚拟柱子?** + +```go +for i := 0; i <= n; i++ { // 注意 i <= n + h := 0 + if i < n { + h = heights[i] + } + // ... +} +``` + +``` +没有虚拟柱子的问题: +输入:[2, 3, 4](严格递增) +栈:[0, 1, 2] + +遍历结束后,栈中还有 [0, 1, 2] +这些元素没有找到右边界! +需要额外处理剩余元素 + +有虚拟柱子的好处: +i=3, h=0 会强制弹出所有元素 +无需额外处理 +``` + +**细节2:为什么栈中存储索引而不是高度?** + +```go +stack := []int{} // 存储索引 +// 而不是 []int{heights[0], heights[1], ...} +``` + +``` +存储索引的原因: +1. 需要通过索引计算宽度 +2. 索引是唯一的,高度可能重复 +3. 方便确定位置关系 + +示例:[2, 2, 2] +如果存储高度:栈 = [2, 2, 2] 无法区分 +如果存储索引:栈 = [0, 1, 2] 清晰明确 +``` + +**细节3:为什么是严格递增而不是非递减?** + +```go +// 条件:h < heights[stack[len(stack)-1]] +// 使用 < 而不是 <= + +// 如果使用 <= 会怎样? +for len(stack) > 0 && h <= heights[stack[len(stack)-1]] { + // 会弹出相等的元素 +} +``` + +``` +使用 < 的原因: +- 相等高度的柱子可以形成更大的矩形 +- 弹出相等的元素会错过可能的更大面积 + +示例:[2, 2] +使用 <:栈 = [0, 1] + i=2(虚拟柱子)弹出 1, 0 + 最大面积 = 2 × 2 = 4 ✓ + +使用 <=:i=1 弹出 0(错误!) + 栈 = [1] + 最大面积 = 2 × 1 = 2 ✗ +``` + +### 边界条件分析 + +**边界1:空数组** +``` +输入:heights = [] +输出:0 +处理:循环不执行,maxArea = 0 +``` + +**边界2:单个元素** +``` +输入:heights = [5] +输出:5 +过程: +i=0: 栈 = [0] +i=1: 弹出 0,area = 5 × 1 = 5 +maxArea = 5 ✓ +``` + +**边界3:所有元素相同** +``` +输入:heights = [3, 3, 3] +输出:9 +过程: +i=0: 栈 = [0] +i=1: 栈 = [0, 1] +i=2: 栈 = [0, 1, 2] +i=3: 依次弹出 + 弹出 2: area = 3 × 1 = 3 + 弹出 1: area = 3 × 2 = 6 + 弹出 0: area = 3 × 3 = 9 ✓ +maxArea = 9 +``` + +**边界4:严格递增** +``` +输入:heights = [1, 2, 3, 4] +输出:6(不是 10!) +过程: +最大面积是 3×2=6(高度 3,宽度 2) +或者 2×3=6(高度 2,宽度 3) + +无法形成 4×4 的矩形,因为高度不够 +``` + +**边界5:严格递减** +``` +输入:heights = [4, 3, 2, 1] +输出:6 +过程: +每个柱子都会立即弹出 +最大面积 = 4 × 1 = 4 +或者 3 × 2 = 6(高度 3,从索引 1 到 2) +``` + +### 复杂度分析(详细版) + +**时间复杂度**: +``` +- 外层循环:O(n+1) ≈ O(n) +- 内层 while 循环:看似 O(n),实际上总计 O(n) + +为什么内层循环总计 O(n)? +- 每个索引最多入栈 1 次 +- 每个索引最多出栈 1 次 +- 总操作次数 = 2n = O(n) + +总计:O(n) + O(n) = O(n) +``` + +**空间复杂度**: +``` +- 栈空间:O(n) 最坏情况 +- 其他变量:O(1) +- 总计:O(n) + +最坏情况:严格递增数组 +栈会存储所有索引 +``` + +## 执行过程演示 + +**示例**:heights = [2, 1, 5, 6, 2, 3] + +``` +图示: + █ + █ █ + █ █ █ +█ █ █ █ █ +█ █ █ █ █ +0 1 2 3 4 5 + +步骤详解: + +Step 0: 初始化 +stack = [] +maxArea = 0 + +Step 1: i=0, heights[0]=2 +stack = [] → [0] +(空栈,直接入栈) + +Step 2: i=1, heights[1]=1 +stack = [0] +弹出 0(高度 2): + 宽度 = 1(栈为空) + 面积 = 2 × 1 = 2 +stack = [1] +maxArea = 2 + +Step 3: i=2, heights[2]=5 +stack = [1] → [1, 2] +(5 > 1,直接入栈) + +Step 4: i=3, heights[3]=6 +stack = [1, 2] → [1, 2, 3] +(6 > 5,直接入栈) + +Step 5: i=4, heights[4]=2 +stack = [1, 2, 3] + +弹出 3(高度 6): + 宽度 = 4 - 2 - 1 = 1 + 面积 = 6 × 1 = 6 + +弹出 2(高度 5): + 宽度 = 4 - 1 - 1 = 2 + 面积 = 5 × 2 = 10 ← 最大 + +stack = [1] → [1, 4] +maxArea = 10 + +Step 6: i=5, heights[5]=3 +stack = [1, 4] → [1, 4, 5] +(3 > 2,直接入栈) + +Step 7: i=6(虚拟柱子,h=0) +stack = [1, 4, 5] + +弹出 5(高度 3): + 宽度 = 6 - 4 - 1 = 1 + 面积 = 3 × 1 = 3 + +弹出 4(高度 2): + 宽度 = 6 - 1 - 1 = 4 + 面积 = 2 × 4 = 8 + +弹出 1(高度 1): + 宽度 = 6(栈为空) + 面积 = 1 × 6 = 6 + +stack = [] +maxArea = 10(不变) + +最终结果:10 +``` ## 代码实现 ### Go 实现 ```go -package main - func largestRectangleArea(heights []int) int { - stack := []int{} - maxArea := 0 - n := len(heights) + stack := []int{} // 单调递增栈,存储索引 + maxArea := 0 + n := len(heights) - for i := 0; i <= n; i++ { - h := 0 - if i < n { - h = heights[i] - } + // 遍历所有柱子,包括一个虚拟的末尾柱子(高度为 0) + for i := 0; i <= n; i++ { + h := 0 + if i < n { + h = heights[i] + } - for len(stack) > 0 && h < heights[stack[len(stack)-1]] { - height := heights[stack[len(stack)-1]] - stack = stack[:len(stack)-1] + // 当前柱子比栈顶小,弹出栈顶并计算面积 + for len(stack) > 0 && h < heights[stack[len(stack)-1]] { + height := heights[stack[len(stack)-1]] + stack = stack[:len(stack)-1] // 弹出栈顶 - width := i - if len(stack) > 0 { - width = i - stack[len(stack)-1] - 1 - } + // 计算宽度 + width := i + if len(stack) > 0 { + width = i - stack[len(stack)-1] - 1 + } - area := height * width - if area > maxArea { - maxArea = area - } - } + area := height * width + if area > maxArea { + maxArea = area + } + } - stack = append(stack, i) - } + // 当前柱子入栈 + stack = append(stack, i) + } - return maxArea + return maxArea } ``` -- LeetCode 85: 最大矩形(二维版本) -- LeetCode 42: 接雨水 +### 带详细注释的版本 + +```go +func largestRectangleArea(heights []int) int { + // 单调递增栈:存储柱子的索引 + // 栈中对应的高度严格递增 + stack := []int{} + + maxArea := 0 + n := len(heights) + + // 遍历到 n(包含虚拟柱子) + for i := 0; i <= n; i++ { + // 当前柱子的高度 + // 虚拟柱子的高度为 0,用于清空栈 + h := 0 + if i < n { + h = heights[i] + } + + // 维护单调递增栈 + // 如果当前柱子比栈顶小,说明找到了栈顶的右边界 + for len(stack) > 0 && h < heights[stack[len(stack)-1]] { + // 弹出栈顶,计算以该柱子为高的最大矩形 + top := stack[len(stack)-1] + height := heights[top] + stack = stack[:len(stack)-1] + + // 计算宽度 + // 如果栈为空,说明当前是最小的柱子,可以扩展到最左边 + // 如果栈不为空,宽度 = 当前位置 - 新栈顶位置 - 1 + width := i + if len(stack) > 0 { + width = i - stack[len(stack)-1] - 1 + } + + // 计算面积并更新最大值 + area := height * width + if area > maxArea { + maxArea = area + } + } + + // 当前柱子入栈 + stack = append(stack, i) + } + + return maxArea +} +``` + +## 常见错误 + +### 错误1:忘记虚拟柱子 + +❌ **错误写法**: +```go +for i := 0; i < n; i++ { // ❌ 只遍历到 n-1 + h := heights[i] + // ... +} +// 栈中可能还有元素未处理! +``` + +✅ **正确写法**: +```go +for i := 0; i <= n; i++ { // ✓ 遍历到 n + h := 0 + if i < n { + h = heights[i] + } + // ... +} +``` + +**原因**: +- 虚拟柱子强制弹出所有剩余元素 +- 避免栈中元素遗漏计算 + +### 错误2:宽度计算错误 + +❌ **错误写法**: +```go +width = i - stack[len(stack)-1] // ❌ 没有减 1 +``` + +✅ **正确写法**: +```go +width = i - stack[len(stack)-1] - 1 // ✓ 正确计算 +``` + +**原因**: +- stack[len(stack)-1] 是弹出后新的栈顶 +- 新栈顶的位置不包含在宽度范围内 +- 必须减 1 + +### 错误3:比较条件错误 + +❌ **错误写法**: +```go +for len(stack) > 0 && h <= heights[stack[len(stack)-1]] { + // ❌ 使用 <= 会弹出相等的元素 +} +``` + +✅ **正确写法**: +```go +for len(stack) > 0 && h < heights[stack[len(stack)-1]] { + // ✓ 只弹出严格更大的元素 +} +``` + +**原因**: +- 相等高度的柱子可以合并成更大的矩形 +- 提前弹出会错过最优解 + +### 错误4:忘记处理空栈情况 + +❌ **错误写法**: +```go +width = i - stack[len(stack)-1] - 1 // ❌ 空栈会越界 +``` + +✅ **正确写法**: +```go +width := i +if len(stack) > 0 { + width = i - stack[len(stack)-1] - 1 +} +``` + +**原因**: +- 空栈说明当前是最小的柱子 +- 可以扩展到最左边(索引 0) + +## 进阶问题 + +### Q1:空间复杂度能否优化? + +**答案**:不能,单调栈本身就需要 O(n) 空间。 + +但是可以用数组模拟栈,避免动态分配: +```go +func largestRectangleArea(heights []int) int { + n := len(heights) + stack := make([]int, n+1) // 预分配 + top := -1 // 栈顶指针 + + maxArea := 0 + + for i := 0; i <= n; i++ { + h := 0 + if i < n { + h = heights[i] + } + + for top >= 0 && h < heights[stack[top]] { + height := heights[stack[top]] + top-- + + width := i + if top >= 0 { + width = i - stack[top] - 1 + } + + area := height * width + if area > maxArea { + maxArea = area + } + } + + top++ + stack[top] = i + } + + return maxArea +} +``` + +### Q2:如何找到最大矩形的位置? + +**方案**:记录最大矩形的左右边界 + +```go +func largestRectangleAreaWithPosition(heights []int) (int, int, int) { + stack := []int{} + maxArea := 0 + left, right := -1, -1 + n := len(heights) + + for i := 0; i <= n; i++ { + h := 0 + if i < n { + h = heights[i] + } + + for len(stack) > 0 && h < heights[stack[len(stack)-1]] { + top := stack[len(stack)-1] + height := heights[top] + stack = stack[:len(stack)-1] + + l := 0 + if len(stack) > 0 { + l = stack[len(stack)-1] + 1 + } + r := i - 1 + + area := height * (r - l + 1) + if area > maxArea { + maxArea = area + left, right = l, r + } + } + + stack = append(stack, i) + } + + return maxArea, left, right +} +``` + +### Q3:这道题和"接雨水"有什么区别? + +**接雨水(LeetCode 42)**: +- 找左右两边**更大**的柱子 +- 使用单调**递减**栈 +- 计算的是"凹槽"的面积 + +**最大矩形**: +- 找左右边**更小**的柱子 +- 使用单调**递增**栈 +- 计算的是矩形面积 + +### Q4:如何处理高度非常大的情况? + +**答案**:使用 64 位整数避免溢出 + +```go +func largestRectangleArea(heights []int) int64 { + stack := []int{} + var maxArea int64 = 0 + n := len(heights) + + for i := 0; i <= n; i++ { + h := 0 + if i < n { + h = heights[i] + } + + for len(stack) > 0 && h < heights[stack[len(stack)-1]] { + height := int64(heights[stack[len(stack)-1]]) + stack = stack[:len(stack)-1] + + var width int64 = int64(i) + if len(stack) > 0 { + width = int64(i - stack[len(stack)-1] - 1) + } + + area := height * width + if area > maxArea { + maxArea = area + } + } + + stack = append(stack, i) + } + + return maxArea +} +``` + +## 相关题目 + +1. **LeetCode 85. 最大矩形** + - 二维版本,每行看作柱状图 + - 先预处理每列的连续 1 的个数 + - 对每行调用本题的解法 + +2. **LeetCode 42. 接雨水** + - 单调递减栈 + - 找更大的柱子形成凹槽 + +3. **LeetCode 739. 每日温度** + - 单调递减栈 + - 找下一个更大的元素 + +## 总结 + +**核心要点**: +1. **单调递增栈**:维护一个递增的索引序列 +2. **右边界确定**:遇到更小的柱子时,栈顶元素的右边界确定 +3. **虚拟柱子**:高度为 0,强制清空栈 +4. **宽度计算**:根据栈的状态动态计算 + +**易错点**: +- ❌ 忘记虚拟柱子,导致栈中元素未处理 +- ❌ 宽度计算错误(忘记减 1) +- ❌ 比较条件错误(使用 <= 而不是 <) +- ❌ 空栈情况未处理 + +**为什么这样思考?** +- 单调栈本质上是在"延迟计算" +- 每个柱子入栈后,等待找到它的右边界 +- 一旦找到右边界,立即计算该柱子能形成的最大矩形 +- 每个柱子只处理一次,时间复杂度 O(n) +- 暴力解法的 O(n²) 优化到 O(n)