1. 问题背景与核心需求
每日温度问题(Daily Temperatures)是LeetCode中一道经典的栈应用题目,编号为739。题目要求:给定一个温度列表,计算每一天需要等待多少天才能遇到更高的温度。如果未来没有更高的温度,则该位置结果为0。
举个实际例子,假设输入温度列表为[73,74,75,71,69,72,76,73],那么对应的输出应该是[1,1,4,2,1,1,0,0]。这个结果表示:
- 第1天温度73,第2天74更高,所以等待1天
- 第2天温度74,第3天75更高,等待1天
- 第3天温度75,需要等到第7天(76度)才有更高温度,等待4天
- 以此类推...
这个问题看似简单,但直接暴力解法的时间复杂度会达到O(n²),对于大规模数据效率极低。而使用单调栈可以将时间复杂度优化到O(n),这正是我们需要深入探讨的核心。
2. 单调栈原理剖析
2.1 什么是单调栈
单调栈(Monotonic Stack)是一种特殊的栈结构,它要求栈中的元素始终保持单调递增或单调递减的顺序。在每日温度问题中,我们使用的是单调递减栈 - 即从栈底到栈顶,温度值是逐渐降低的。
这种数据结构的神奇之处在于,它能高效地找到某个元素"下一个更大"或"下一个更小"的元素。对于温度问题,我们正是要寻找"下一个更高温度"。
2.2 单调栈的工作机制
当处理第i天的温度时,单调栈的工作流程如下:
- 检查栈顶元素对应的温度是否小于当前温度
- 如果是,则栈顶元素找到了它的"下一个更高温度",计算天数差并记录结果
- 弹出栈顶元素,继续检查新的栈顶元素
- 将当前温度压入栈中
这个过程保证了栈中元素的温度值始终保持单调递减的顺序。每个元素最多入栈和出栈各一次,因此时间复杂度是O(n)。
3. Java实现与代码解析
3.1 基础实现
java复制public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] result = new int[n];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
int prevIndex = stack.pop();
result[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return result;
}
3.2 关键点解析
- 栈存储的是索引而非温度值:这样既能比较温度大小,又能方便计算天数差
- 初始化结果数组为0:Java中int数组默认初始化为0,正好符合"没有更高温度则返回0"的要求
- 使用Deque代替Stack:Java中ArrayDeque比Stack性能更好,是更现代的栈实现方式
- 循环条件:只有当栈不为空且当前温度大于栈顶温度时才进入处理逻辑
3.3 复杂度分析
- 时间复杂度:O(n),每个元素最多被压入和弹出栈各一次
- 空间复杂度:O(n),最坏情况下所有元素都在栈中
4. 算法正确性证明
为了验证这个算法的正确性,我们可以从以下几个方面考虑:
- 单调性保持:每次处理新元素时,只有比栈顶温度高的元素才会导致弹出,因此栈中温度始终保持单调递减
- 结果正确性:当某个元素被弹出时,当前元素就是它右边第一个比它大的元素
- 完整性:所有元素最终都会被处理,要么找到更高温度,要么保留在栈中(结果为0)
5. 变种与扩展问题
5.1 循环数组版本
如果温度列表是循环的(即第n天后又回到第1天),如何修改算法?这时我们需要遍历两次数组,或者使用取模运算来处理循环:
java复制public int[] dailyTemperaturesCircular(int[] temperatures) {
int n = temperatures.length;
int[] result = new int[n];
Arrays.fill(result, -1); // 初始化为-1表示未找到
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < 2 * n; i++) {
int idx = i % n;
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[idx]) {
int prev = stack.pop();
if (result[prev] == -1) {
result[prev] = (idx - prev + n) % n;
}
}
if (i < n) {
stack.push(idx);
}
}
return result;
}
5.2 前一个更冷/更热温度
类似地,我们可以找到前一个更冷或更热的温度。只需要改变遍历方向和单调栈的单调性即可。
5.3 股票价格跨度问题
这是LeetCode 901题,与每日温度问题非常相似,但需要计算的是连续小于等于当前价格的天数。可以使用单调递增栈来解决。
6. 性能优化与工程实践
6.1 使用数组替代栈
在性能敏感的场合,可以使用数组和指针来模拟栈,减少对象创建开销:
java复制public int[] dailyTemperaturesOptimized(int[] temperatures) {
int n = temperatures.length;
int[] result = new int[n];
int[] stack = new int[n];
int top = -1;
for (int i = 0; i < n; i++) {
while (top >= 0 && temperatures[stack[top]] < temperatures[i]) {
int prev = stack[top--];
result[prev] = i - prev;
}
stack[++top] = i;
}
return result;
}
6.2 多语言实现对比
虽然本文以Java为例,但单调栈的思想是通用的。其他语言的实现也大同小异:
Python示例:
python复制def dailyTemperatures(temperatures):
n = len(temperatures)
result = [0] * n
stack = []
for i in range(n):
while stack and temperatures[stack[-1]] < temperatures[i]:
prev = stack.pop()
result[prev] = i - prev
stack.append(i)
return result
C++示例:
cpp复制vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> result(n, 0);
stack<int> s;
for (int i = 0; i < n; ++i) {
while (!s.empty() && temperatures[s.top()] < temperatures[i]) {
int prev = s.top();
s.pop();
result[prev] = i - prev;
}
s.push(i);
}
return result;
}
7. 常见错误与调试技巧
7.1 典型错误模式
- 栈中存储温度值而非索引:这样会导致无法计算天数差
- 结果数组初始化错误:忘记初始化或初始化为错误值
- 边界条件处理不当:如空输入或单元素输入的情况
- 单调性方向错误:混淆了递增栈和递减栈的使用场景
7.2 调试建议
- 小规模测试用例:先用简单的例子手动模拟算法执行过程
- 打印栈状态:在循环中添加调试输出,观察栈的变化
- 边界测试:测试空数组、单元素数组、全相同温度等特殊情况
- 性能测试:用大规模随机数据测试算法的时间复杂度
8. 实际应用场景
单调栈虽然源于算法题,但在实际工程中有广泛应用:
- 数据库查询优化:某些范围查询可以使用单调栈思想优化
- 编译器设计:处理嵌套结构时常用到栈结构
- 图形渲染:某些光栅化算法会用到类似思想
- 金融分析:计算股票价格波动模式
- 气象数据分析:寻找温度变化趋势
9. 算法思维扩展
单调栈是"空间换时间"思想的典型体现。类似的算法思维还包括:
- 滑动窗口:维护一个窗口内的极值
- 双指针:从两端向中间遍历
- 前缀和:预处理数据加速区间查询
- 线段树/RMQ:处理区间查询问题
理解这些算法之间的异同,能够帮助我们在面对新问题时快速选择合适的解决方案。