栈
1.栈核心特性与操作详解
在 LeetCode Hot 100 中,栈是解决高频问题的核心数据结构之一。其 "后进先出(LIFO)" 的特性使其在处理嵌套结构 、单调序列 、临时缓存等场景中表现优异。本文结合 Hot 100 中的经典题目,详解栈的核心特性、操作及实战应用。
一、栈的核心特性
栈是一种 "操作受限" 的线性数据结构,仅允许在一端(栈顶)进行插入和删除操作,核心特性如下:
- 后进先出(LIFO)
最后入栈的元素最先出栈。例如:入栈顺序为a→b→c,出栈顺序必为c→b→a。
✅ 典型应用:括号匹配(最近的左括号需优先匹配对应的右括号)、函数调用栈(最后调用的函数最先返回)。 - 操作受限
仅支持三种基础操作:push(x):将元素x压入栈顶;pop():移除并返回栈顶元素(栈空时操作会报错);peek():返回栈顶元素(不删除,栈空时返回null或报错)。
这种受限特性降低了逻辑复杂度,适合解决具有明确 "先后依赖" 的问题。
- O (1) 时间复杂度
上述基础操作的时间复杂度均为O(1),适合高频读写场景。
二、栈的基础操作与实现
在算法题中,栈通常通过编程语言自带的集合类实现(如 Java 的 Deque、Python 的 list),也可手动实现(链表或数组)。
1. 基础实现(以数组为例)
java
class Stack {
private int[] arr;
private int top; // 栈顶指针(-1 表示栈空)
public Stack(int capacity) {
arr = new int[capacity];
top = -1;
}
public void push(int x) {
if (top == arr.length - 1) throw new RuntimeException("栈满");
arr[++top] = x; // 先移动指针,再存值
}
public int pop() {
if (top == -1) throw new RuntimeException("栈空");
return arr[top--]; // 先取值,再移动指针
}
public int peek() {
if (top == -1) throw new RuntimeException("栈空");
return arr[top];
}
public boolean isEmpty() {
return top == -1;
}
}
2. 编程语言自带工具
- Java :推荐用
Deque<Integer> stack = new LinkedList<>()(push()入栈,pop()出栈,peek()取栈顶)。 - Python :用列表模拟(
append(x)入栈,pop()出栈,stack[-1]取栈顶)。
三、Hot 100 中栈的核心应用场景
栈在 Hot 100 中的高频考点可归纳为 4 类,结合具体题目解析如下:
场景 1:括号匹配问题(利用 LIFO 特性)
核心逻辑 :左括号入栈,右括号与栈顶左括号匹配(栈顶是 "最近未匹配" 的左括号)。
典型题目 :20. 有效的括号
解题步骤:
- 遍历字符串,遇到左括号(
(/{/[)则入栈; - 遇到右括号时:
- 若栈空(无匹配的左括号),直接返回
false; - 弹出栈顶元素,检查是否为对应的左括号(如
)对应(),不匹配则返回false;
- 若栈空(无匹配的左括号),直接返回
- 遍历结束后,栈必须为空(所有左括号均匹配)。
关键代码:
java
public boolean isValid(String s) {
Deque<Character> stack = new LinkedList<>();
Map<Character, Character> map = new HashMap<>() {{
put(')', '('); put('}', '{'); put(']', '[');
}};
for (char c : s.toCharArray()) {
if (map.containsKey(c)) { // 右括号
if (stack.isEmpty() || stack.pop() != map.get(c)) return false;
} else { // 左括号
stack.push(c);
}
}
return stack.isEmpty();
}
场景 2:单调栈(解决 "Next Greater Element" 类问题)
核心逻辑 :维持栈内元素的单调性(递增或递减),快速找到 "下一个更大 / 更小元素"。
典型题目 :739. 每日温度、42. 接雨水、84. 柱状图中最大的矩形
例:每日温度(找下一个更大元素的距离)
问题 :给定温度数组,返回每个元素距离下一个更高温度的天数。
思路 :用单调递减栈存储温度索引,遍历数组时:
- 若当前温度 > 栈顶索引对应的温度,说明栈顶元素的 "下一个更高温度" 是当前元素,计算距离并弹出栈顶;
- 否则,将当前索引入栈(维持递减性)。
关键代码:
java
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Deque<Integer> stack = new LinkedList<>(); // 存储索引,栈内温度递减
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int prev = stack.pop(); // 弹出栈顶(找到下一个更高温度)
res[prev] = i - prev;
}
stack.push(i); // 当前索引入栈
}
return res; // 未找到的元素默认是0
}
场景 3:栈实现特殊数据结构
核心逻辑 :用栈模拟队列、或设计带额外功能的栈(如 "最小栈")。
典型题目 :155. 最小栈
问题 :设计一个栈,支持 push、pop、top 操作,并能在常数时间内检索到最小元素。
思路 :用辅助栈存储当前栈中的最小值:
- 主栈存储所有元素,辅助栈存储 "截止当前主栈元素的最小值";
- 入栈时,辅助栈压入 "当前值与辅助栈顶最小值的较小者";
- 出栈时,两栈同时弹出,确保辅助栈顶始终是当前主栈的最小值。
关键代码:
java
class MinStack {
private Deque<Integer> mainStack;
private Deque<Integer> minStack; // 辅助栈,存储最小值
public MinStack() {
mainStack = new LinkedList<>();
minStack = new LinkedList<>();
minStack.push(Integer.MAX_VALUE); // 初始化哨兵
}
public void push(int val) {
mainStack.push(val);
minStack.push(Math.min(val, minStack.peek())); // 辅助栈压入当前最小值
}
public void pop() {
mainStack.pop();
minStack.pop();
}
public int top() {
return mainStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
场景 4:逆波兰表达式求值(栈作为临时缓存)
核心逻辑 :用栈缓存操作数,遇到运算符时弹出栈顶两个元素计算,结果入栈。
典型题目 :150. 逆波兰表达式求值
问题 :计算逆波兰表达式(后缀表达式)的值,如 ["2","1","+","3","*"] 对应 (2+1)*3=9。
思路:
- 遍历表达式,遇到数字则入栈;
- 遇到运算符(
+/-/*//),弹出栈顶两个元素(b先弹出,a后弹出),计算a op b,结果入栈; - 遍历结束后,栈顶元素即为结果。
关键代码:
java
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList<>();
for (String s : tokens) {
if (s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")) {
int b = stack.pop();
int a = stack.pop();
switch (s) {
case "+": stack.push(a + b); break;
case "-": stack.push(a - b); break;
case "*": stack.push(a * b); break;
case "/": stack.push(a / b); break;
}
} else {
stack.push(Integer.parseInt(s));
}
}
return stack.pop();
}
四、栈的解题技巧总结
- 识别适用场景 :
- 涉及 "嵌套结构"(如括号、HTML 标签)→ 用栈的 LIFO 特性匹配最近元素;
- 需找 "下一个更大 / 更小元素"→ 用单调栈(时间复杂度
O(n)); - 需临时缓存中间结果(如表达式求值)→ 用栈存储操作数 / 中间值。
- 单调栈的细节 :
- 明确栈内元素是 "索引" 还是 "值"(索引通常用于计算距离或范围);
- 确定单调性(递增 / 递减):找 "下一个更大元素" 用递减栈,找 "下一个更小元素" 用递增栈。
- 边界处理 :
- 操作前检查栈是否为空(避免
pop()或peek()报错); - 用 "哨兵" 简化边界判断(如最小栈初始化时压入
Integer.MAX_VALUE)。
- 操作前检查栈是否为空(避免
通过掌握栈的 LIFO 特性和单调栈技巧,可高效解决 Hot 100 中 10% 以上的高频问题,尤其是中等难度的嵌套结构和序列分析题。
2.20. 有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
**输入:**s = "()"
**输出:**true
有效的括号问题是栈的经典应用场景。判断字符串是否有效的核心在于验证++括号的匹配关系和嵌套顺序,栈的 "后进先出" 特性恰++好适合处理这种嵌套结构。
一、核心解法:栈匹配法
(一)思路概述
-
栈的作用:用于存储遇到的左括号,当遇到右括号时,弹出栈顶元素并检查是否匹配。
-
匹配规则:
- 每遇到一个左括号(
(、{、[),将其压入栈中。 - 每遇到一个右括号(
)、}、]),检查栈是否为空(为空则无匹配的左括号,无效)。 - 若栈不为空,弹出栈顶元素,判断是否为对应的左括号(如
)对应()。
- 每遇到一个左括号(
-
最终检查:遍历结束后,栈必须为空(所有左括号都有匹配的右括号)。
(二)具体步骤
- 初始化栈:用于存储左括号。
- 遍历字符串:
- 若当前字符是左括号,压入栈中。
- 若当前字符是右括号:
- 栈为空 → 无效(返回
false)。 - 弹出栈顶元素,检查是否匹配 → 不匹配则返回
false。
- 栈为空 → 无效(返回
- 遍历结束后:
- 栈为空 → 所有括号匹配,返回
true。 - 栈不为空 → 存在未匹配的左括号,返回
false。
- 栈为空 → 所有括号匹配,返回
(三)代码实现(使用栈和哈希表)
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
class Solution {
public boolean isValid(String s) {
// 哈希表存储右括号与对应左括号的映射
Map<Character, Character> map = new HashMap<>();
map.put(')', '(');
map.put('}', '{');
map.put(']', '[');
// 栈用于存储左括号
Deque<Character> stack = new LinkedList<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (map.containsKey(c)) {
// 当前字符是右括号,需匹配栈顶左括号
if (stack.isEmpty()) {
return false; // 无匹配的左括号
}
// 弹出栈顶元素并检查是否匹配
if (stack.pop() != map.get(c)) {
return false;
}
} else {
// 当前字符是左括号,压入栈中
stack.push(c);
}
}
// 遍历结束后栈必须为空(所有左括号都有匹配)
return stack.isEmpty();
}
}
(四)执行流程示例
以 s = "()[]{}" 为例:
- 字符
'('是左括号 → 栈:[ '(' ]。 - 字符
')'是右括号 → 栈顶弹出'(',匹配 → 栈为空。 - 字符
'['是左括号 → 栈:[ '[' ]。 - 字符
']'是右括号 → 栈顶弹出'[',匹配 → 栈为空。 - 字符
'{'是左括号 → 栈:[ '{' ]。 - 字符
'}'是右括号 → 栈顶弹出'{',匹配 → 栈为空。 - 遍历结束,栈为空 → 返回
true。
以 s = "([)]" 为例:
- 字符
'('→ 栈:[ '(' ]。 - 字符
'['→ 栈:[ '(', '[' ]。 - 字符
')'→ 栈顶弹出'[',与')'不匹配 → 返回false。
二、优化解法:简化栈操作(无哈希表)
(一)思路概述
省去哈希表,直接通过条件判断处理括号匹配:
- 遇到右括号时,检查栈顶是否为对应的左括号,若不匹配则返回
false。 - 其他逻辑与栈匹配法一致。
(二)代码实现
import java.util.Deque;
import java.util.LinkedList;
class Solution {
public boolean isValid(String s) {
Deque<Character> stack = new LinkedList<>();
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(')'); // 压入对应的右括号,简化后续判断
} else if (c == '{') {
stack.push('}');
} else if (c == '[') {
stack.push(']');
} else {
// 右括号:检查栈顶是否匹配
if (stack.isEmpty() || stack.pop() != c) {
return false;
}
}
}
return stack.isEmpty();
}
}
(三)逻辑解析
- 压栈时直接存储左括号对应的右括号(如
'('对应压入')')。 - 遇到右括号时,直接与栈顶元素比较(若相等则匹配,否则无效)。
- 该方法通过提前存储对应右括号,减少了判断次数,代码更简洁。
三、两种解法对比
| 解法 | 核心实现 | 时间复杂度 | 空间复杂度 | 优势 |
|---|---|---|---|---|
| 栈 + 哈希表 | 哈希表存储映射 | O(n) | O(n) | 逻辑清晰,易扩展 |
| 简化栈操作 | 直接压入对应右括号 | O(n) | O(n) | 代码更简洁,减少判断次数 |
两种方法时间复杂度均为 O(n)(遍历字符串一次),空间复杂度均为 O(n)(最坏情况下栈存储所有左括号)。
总结
有效的括号问题的核心是利用栈的后进先出特性处理嵌套结构:
- 左括号入栈,右括号与栈顶左括号匹配,不匹配则无效。
- 遍历结束后栈必须为空,确保所有左括号都有对应的右括号。
两种解法均能高效解决问题,简化版通过提前存储对应右括号使代码更简洁,而哈希表版更直观,适合理解基本思路。掌握这种栈匹配思想,可解决类似的嵌套结构验证问题(如 HTML 标签匹配)。
3.155. 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack()初始化堆栈对象。void push(int val)将元素val推入堆栈。void pop()删除堆栈顶部的元素。int top()获取堆栈顶部的元素。int getMin()获取堆栈中的最小元素。
示例 1:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
- 最小栈:解法与实现思路
最小栈问题要求设计一个支持常规栈操作(push、pop、top),并能在常数时间内获取最小值的栈。核心挑战是如何高效维护 "最小值" 信息,同时保证所有操作的时间复杂度为 O(1)。
一、核心解法:辅助栈(双栈法)
(一)思路概述
使用两个栈:
- 主栈 :存储所有元素,负责常规的
push、pop、top操作。 - 辅助栈 :专门存储 "当前主栈中的最小值",与主栈同步操作,确保
getMin()能直接返回辅助栈的栈顶元素。
辅助栈的维护规则:
push时:若新元素val小于等于辅助栈顶元素(或辅助栈为空),则将val压入辅助栈(更新当前最小值);否则不压入(当前最小值不变)。pop时:若主栈弹出的元素等于辅助栈顶元素(说明弹出的是当前最小值),则辅助栈也弹出栈顶(更新最小值为前一个最小值);否则辅助栈不操作。
(二)代码实现
import java.util.Deque;
import java.util.LinkedList;
class MinStack {
// 主栈:存储所有元素
private Deque<Integer> mainStack;
// 辅助栈:存储当前最小值
private Deque<Integer> minStack;
public MinStack() {
mainStack = new LinkedList<>();
minStack = new LinkedList<>();
}
public void push(int val) {
// 主栈正常压入元素
mainStack.push(val);
// 辅助栈:若为空或新元素小于等于当前最小值,则压入
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
// 主栈弹出栈顶元素
int top = mainStack.pop();
// 若弹出的元素是当前最小值,辅助栈也弹出
if (top == minStack.peek()) {
minStack.pop();
}
}
public int top() {
// 返回主栈栈顶元素
return mainStack.peek();
}
public int getMin() {
// 返回辅助栈栈顶元素(当前最小值)
return minStack.peek();
}
}
(三)执行流程示例
以操作序列 push(-2) → push(0) → push(-3) → getMin() → pop() → top() → getMin() 为例:
push(-2):- 主栈:
[-2] - 辅助栈为空,压入
-2→ 辅助栈:[-2]
- 主栈:
push(0):- 主栈:
[-2, 0] 0 > -2(辅助栈顶),辅助栈不操作 → 辅助栈:[-2]
- 主栈:
push(-3):- 主栈:
[-2, 0, -3] -3 <= -2,压入-3→ 辅助栈:[-2, -3]
- 主栈:
getMin():返回辅助栈顶-3(正确)。pop():- 主栈弹出
-3→ 主栈:[-2, 0] - 弹出的
-3等于辅助栈顶,辅助栈弹出 → 辅助栈:[-2]
- 主栈弹出
top():返回主栈顶0(正确)。getMin():返回辅助栈顶-2(正确)。
二、优化解法:单栈存储 "值与当前最小值的差值"
(一)思路概述
只用一个栈,存储 "当前元素与入栈时的最小值的差值",同时用一个变量 minVal 记录当前最小值。通过差值计算反推原始值,实现 O(1) 空间优化(但逻辑更复杂)。
核心逻辑:
push(val):
- 若栈为空,
minVal = val,栈存入0(差值为 0)。 - 若栈非空,计算
diff = val - minVal,栈存入diff。 - 若
diff < 0(说明val是新的最小值),更新minVal = val。
pop():
-
弹出栈顶
diff,若diff < 0(说明弹出的是之前的最小值),则minVal需要回退到上一个最小值(minVal = minVal - diff)。 -
top():根据diff和minVal反推原始值(diff >= 0则minVal + diff,否则minVal)。 -
getMin():直接返回minVal。
(二)代码实现
import java.util.Deque;
import java.util.LinkedList;
class MinStack {
private Deque<Long> stack; // 存储差值(用long避免溢出)
private int minVal;
public MinStack() {
stack = new LinkedList<>();
}
public void push(int val) {
if (stack.isEmpty()) {
// 栈为空时,最小值为val,差值为0
minVal = val;
stack.push(0L);
} else {
// 计算当前值与最小值的差值
long diff = (long) val - minVal;
stack.push(diff);
// 若差值为负,说明val是新的最小值
if (diff < 0) {
minVal = val;
}
}
}
public void pop() {
long diff = stack.pop();
// 若差值为负,说明弹出的是之前的最小值,需要回退minVal
if (diff < 0) {
minVal = (int) (minVal - diff);
}
}
public int top() {
long diff = stack.peek();
// 差值非负:原始值 = 最小值 + 差值
// 差值为负:原始值就是当前最小值(因为push时已更新minVal)
return diff >= 0 ? (int) (minVal + diff) : minVal;
}
public int getMin() {
return minVal;
}
}
(三)执行流程解析
以相同操作序列 push(-2) → push(0) → push(-3) → getMin() → pop() → top() → getMin() 为例:
push(-2):- 栈为空,
minVal = -2,栈存入0→ 栈:[0]。
- 栈为空,
push(0):diff = 0 - (-2) = 2(非负),栈存入2→ 栈:[0, 2]。minVal仍为-2。
push(-3):diff = -3 - (-2) = -1(负),栈存入-1→ 栈:[0, 2, -1]。- 更新
minVal = -3。
getMin():返回minVal = -3(正确)。pop():- 弹出
diff = -1(负),回退minVal = -3 - (-1) = -2→ 栈:[0, 2]。
- 弹出
top():- 栈顶
diff = 2(非负),原始值 =-2 + 2 = 0(正确)。
- 栈顶
getMin():返回minVal = -2(正确)。
三、两种解法对比
| 解法 | 空间复杂度 | 时间复杂度 | 优势 | 劣势 |
|---|---|---|---|---|
| 辅助栈(双栈法) | O(n) | O(1) | 逻辑直观,易于理解和实现 | 空间开销较大(额外栈) |
| 单栈差值法 | O(n) | O(1) | 节省空间(无需辅助栈) | 逻辑复杂,需处理溢出问题 |
两种方法的时间复杂度均为 O(1)(所有操作均为常数时间),但双栈法的空间复杂度是 "最坏 O(n)"(当元素严格递减时,辅助栈与主栈元素数量相同),而单栈法的空间复杂度稳定为 O(n)(仅一个栈)。
总结
实现最小栈的核心是高效维护最小值信息,两种主流方法各有侧重:
- 双栈法通过辅助栈同步记录最小值,逻辑清晰,适合初学者理解,是面试中的常用解法。
- 单栈差值法 通过数学计算压缩信息,节省空间,但需要注意数据溢出(用
long存储差值),适合对空间敏感的场景。
实际应用中,双栈法因易于实现和调试更为常用,而单栈法则体现了对问题的深度优化能力。掌握这两种思路,可应对各类变种的 "带最小值操作的栈" 问题。
4.394. 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
测试用例保证输出的长度不会超过 105。
示例 1:
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
字符串解码问题的核心是处理嵌套的括号结构 (如 3[a2[bc]])和多位数重复次数 (如 10[ab]),需通过数据结构记录上下文信息以正确解析嵌套逻辑。主流解法为双栈法 (迭代)和递归法 ,均能高效处理嵌套场景,且时间复杂度为 O(n)(n 为解码后字符串长度)。
一、核心解法:双栈法(迭代处理嵌套)
(一)思路概述
利用两个栈分别存储重复次数 和括号外的前缀字符串,通过 "入栈保存上下文、出栈恢复上下文" 的逻辑处理嵌套:
- numStack :存储括号前的重复次数(如
3[a2[bc]]中的3和2)。 - strStack :存储左括号前已解析的前缀字符串(如
3[a2[bc]]中,遇到2[bc]时,将前缀a压入栈)。 - 遍历过程:
- 遇到数字 :累积计算多位数(如
12[ab]中,1和2需合并为12)。 - 遇到左括号
[:将当前累积的重复次数压入numStack,将当前已解析的字符串压入strStack,然后重置 "当前次数" 和 "当前字符串",准备处理括号内内容。 - 遇到右括号
]:弹出numStack栈顶(重复次数k),弹出strStack栈顶(前缀字符串prevStr),将当前字符串(括号内内容)重复k次后,与prevStr拼接,作为新的 "当前字符串"。 - 遇到字母:直接追加到 "当前字符串" 中。
- 遇到数字 :累积计算多位数(如
(二)代码实现
import java.util.Deque; import java.util.LinkedList;
class Solution {
public String decodeString (String s) {
// 存储重复次数的栈
Deque<Integer> numStack = new LinkedList<>();
// 存储前缀字符串的栈
Deque<StringBuilder> strStack = new LinkedList<>();
// 当前累积的重复次数(处理多位数)
int currentNum = 0;
// 当前正在构建的字符串(括号内或前缀)
StringBuilder currentStr = new StringBuilder ();
for (char c : s.toCharArray ()) {
if (Character.isDigit (c)) {
// 1. 处理数字:累积多位数(如 '12' → 1*10 + 2 = 12)
currentNum = currentNum * 10 + (c - '0');
} else if (c == '[') {
// 2. 处理左括号:保存上下文,重置当前状态
numStack.push (currentNum);
strStack.push (currentStr);
currentNum = 0; // 重置次数
currentStr = new StringBuilder (); // 重置当前字符串
} else if (c == ']') {
// 3. 处理右括号:恢复上下文,拼接重复后的字符串
int k = numStack.pop (); // 弹出重复次数
StringBuilder prevStr = strStack.pop (); // 弹出前缀字符串
// 将当前字符串(括号内内容)重复 k 次,拼接到前缀后
for (int i = 0; i < k; i++) {
prevStr.append (currentStr);
}
currentStr = prevStr; // 更新当前字符串为拼接结果
} else {
// 4. 处理字母:直接追加到当前字符串
currentStr.append (c);
}
}
return currentStr.toString();
}
}
(三)执行流程示例
以输入 s = "3[a2[bc]]" 为例,分步拆解执行过程:
| 字符 | 操作逻辑 | currentNum | currentStr | numStack | strStack |
|---|---|---|---|---|---|
| '3' | 数字累积:3 → currentNum=3 | 3 | "" | [] | [] |
| '[' | 压栈:numStack.push (3),strStack.push ("");重置状态 | 0 | "" | [3] | [""] |
| 'a' | 字母追加 | 0 | "a" | [3] | [""] |
| '2' | 数字累积:2 → currentNum=2 | 2 | "a" | [3] | [""] |
| '[' | 压栈:numStack.push (2),strStack.push ("a");重置状态 | 0 | "" | [3,2] | ["", "a"] |
| 'b' | 字母追加 | 0 | "b" | [3,2] | ["", "a"] |
| 'c' | 字母追加 | 0 | "bc" | [3,2] | ["", "a"] |
| ']' | 出栈:k=2,prevStr="a";"bc" 重复 2 次 → "bcbc",拼接为 "abcbc" | 0 | "abcbc" | [3] | [""] |
| ']' | 出栈:k=3,prevStr="";"abcbc"重复 3 次 →"abcbcabcbcabcbc" | 0 | "abcbcabcbcabcbc" | [] | [] |
最终 currentStr 即为解码结果:"abcbcabcbcabcbc"。
二、变种解法:递归法(处理嵌套结构)
(一)思路概述
嵌套结构天然适合用递归处理:遇到左括号时,递归解析括号内的子字符串;遇到右括号时,返回当前解析结果和下一个待处理的索引(避免重复遍历)。核心步骤:
- 定义递归函数
dfs(index):返回值为[解码后的子字符串, 下一个待处理的索引]。 - 遍历字符串(从index开始):
- 遇到数字 :累积计算多位数
k。 - 遇到左括号
[:递归调用dfs(index+1),获取括号内解码结果subStr和返回的索引nextIdx,然后将subStr重复k次,更新当前字符串,继续从nextIdx遍历。 - 遇到右括号
]:终止递归,返回当前字符串和当前索引 + 1(下一个待处理位置)。 - 遇到字母:直接追加到当前字符串。
- 遇到数字 :累积计算多位数
(二)代码实现
java
class Solution { public String decodeString(String s) { // 调用递归函数,从索引0开始,返回解码后的字符串 return dfs(s, 0)[0]; }
/**
- 递归解析字符串
- @param s 输入字符串
- @param index 当前处理的索引
- @return 数组:[0] 解码后的子字符串,[1] 下一个待处理的索引
*/
private String [] dfs (String s, int index) {
StringBuilder currentStr = new StringBuilder ();
int currentNum = 0;
while (index < s.length ()) {
char c = s.charAt (index);
if (Character.isDigit (c)) {
// 1. 处理数字:累积多位数
currentNum = currentNum * 10 + (c - '0');
index++;
} else if (c == '[') {
// 2. 处理左括号:递归解析括号内内容
String [] subResult = dfs (s, index + 1);
String subStr = subResult [0]; // 括号内解码结果
index = Integer.parseInt (subResult [1]); // 下一个待处理索引
// 将子字符串重复 currentNum 次,追加到当前字符串
for (int i = 0; i < currentNum; i++) {
currentStr.append (subStr);
}
currentNum = 0; // 重置次数
} else if (c == ']') {
// 3. 处理右括号:终止递归,返回结果和下一个索引
index++;
return new String []{currentStr.toString (), String.valueOf (index)};
} else {
// 4. 处理字母:追加到当前字符串
currentStr.append (c);
index++;
}
}
// 遍历结束(无更多右括号),返回当前结果
return new String []{currentStr.toString (), String.valueOf (index)};
}
}
(三)执行流程示例
仍以 s = "3[a2[bc]]" 为例,递归调用流程如下:
- 初始调用
dfs(0):- 索引 0:'3' → currentNum=3,index=1。
- 索引 1:'[' → 调用
dfs(2),进入子递归。
- 子递归
dfs(2):- 索引 2:'a' → currentStr="a",index=3。
- 索引 3:'2' → currentNum=2,index=4。
- 索引 4:'[' → 调用
dfs(5),进入子递归。
- 子递归
dfs(5):- 索引 5:'b' → currentStr="b",index=6。
- 索引 6:'c' → currentStr="bc",index=7。
- 索引 7:']' → 终止递归,返回
["bc", "8"]。
- 回到
dfs(2):- currentNum=2 → "bc" 重复 2 次 → "bcbc",追加到 currentStr("a")→ "abcbc"。
- index=8,currentNum=0,继续遍历。
- 索引 8:']' → 终止递归,返回
["abcbc", "9"]。
- 回到
dfs(0):- currentNum=3 → "abcbc" 重复 3 次 → "abcbcabcbcabcbc"。
- index=9(遍历结束),返回
["abcbcabcbcabcbc", "9"]。
最终结果与双栈法一致。
三、两种解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 核心优势 | 潜在局限 |
|---|---|---|---|---|
| 双栈法(迭代) | O(n) | O(n) | 无递归栈溢出风险,适合大规模字符串 | 需维护两个栈,逻辑稍繁琐 |
| 递归法 | O(n) | O(n) | 逻辑直观,嵌套处理自然 | 嵌套过深可能导致栈溢出(题目中输出长度有限,风险低) |
注:两种方法的空间复杂度均包含 "存储解码结果的空间" 和 "栈 / 递归栈的空间",最坏情况下均为 O(n)(如 1000[abc] 解码后长度为 3000,栈 / 递归栈空间为 1)。
总结
字符串解码的核心是正确处理嵌套结构和多位数重复次数:
- 双栈法通过 "入栈保存上下文、出栈恢复拼接" 的迭代逻辑,适合工程实现,避免递归溢出;
- 递归法利用递归的嵌套处理能力,逻辑更简洁,适合理解嵌套结构的本质。
两种方法均需重点关注 "多位数累积" 和 "上下文状态保存",掌握其中一种即可高效解决问题,双栈法在实际面试中更为常用(稳定性更高)。
5.739. 每日温度
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
示例 1:
输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
每日温度问题要求找出++每个元素之后第一个比它大的元素的距离,核心挑战是如何高效地找到 "下一个更大元素"++ 。最经典的解法是利用单调栈 将时间复杂度优化至 O(n),远超暴力解法的 O(n²)。
一、核心解法:单调栈(递减栈)
(一)思路概述
单调栈是解决 "下一个更大元素" 类问题的核心工具,其核心思想是维护一个栈内元素单调递减的栈,栈中存储的是数组索引(而非值),通过栈顶元素与当前元素的比较来确定 "下一个更大元素" 的位置。
具体逻辑:
- 遍历温度数组,栈中保存 "尚未找到下一个更高温度" 的天数索引。
- 对于当前天数i:
- 若栈不为空且当前温度
temperatures[i]大于栈顶索引对应的温度,则栈顶索引j的 "下一个更高温度" 就是i,计算距离i - j并弹出栈顶。 - 重复上述操作,直到栈为空或当前温度不大于栈顶温度,将当前索引
i入栈。
- 若栈不为空且当前温度
- 遍历结束后,栈中剩余索引(未找到下一个更高温度)的距离均为
0。
(二)代码实现
import java.util.Deque;
import java.util.LinkedList;
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] answer = new int[n]; // 结果数组,默认值为0
Deque<Integer> stack = new LinkedList<>(); // 单调栈,存储索引(栈内温度递减)
for (int i = 0; i < n; i++) {
// 当栈不为空且当前温度 > 栈顶索引对应的温度时,说明找到下一个更高温度
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int prevIndex = stack.pop(); // 弹出栈顶(之前未找到更高温度的索引)
answer[prevIndex] = i - prevIndex; // 计算距离
}
// 当前索引入栈(等待后续更高温度)
stack.push(i);
}
// 栈中剩余元素(未找到更高温度),answer默认值为0,无需额外处理
return answer;
}
}
(三)执行流程示例
以 temperatures = [73, 74, 75, 71, 69, 72, 76, 73] 为例,分步解析:
索引 i |
温度 | 栈操作(栈内为索引,对应温度递减) | answer 数组变化 |
|---|---|---|---|
| 0 | 73 | 栈空,push (0) → 栈:[0] | [0,0,0,0,0,0,0,0] |
| 1 | 74 | 74 > 73(栈顶 0 的温度):pop (0),answer [0] = 1-0=1;栈空,push (1) → 栈:[1] | [1,0,0,0,0,0,0,0] |
| 2 | 75 | 75 > 74(栈顶 1 的温度):pop (1),answer [1] = 2-1=1;栈空,push (2) → 栈:[2] | [1,1,0,0,0,0,0,0] |
| 3 | 71 | 71 <75(栈顶 2 的温度),push (3) → 栈:[2,3] | [1,1,0,0,0,0,0,0] |
| 4 | 69 | 69 <71(栈顶 3 的温度),push (4) → 栈:[2,3,4] | [1,1,0,0,0,0,0,0] |
| 5 | 72 | 72 > 69(栈顶 4 的温度):pop (4),answer [4] = 5-4=1; 72 > 71(栈顶 3 的温度):pop (3),answer [3] = 5-3=2; 72 <75(栈顶 2 的温度),push (5) → 栈:[2,5] | [1,1,0,2,1,0,0,0] |
| 6 | 76 | 76 > 72(栈顶 5 的温度):pop (5),answer [5] = 6-5=1; 76 > 75(栈顶 2 的温度):pop (2),answer [2] = 6-2=4; 栈空,push (6) → 栈:[6] | [1,1,4,2,1,1,0,0] |
| 7 | 73 | 73 <76(栈顶 6 的温度),push (7) → 栈:[6,7] | [1,1,4,2,1,1,0,0] |
最终结果:[1,1,4,2,1,1,0,0](与预期一致)。
二、对比解法:暴力法(时间复杂度不达标,仅作参考)
(一)思路概述
对于每个元素,向后遍历找到第一个比它大的元素,记录距离。若遍历到末尾仍未找到,距离为 0。时间复杂度 O(n²),空间复杂度 O(1)(不含结果数组)。
(二)代码实现(仅作对比)
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] answer = new int[n];
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (temperatures[j] > temperatures[i]) {
answer[i] = j - i;
break; // 找到第一个更高温度,跳出内层循环
}
}
// 未找到时,answer[i]保持默认值0
}
return answer;
}
}
三、两种解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优势 | 劣势 |
|---|---|---|---|---|
| 单调栈法 | O(n) | O(n) | 高效,适合大规模数组 | 需要额外栈空间 |
| 暴力法 | O(n²) | O(1) | 逻辑简单,无需额外空间 | 效率低,不适用于大数组 |
关键差异 :单调栈通过 "一次遍历 + 栈内元素最多入栈出栈各一次" 将时间复杂度降至线性,而暴力法的嵌套循环在最坏情况(温度严格递减)下需 O(n²) 时间。
总结
每日温度问题的最优解是单调栈法,其核心是利用栈维护 "尚未找到下一个更高温度" 的索引,通过栈顶与当前元素的比较快速确定距离:
- 栈内元素保持温度递减的单调性,确保每次弹出的元素都能找到对应的 "下一个更高温度"。
- 每个元素最多入栈和出栈一次,总操作次数为
O(n),时间效率远超暴力法。
掌握单调栈的思想,可解决一系列 "下一个更大 / 更小元素" 问题(如 496. 下一个更大元素 I),是处理这类序列问题的核心工具。
6.84. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:

输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
柱状图中最大的矩形问题的核心是找到每个柱子能向左右扩展的最大宽度 ,其面积为 "高度 × 宽度",最大面积即为所有可能面积中的最大值。常见解法包括暴力法、单调栈法(最优)和分治法,其中单调栈法时间复杂度为 O(n),是工程中首选的高效解法。
一、核心解法:单调栈(递增栈)
(一)思路概述
单调栈的核心是维护一个存储柱子索引的递增栈 (栈中索引对应的高度严格递增),通过栈顶元素与当前元素的比较,快速确定每个柱子的左右边界(左边第一个比它矮的柱子和右边第一个比它矮的柱子)。
具体逻辑:
- 栈中存储柱子索引,确保索引对应的高度严格递增(便于确定左边界)。
- 遍历每个柱子(包括末尾添加的 "哨兵" 0,用于触发栈中剩余元素的计算):
- 若当前柱子高度 < 栈顶索引对应的高度:弹出栈顶索引
mid,以heights[mid]为高度计算面积。 - 此时,左边界为新栈顶索引 (若栈为空则为
-1),右边界为当前索引,宽度 = 右边界 - 左边界 - 1。 - 计算面积并更新最大面积。
- 重复弹出直到当前柱子高度 ≥ 栈顶高度,将当前索引入栈。
- 若当前柱子高度 < 栈顶索引对应的高度:弹出栈顶索引
- 遍历结束后,最大面积即为结果。
也就是说:
在遍历到索引 i 时,若当前柱子高度 h[i] 小于栈顶索引对应的柱子高度,则栈顶元素的右边界已确定为 i,此时弹出栈顶并计算其面积;之后继续检查新栈顶元素,若其高度仍大于 h[i],则重复弹出并计算面积的操作(这些元素的右边界均为 i);++直到栈顶元素高度小于等于 h[i] 时,停止弹出,将当前索引 i 入栈(维持栈的递增特性)。++
这一过程的核心是:用 h[i] 作为 "参照标准",批量确定所有高度大于 h[i] 的栈顶元素的右边界,一次性计算它们的面积,既保证了每个元素只被处理一次,又通过栈的单调性高效维护了左边界信息。
(二)代码实现
import java.util.Deque;
import java.util.LinkedList;
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
if (n == 0) return 0;
// 复制原数组并添加哨兵0(便于处理栈中剩余元素)
int[] newHeights = new int[n + 1];
System.arraycopy(heights, 0, newHeights, 0, n);
newHeights[n] = 0;
Deque<Integer> stack = new LinkedList<>(); // 单调递增栈,存储索引
int maxArea = 0;
for (int i = 0; i < newHeights.length; i++) {
// 当栈不为空且当前高度 < 栈顶索引对应的高度时,弹出栈顶并计算面积
while (!stack.isEmpty() && newHeights[i] < newHeights[stack.peek()]) {
int mid = stack.pop(); // 弹出的索引,以其高度为矩形高度
int left = stack.isEmpty() ? -1 : stack.peek(); // 左边界(第一个更矮的柱子)
int width = i - left - 1; // 宽度 = 右边界(i) - 左边界(left) - 1
int area = newHeights[mid] * width;
maxArea = Math.max(maxArea, area);
}
stack.push(i); // 当前索引入栈(维持递增性)
}
return maxArea;
}
}
注意:
1.++为什么宽度 = 右边界 - 左边界 - 1。?++
在这段代码中,宽度 = 右边界 - 左边界 - 1 的计算公式是由左右边界的定义 和矩形覆盖范围共同决定的,我们可以通过具体场景拆解:
一、明确三个核心索引的含义
mid:当前弹出的栈顶索引,代表以newHeights[mid]为高度的矩形(我们要计算这个矩形的最大宽度)。left:左边界,即第一个高度小于newHeights[mid]的柱子索引 (栈弹出mid后,新栈顶就是左边界;若栈为空,左边界为-1)。i(右边界) :当前遍历的索引,代表第一个高度小于newHeights[mid]的柱子索引 (因为循环条件是newHeights[i] < newHeights[mid],所以i是右侧第一个更矮的柱子)。
二、矩形的覆盖范围
以 mid 为中心的矩形,能向左右扩展的范围是:
- 左极限 :
left + 1(因为left是第一个比mid矮的柱子,++所以left右侧的第一个柱子left+1开始,高度都 ≥newHeights[mid])。++ - 右极限 :
i - 1(因为i是第一个比mid矮的柱子,所以i左侧的最后一个柱子i-1为止,高度都 ≥newHeights[mid])。
因此,矩形覆盖的索引范围是 [left+1, i-1],这个区间内的所有柱子都能与 mid 组成高度为 newHeights[mid] 的矩形。
三、宽度计算公式的推导
区间 [left+1, i-1] 包含的柱子数量(即宽度)为:
plaintext
宽度 = (i - 1) - (left + 1) + 1 = i - left - 1
- 公式解析:
(i-1) - (left+1)是区间首尾的差值,+1是因为包含首尾两个端点(例如[2,4]包含 2、3、4 三个数,4-2+1=3)。
2.哨兵 0 的作用:强制触发栈中剩余元素的弹出
哨兵 0 的高度为 0,而原数组中所有柱子的高度都是非负整数(题目给定)。当遍历到哨兵 0 时:
- 由于
0 ≤ 所有柱子高度,循环条件newHeights[i] < newHeights[stack.peek()]会被触发。 - 栈中所有剩余元素(索引)会被依次弹出,并计算各自对应的矩形面积。
- 最终栈会被清空,确保所有可能的矩形面积都被考虑。
3.如何想到单调栈能解决这个问题
要理解 "为什么会想到用单调栈解决柱状图最大矩形问题",需要从问题本质出发,逐步推导 "从暴力解法到高效解法" 的思考路径。这个过程的核心是发现问题的 "重复性" 和 "单调性",并找到能利用这些特性的工具(单调栈)。
一、先看透问题本质:面积由 "高度" 和 "宽度" 决定
柱状图中,任何一个矩形的面积 = 某根柱子的高度 × 该高度能向左右扩展的最大宽度。
例如,对于高度为 h 的柱子,若能向左扩展到第 L 根柱子,向右扩展到第 R 根柱子,则宽度为 R-L+1,面积为 h×(R-L+1)。
因此,问题可转化为:对每根柱子,找到它左右两侧 "第一个比它矮的柱子"(这两个柱子是扩展的边界),然后计算面积并取最大值。
二、从暴力解法中发现 "低效点"
暴力解法的思路是:
- 对每根柱子
i,向左遍历找到第一个比heights[i]矮的柱子L; - 向右遍历找到第一个比
heights[i]矮的柱子R; - 计算面积
heights[i]×(R-L-1),更新最大值。
但暴力解法的时间复杂度是 O(n²)(每个柱子可能遍历左右各 O(n) 次),效率太低。其核心低效点是:
- 重复遍历:多个柱子的左右边界查找存在重叠的遍历路径(例如,柱子
i向右遍历到j,柱子i+1可能也需要遍历到j)。
三、思考:如何 "一次性" 找到所有柱子的边界?
要优化效率,需避免重复遍历。关键观察是:柱子的边界具有 "单调性" ------
如果柱子 i 的高度 < 柱子 j 的高度(i < j),那么柱子 j 的左边界不可能在 i 的左侧(因为 i 已经比 j 矮,j 的左边界最多是 i)。
这种单调性暗示:可以用一种数据结构按顺序记录柱子,当遇到更矮的柱子时,就能确定之前某些柱子的右边界。而栈恰好适合这种 "先进后出" 的顺序处理 ------ 这就是单调栈的雏形。
四、单调栈的 "天然适配性"
单调栈(此处为递增栈 )的核心作用是维护 "尚未确定右边界" 的柱子 ,栈内柱子的高度严格递增。当遍历到新柱子 i 时:
- 若
heights[i]大于栈顶柱子的高度:说明栈顶柱子的右边界还没找到,将i入栈(继续等待右边界)。 - 若
heights[i]小于栈顶柱子的高度:说明栈顶柱子的右边界就是i,此时可以弹出栈顶,计算其面积(左边界是弹出后新的栈顶,因为栈是递增的,新栈顶必然比弹出的柱子矮)。
这个过程中,每个柱子只入栈一次、出栈一次 ,总操作次数为 O(n),实现了线性时间复杂度。
五、总结:想到单调栈的思考路径
- 问题转化:将 "最大矩形面积" 转化为 "找每个柱子的左右边界"。
- 发现低效:暴力法重复遍历,时间复杂度高。
- 观察特性:柱子高度的 "单调性" 决定了边界的 "顺序可确定"。
- 工具匹配:单调栈适合 "按顺序维护未处理元素,遇到触发条件时批量处理",恰好能一次性确定多个柱子的边界。
简言之,单调栈的出现是为了利用单调性消除重复计算,将 "多次遍历找边界" 优化为 "一次遍历 + 栈内操作",这是从问题特性到数据结构的自然推导。
(三)执行流程示例
以 heights = [2, 1, 5, 6, 2, 3] 为例(新数组为 [2,1,5,6,2,3,0]),分步解析:
索引 i |
高度 | 栈操作(栈内为索引,对应高度递增) | 弹出 mid |
左边界 left |
宽度 | 面积 | 最大面积 |
|---|---|---|---|---|---|---|---|
| 0 | 2 | 栈空,push (0) → 栈:[0] | - | - | - | - | 0 |
| 1 | 1 | 1 < 2(栈顶 0 的高度):弹出 0 → 栈空 | mid=0 | left=-1 | 1-(-1)-1=1 | 2×1=2 | 2 |
| 1 | 1 | 栈空,push (1) → 栈:[1] | - | - | - | - | 2 |
| 2 | 5 | 5 > 1(栈顶 1 的高度),push (2) → 栈:[1,2] | - | - | - | - | 2 |
| 3 | 6 | 6 > 5(栈顶 2 的高度),push (3) → 栈:[1,2,3] | - | - | - | - | 2 |
| 4 | 2 | 2 <6(栈顶 3 的高度):弹出 3 → 栈:[1,2] | mid=3 | left=2 | 4-2-1=1 | 6×1=6 | 6 |
| 4 | 2 | 2 <5(栈顶 2 的高度):弹出 2 → 栈:[1] | mid=2 | left=1 | 4-1-1=2 | 5×2=10 | 10 |
| 4 | 2 | 2 > 1(栈顶 1 的高度),push (4) → 栈:[1,4] | - | - | - | - | 10 |
| 5 | 3 | 3 > 2(栈顶 4 的高度),push (5) → 栈:[1,4,5] | - | - | - | - | 10 |
| 6 | 0 | 0 ❤️(栈顶 5 的高度):弹出 5 → 栈:[1,4] | mid=5 | left=4 | 6-4-1=1 | 3×1=3 | 10 |
| 6 | 0 | 0 <2(栈顶 4 的高度):弹出 4 → 栈:[1] | mid=4 | left=1 | 6-1-1=4 | 2×4=8 | 10 |
| 6 | 0 | 0 < 1(栈顶 1 的高度):弹出 1 → 栈空 | mid=1 | left=-1 | 6-(-1)-1=6 | 1×6=6 | 10 |
| 6 | 0 | 栈空,push (6) → 栈:[6] | - | - | - | - | 10 |
最终最大面积为 10(正确,对应高度 5 和 6 的柱子形成的矩形:宽度 2× 高度 5=10)。
二、对比解法 1:暴力法(枚举每个柱子的左右边界)
(一)思路概述
对每个柱子,向左右扩展找到第一个比它矮的柱子 ,确定宽度后计算面积。时间复杂度 O(n²),空间复杂度 O(1)。
(二)代码实现
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
if (n == 0) return 0;
int maxArea = 0;
for (int i = 0; i < n; i++) {
int height = heights[i];
// 向左找第一个比当前矮的柱子
int left = i;
while (left > 0 && heights[left - 1] >= height) {
left--;
}
// 向右找第一个比当前矮的柱子
int right = i;
while (right < n - 1 && heights[right + 1] >= height) {
right++;
}
// 计算宽度和面积
int width = right - left + 1;
maxArea = Math.max(maxArea, height * width);
}
return maxArea;
}
}
三、对比解法 2:分治法
(一)思路概述
分治法的核心是 "找到区间内的最矮柱子,以此分割问题":
- 最矮柱子的最大面积 = 高度 × 区间长度(向左右扩展到边界)。
- 递归计算最矮柱子左边区间和右边区间的最大面积。
- 总最大面积为三者中的最大值。
时间复杂度:平均 O(n log n),最坏 O(n²)(如数组严格递增,每次最矮柱子在边缘,递归退化为线性)。
(二)代码实现
class Solution {
public int largestRectangleArea(int[] heights) {
return calculateArea(heights, 0, heights.length - 1);
}
// 计算[left, right]区间内的最大矩形面积
private int calculateArea(int[] heights, int left, int right) {
if (left > right) return 0;
if (left == right) return heights[left]; // 单个柱子
// 找到区间内最矮柱子的索引
int minIndex = left;
for (int i = left; i <= right; i++) {
if (heights[i] < heights[minIndex]) {
minIndex = i;
}
}
// 最矮柱子的面积
int currentArea = heights[minIndex] * (right - left + 1);
// 左边区间的最大面积
int leftArea = calculateArea(heights, left, minIndex - 1);
// 右边区间的最大面积
int rightArea = calculateArea(heights, minIndex + 1, right);
// 返回三者中的最大值
return Math.max(currentArea, Math.max(leftArea, rightArea));
}
}
以 heights = [2,1,5,6,2,3] 为例,分治法的执行过程如下:
- 初始区间
[0,5]:- 最矮柱子是
1(索引 1),面积1×6=6; - 左子区间
[0,0](只有柱子 2):面积2×1=2; - 右子区间
[2,5](柱子 5,6,2,3):递归求解。
- 最矮柱子是
- 右子区间
[2,5]:- 最矮柱子是
2(索引 4),面积2×4=8; - 左子区间
[2,3](柱子 5,6):递归求解; - 右子区间
[5,5](柱子 3):面积3×1=3。
- 最矮柱子是
- 左子区间
[2,3]:- 最矮柱子是
5(索引 2),面积5×2=10; - 左子区间
[2,1](空,面积 0); - 右子区间
[3,3](柱子 6):面积6×1=6。
- 最矮柱子是
- 合并结果 :
全局最大面积 =max(6, 2, max(8, 10, 3)) = 10(正确,对应柱子 5 和 6 的矩形)。
可以看到,所有可能的最大矩形(情况 1 的 6、情况 2 的 2、情况 3 的 8/10/3)都被覆盖,最终通过递归合并得到了全局最大值。
四、三种解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优势 | 劣势 |
|---|---|---|---|---|
| 单调栈法 | O(n) | O(n) | 高效,线性时间,适合大数据 | 需要维护栈,逻辑稍复杂 |
| 暴力法 | O(n²) | O(1) | 逻辑简单,易于理解 | 效率低,不适合大数组 |
| 分治法 | 平均 O (n log n) | O(log n) | 思路直观,递归分割问题 | 最坏 O (n²),递归栈开销 |
总结
柱状图中最大的矩形问题的最优解是单调栈法,其核心是通过维护递增栈快速确定每个柱子的左右边界(第一个更矮的柱子),从而计算面积。关键逻辑包括:
- 栈存储索引,保持高度递增,确保弹出时能确定左边界。
- 当前索引作为右边界,宽度 = 右边界 - 左边界 - 1。
- 末尾添加哨兵 0,确保栈中所有元素都能被弹出计算。
掌握单调栈法不仅能解决本题,还能推广到 "最大矩形""接雨水" 等依赖 "左右边界" 的问题,是处理此类序列问题的核心工具。