1. 问题背景与核心挑战
每日温度问题(LeetCode 739题)是算法面试中的高频考点,题目要求:给定一个温度列表,对于每一天,你需要计算出需要等待多少天才能遇到更高的温度。如果未来没有更高的温度,则在该位置用0代替。
这个看似简单的问题实际上考察了以下几个关键能力:
- 对数据特性的敏感度(温度变化的趋势)
- 对暴力解法局限性的认知(O(n²)时间复杂度)
- 对栈结构特性的理解(后进先出与问题特性的契合)
- 对空间换时间策略的权衡
提示:在实际面试中,面试官通常会先让候选人写出暴力解法,然后引导优化到单调栈解法,以此考察候选人的算法优化能力。
2. 暴力解法与性能瓶颈
2.1 直观的双重循环实现
最直接的解法是使用双重循环遍历每一天,对于第i天的温度,向后查找第一个大于它的温度:
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; } } } return answer; }2.2 时间复杂度分析
这种解法的时间复杂度为O(n²),在LeetCode上提交时会遇到TLE(Time Limit Exceeded)错误。当输入规模n=10⁵时,操作次数将达到10¹⁰量级,远超一般OJ系统的承受能力。
注意:虽然题目给出的示例通常是小规模数据,但面试时需要主动考虑大规模数据的处理,这是区分初级和中级开发者的重要标志。
3. 单调栈的核心思想
3.1 什么是单调栈
单调栈是一种特殊的栈结构,它保证栈内元素始终保持单调性(递增或递减)。在每日温度问题中,我们使用单调递减栈:
- 栈中存储的是温度的索引(而非温度值本身)
- 从栈底到栈顶,对应的温度值依次递减
- 当遇到比栈顶温度高的新温度时,触发出栈操作
3.2 为什么单调栈有效
单调栈的高效性来源于它对问题特性的精准把握:
- 局部性原理:当前温度只需要与最近的几个较低温度比较
- 信息复用:已经处理过的温度如果比当前温度高,就不会影响后续判断
- 方向性:温度变化是单向时间序列,只需向后查找
4. Java实现与逐行解析
4.1 完整代码实现
public int[] dailyTemperatures(int[] temperatures) { int n = temperatures.length; int[] answer = new int[n]; Deque<Integer> stack = new ArrayDeque<>(); 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); } return answer; }4.2 关键操作解析
- 栈初始化:使用
ArrayDeque而非Stack类,因为前者在Java中性能更好 - 温度比较:
temperatures[i] > temperatures[stack.peek()]是触发条件 - 索引计算:
i - prevIndex得到等待天数 - 入栈时机:每个索引最终都会入栈,等待后续更高温度
4.3 边界条件处理
- 空输入:返回空数组
- 所有温度相同:返回全0数组
- 温度持续下降:返回全0数组
- 温度持续上升:返回[1,1,...,0]的数组
5. 复杂度分析与优化验证
5.1 时间复杂度证明
虽然代码中有嵌套循环,但每个元素最多入栈和出栈各一次,因此:
- 时间复杂度:O(n)
- 空间复杂度:O(n)(最坏情况下所有温度递减,栈需要存储所有索引)
5.2 实际性能测试
使用10⁵规模的随机温度数据进行测试:
- 暴力解法:超时(>2s)
- 单调栈解法:约15ms
- 内存消耗:约50MB(与暴力解法相当)
6. 单调栈的变种与应用
6.1 相似题目推荐
- LeetCode 496:下一个更大元素 I
- LeetCode 503:下一个更大元素 II(循环数组)
- LeetCode 84:柱状图中最大的矩形
- LeetCode 42:接雨水
6.2 实际工程应用场景
- 股票价格分析(寻找下一个更高价)
- 系统监控(寻找异常峰值)
- 推荐系统(寻找兴趣拐点)
- 时序数据库查询优化
7. 面试技巧与常见误区
7.1 面试回答策略
- 先陈述暴力解法并分析复杂度
- 指出性能瓶颈和优化方向
- 引入单调栈概念并解释适用性
- 写出代码并验证边界条件
- 讨论时间/空间复杂度和优化空间
7.2 常见错误警示
- 存储温度值而非索引(无法计算天数差)
- 错误设置单调性方向(应递减而非递增)
- 忽略栈空检查导致NPE
- 天数计算错误(应使用索引差而非简单递增)
- 未初始化结果数组(默认值不为0)
8. 进阶思考与扩展
8.1 并行化可能性
对于超大规模数据(如n>10⁷),可以考虑:
- 数据分片处理
- 使用并行流(parallel stream)
- GPU加速计算
8.2 空间优化思路
如果允许修改输入数组,可以使用原数组的部分空间存储中间结果,将空间复杂度降低到O(1)。
8.3 温度预测应用
将算法扩展为预测模型:
- 结合历史数据建立温度变化模型
- 使用单调栈识别关键转折点
- 基于模式匹配进行预测
在实际编码练习中,我发现很多初学者容易陷入两个极端:要么过度依赖IDE的调试功能,逐步跟踪栈变化;要么完全不画图,纯靠想象。建议在纸上画出温度曲线和栈的变化过程,这种可视化方法能显著提高对算法本质的理解。例如对于输入[73,74,75,71,69,72,76,73],可以这样分析:
- 初始化空栈和结果数组[0,0,0,0,0,0,0,0]
- i=0(73):栈[0],无操作
- i=1(74):弹出0,res[0]=1-0=1 → 栈[1]
- i=2(75):弹出1,res[1]=2-1=1 → 栈[2]
- i=3(71):栈[2,3]
- i=4(69):栈[2,3,4]
- i=5(72):弹出4(res[4]=5-4=1),弹出3(res[3]=5-3=2) → 栈[2,5]
- i=6(76):弹出5(res[5]=6-5=1),弹出2(res[2]=6-2=4) → 栈[6]
- i=7(73):栈[6,7]
- 最终结果:[1,1,4,2,1,1,0,0]
这种逐步推演的方法虽然耗时,但对于彻底理解算法工作原理非常有效。当你能不借助任何工具完整推演出中等规模案例的正确结果时,就说明真正掌握了这个算法。