markdown复制## 1. 栈结构在算法中的典型应用解析
栈(Stack)作为一种后进先出(LIFO)的数据结构,在算法问题中有着广泛的应用场景。今天我将结合力扣Hot100中的四道经典题目,深入剖析栈结构的核心应用技巧。这些题目覆盖了括号匹配、极值维护、字符串处理和温度分析等典型场景,非常值得算法学习者反复揣摩。
> 提示:本文所有代码示例均基于Java实现,但算法思想适用于任何编程语言。建议读者先自行尝试解题,再对照解析查漏补缺。
## 2. 有效的括号匹配问题
### 2.1 问题描述与核心思路
给定一个仅包含`'('`,`')'`,`'{'`,`'}'`,`'['`,`']'`的字符串,判断其是否为有效的括号组合。有效字符串需满足:
1. 左括号必须用相同类型的右括号闭合
2. 左括号必须以正确的顺序闭合
3. 每个右括号都有对应的同类型左括号
**示例分析**:
- 有效案例:`"()[]{}"` → `true`
- 无效案例:`"([)]"` → `false`(虽然每种括号都能闭合,但顺序不正确)
### 2.2 算法实现与优化
```java
class Solution {
public boolean isValid(String s) {
// 快速校验:奇数长度直接返回false
if (s.length() % 2 != 0) return false;
// 建立括号映射关系(右括号→左括号)
Map<Character, Character> pairs = new HashMap<>() {{
put(')', '(');
put(']', '[');
put('}', '{');
}};
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (pairs.containsKey(c)) { // 遇到右括号
if (stack.isEmpty() || stack.pop() != pairs.get(c)) {
return false;
}
} else { // 遇到左括号
stack.push(c);
}
}
return stack.isEmpty();
}
}
关键优化点:
- 前置长度校验:奇数长度字符串必定无效,可提前返回
- 使用
ArrayDeque替代LinkedList:在栈操作场景下性能更优 - 右括号作为Map的键:更符合"查找闭合关系"的逻辑直觉
2.3 复杂度分析与边界情况
- 时间复杂度:O(n),只需一次遍历
- 空间复杂度:O(n),最坏情况下需要存储所有左括号
常见陷阱:
- 仅统计各类括号数量而不考虑顺序(错误示例:
"([)]") - 忽略空字符串情况(应返回
true) - 未处理纯左括号字符串(如
"(((")
3. 最小栈的设计实现
3.1 问题需求分析
设计一个支持push、pop、top操作,并能常数时间获取栈中最小元素的特殊栈。关键在于如何在O(1)时间内获取当前栈的最小值,而不是通过遍历查找。
操作示例:
code复制push(-2)
push(0)
push(-3)
getMin() → -3
pop()
top() → 0
getMin() → -2
3.2 双栈解法设计
java复制class MinStack {
private Deque<Integer> dataStack;
private Deque<Integer> minStack;
public MinStack() {
dataStack = new ArrayDeque<>();
minStack = new ArrayDeque<>();
minStack.push(Integer.MAX_VALUE); // 哨兵值
}
public void push(int val) {
dataStack.push(val);
minStack.push(Math.min(minStack.peek(), val));
}
public void pop() {
dataStack.pop();
minStack.pop();
}
public int top() {
return dataStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
设计要点:
- 主栈
dataStack正常存储所有元素 - 辅助栈
minStack与主栈同步操作,存储对应位置的最小值 - 初始时
minStack压入Integer.MAX_VALUE作为哨兵,避免空栈判断
3.3 复杂度与变种问题
- 所有操作时间复杂度:O(1)
- 空间复杂度:O(n),需要额外维护最小栈
类似问题扩展:
- 最大栈(只需修改minStack为maxStack)
- 支持O(1)获取栈中平均值的栈(维护累加和栈)
- 支持O(1)获取栈中中间值的栈(更复杂,需要结合堆结构)
4. 字符串解码问题
4.1 问题描述
给定一个形如3[a2[c]]的编码字符串,需要解码为accaccacc。这类问题通常涉及嵌套结构的解析,非常适合用栈来处理。
4.2 双栈解法实现
java复制class Solution {
public String decodeString(String s) {
Deque<Integer> numStack = new ArrayDeque<>();
Deque<StringBuilder> strStack = new ArrayDeque<>();
StringBuilder current = new StringBuilder();
int num = 0;
for (char c : s.toCharArray()) {
if (Character.isDigit(c)) {
num = num * 10 + (c - '0');
} else if (c == '[') {
numStack.push(num);
strStack.push(current);
current = new StringBuilder();
num = 0;
} else if (c == ']') {
int repeat = numStack.pop();
StringBuilder temp = strStack.pop();
for (int i = 0; i < repeat; i++) {
temp.append(current);
}
current = temp;
} else {
current.append(c);
}
}
return current.toString();
}
}
执行流程示例(以3[a2[c]]为例):
- 遇到
3:累积数字num=3 - 遇到
[:数字3入栈,当前字符串""入栈,重置状态 - 遇到
a:构建当前字符串"a" - 遇到
2:累积数字num=2 - 遇到
[:数字2入栈,当前字符串"a"入栈,重置状态 - 遇到
c:构建当前字符串"c" - 遇到
]:弹出数字2和字符串"a",拼接得到"a"+"cc"="acc" - 遇到
]:弹出数字3和字符串"",拼接得到""+"accaccacc"="accaccacc"
5. 每日温度问题
5.1 问题建模
给定每日温度数组[73,74,75,71,69,72,76,73],返回一个数组表示每天需要等待多少天才能遇到更高温度。对于最后一天,因为没有后续数据,结果固定为0。
示例输出:
[1,1,4,2,1,1,0,0]
5.2 单调栈解法
java复制class Solution {
public int[] dailyTemperatures(int[] temps) {
int[] result = new int[temps.length];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < temps.length; i++) {
while (!stack.isEmpty() && temps[i] > temps[stack.peek()]) {
int prev = stack.pop();
result[prev] = i - prev;
}
stack.push(i);
}
return result;
}
}
算法精髓:
- 使用栈存储尚未找到更高温度的日期下标
- 维护栈的单调递减性(栈底到栈顶对应温度递减)
- 当遇到更高温度时,计算日期差并更新结果
5.3 复杂度与同类问题
- 时间复杂度:O(n),每个元素最多入栈出栈各一次
- 空间复杂度:O(n),最坏情况下需要存储所有日期
类似问题:
- 下一个更大元素(LeetCode 496)
- 柱状图中的最大矩形(LeetCode 84)
- 接雨水问题(LeetCode 42)
6. 栈结构应用总结
通过这四道典型题目,我们可以总结出栈在算法问题中的三大核心应用场景:
- 对称性处理:如括号匹配、回文判断等问题,利用栈的LIFO特性验证对称关系
- 极值维护:通过辅助栈在O(1)时间内获取当前极值,如最小栈问题
- 单调性问题:利用单调栈处理Next Greater Element类问题,如每日温度
在实际编码时需要注意:
- Java中优先使用
ArrayDeque而非Stack类(后者因同步开销已不推荐) - 注意处理边界情况(空输入、极端数据等)
- 合理使用哨兵值简化边界判断
掌握这些栈的应用模式后,可以举一反三解决大量类似的算法问题。建议读者可以继续练习:
- 基本计算器(LeetCode 224)
- 验证栈序列(LeetCode 946)
- 删除字符串中的相邻重复项(LeetCode 1047)
最后分享一个调试技巧:在处理栈相关问题时,可以在关键操作前后打印栈的状态,这样能快速定位逻辑错误。例如在每日温度问题中,可以在每次入栈出栈时打印当前栈内容和结果数组状态。