1. 问题背景与核心挑战
每日温度问题(LeetCode 739)是算法面试中的经典栈应用场景。给定一个温度列表temperatures,要求返回一个列表,表示需要等待多少天才能观测到更高温度。如果气温在这之后都不会升高,则在该位置用0来代替。
这个问题看似简单,但考察了以下几个关键能力:
- 对单调栈这种特殊数据结构的理解
- 将实际问题转化为栈操作的能力
- 时间复杂度优化的意识(暴力解法O(n²) vs 栈解法O(n))
我在准备算法面试时,这个问题曾经困扰了我整整两天。最初用暴力解法通过后,发现运行时间排名垫底,这才意识到必须掌握更优解。下面分享我从暴力解法到最优解的完整思考过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法:直观但低效的实现
2.1 基本思路
最直观的解法是对每个温度,向后遍历直到找到第一个更高的温度:
python复制def dailyTemperatures(temperatures):
n = len(temperatures)
answer = [0] * n
for i in range(n):
for j in range(i+1, n):
if temperatures[j] > temperatures[i]:
answer[i] = j - i
break
return answer
2.2 复杂度分析
这种解法的时间复杂度是O(n²),空间复杂度O(1)(不考虑输出数组)。对于LeetCode的测试用例虽然能通过,但当输入规模达到10⁵时就会超时。
提示:在面试中,即使能快速写出暴力解法,也应该主动说明其复杂度缺陷,并表达优化意向。这展示了你的算法思维层次。
2.3 优化方向
观察发现暴力解法存在大量重复比较:
- 对于温度序列[73,74,75,71,69,72,76]
- 计算73时需要比较74,75...
- 计算74时又重复比较了75...
如果能记住之前的比较信息,就能避免重复工作。这正是栈数据结构的用武之地。
3. 单调栈解法:时间与空间的完美平衡
3.1 单调栈的核心思想
单调栈是一种特殊的栈,其中的元素保持单调递增或递减的顺序。对于每日温度问题,我们使用单调递减栈:
- 栈中存储的是温度的索引而非温度值
- 保持栈顶到栈底温度递减
- 遇到更高温度时,弹出栈顶元素并计算天数差
3.2 完整实现代码
python复制def dailyTemperatures(temperatures):
stack = []
answer = [0] * len(temperatures)
for i, temp in enumerate(temperatures):
while stack and temperatures[stack[-1]] < temp:
prev_index = stack.pop()
answer[prev_index] = i - prev_index
stack.append(i)
return answer
3.3 逐步推演示例
以输入[73,74,75,71,69,72,76]为例:
| 步骤 | 当前温度 | 栈状态(索引) | 操作 | answer数组变化 |
|---|---|---|---|---|
| 1 | 73 | [] | 压入0 | [0,0,0,0,0,0,0] |
| 2 | 74 | [0] | 弹出0,计算0位置答案 | [1,0,0,0,0,0,0] |
| 3 | 75 | [1] | 弹出1,计算1位置答案 | [1,1,0,0,0,0,0] |
| 4 | 71 | [2] | 压入3 | [1,1,0,0,0,0,0] |
| 5 | 69 | [2,3] | 压入4 | [1,1,0,0,0,0,0] |
| 6 | 72 | [2,3,4] | 弹出4,计算4位置答案 | [1,1,0,0,1,0,0] |
| 7 | 72 | [2,3] | 弹出3,计算3位置答案 | [1,1,0,2,1,0,0] |
| 8 | 76 | [2] | 弹出2,计算2位置答案 | [1,1,4,2,1,0,0] |
3.4 复杂度分析
时间复杂度:每个元素最多入栈和出栈一次,因此是O(n)
空间复杂度:最坏情况下栈需要存储所有元素,O(n)
4. 边界条件与常见错误
4.1 空输入处理
虽然题目保证输入非空,但好的习惯是添加边界检查:
python复制if not temperatures:
return []
4.2 相同温度的处理
题目要求"更高温度",因此等于当前温度时不弹出栈。这是常见错误点:
python复制# 错误写法:使用<=会导致错误
while stack and temperatures[stack[-1]] <= temp: # 错误!
4.3 栈溢出风险
Python的列表实现栈时理论上不会溢出,但在其他语言如C++中需要注意:
cpp复制// C++中需要预先分配足够空间或使用动态数据结构
stack<int> st;
st.reserve(temperatures.size()); // 预分配
5. 算法变种与扩展思考
5.1 前向vs后向遍历
我们采用的是前向遍历(从左到右),也可以实现后向遍历版本:
python复制def dailyTemperatures_backward(temperatures):
n = len(temperatures)
answer = [0] * n
stack = []
for i in range(n-1, -1, -1):
while stack and temperatures[i] >= temperatures[stack[-1]]:
stack.pop()
if stack:
answer[i] = stack[-1] - i
stack.append(i)
return answer
5.2 类似问题举一反三
掌握单调栈后,可以解决一系列类似问题:
- LeetCode 496 下一个更大元素 I
- LeetCode 503 下一个更大元素 II
- LeetCode 84 柱状图中最大的矩形
- LeetCode 42 接雨水
5.3 实际应用场景
这种模式在现实中有诸多应用:
- 股票分析(寻找下一个更高股价)
- 天气预报(未来更热/更冷的天数)
- 资源调度(等待更高优先级任务)
6. 性能优化与语言特性
6.1 Python中的优化技巧
使用预分配数组和内置函数可以略微提升性能:
python复制def dailyTemperatures_opt(temperatures):
n = len(temperatures)
answer = [0] * n
stack = []
for i in range(n):
current_temp = temperatures[i]
while stack and temperatures[stack[-1]] < current_temp:
prev = stack.pop()
answer[prev] = i - prev
stack.append(i)
return answer
6.2 其他语言实现对比
Java实现示例(注意栈的包装类处理):
java复制public int[] dailyTemperatures(int[] temperatures) {
int[] answer = new int[temperatures.length];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < temperatures.length; i++) {
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
int prev = stack.pop();
answer[prev] = i - prev;
}
stack.push(i);
}
return answer;
}
7. 调试技巧与测试用例设计
7.1 有效的测试用例
设计测试用例时应考虑:
- 普通情况:[73,74,75,71,69,72,76]
- 全部相同温度:[30,30,30]
- 单调递增:[1,2,3,4]
- 单调递减:[4,3,2,1]
- 空输入:[]
- 单元素输入:[100]
7.2 调试打印技巧
在算法中添加调试打印:
python复制def dailyTemperatures_debug(temperatures):
stack = []
answer = [0] * len(temperatures)
for i, temp in enumerate(temperatures):
print(f"\nDay {i}, Temperature: {temp}")
print(f"Before processing - Stack: {stack}")
while stack and temperatures[stack[-1]] < temp:
prev_index = stack.pop()
answer[prev_index] = i - prev_index
print(f" Found warmer day for index {prev_index}: wait {i - prev_index} days")
stack.append(i)
print(f"After processing - Stack: {stack}, Answer: {answer}")
return answer
7.3 可视化调试工具
推荐使用:
- Python Tutor (pythontutor.com)
- LeetCode的调试功能
- VS Code的调试器配合可视化插件
8. 面试技巧与常见问题
8.1 面试官可能追问的问题
- 为什么选择栈而不是其他数据结构?
- 如何处理输入非常大的情况?
- 如何证明你的算法是正确的?
- 这个算法的时间复杂度是多少?如何计算?
- 能否用递归实现?会有什么问题?
8.2 回答策略
对于"为什么选择栈"这个问题,可以这样回答:
"栈的LIFO特性非常适合这种需要比较最近元素的场景。具体到这个问题:
- 我们需要记住尚未找到更高温度的那些天的索引
- 当遇到更高温度时,最近的那些天是最先被满足的
- 栈的弹出操作正好可以处理这种'最近优先'的逻辑
- 保持栈单调递减可以确保每次比较都是必要的"
8.3 白板编程技巧
在白板上写代码时:
- 先写出函数签名和返回类型
- 注释算法步骤
- 最后填充代码细节
- 留出空间画示例推演
9. 进阶思考:分布式处理海量数据
如果温度数据量极大(如全球气象站数据),如何分布式处理?
9.1 MapReduce思路
- 按日期范围分区
- 每个分区内使用单调栈算法
- 合并时处理分区边界情况
9.2 流式处理方案
对于实时数据流:
- 使用时间窗口
- 维护多个栈对应不同时间窗口
- 定期合并结果
10. 个人实战经验分享
在多次面试中遇到这个问题后,我总结了以下经验:
-
先写暴力解法:即使知道更优解,也先写出暴力解法并分析其缺点,展示思考过程
-
画图辅助:在白板上画出温度曲线和栈的变化,更容易发现规律
-
边界测试:特别注意空输入、全相同温度、单调序列等情况
-
语言细节:不同语言实现时注意:
- Python中列表作为栈
- Java中使用Deque而非Stack类
- C++注意栈容量
-
变种准备:掌握问题的各种变种,如循环数组版本(LeetCode 503)
最后记住,面试官考察的不仅是正确解法,更是你解决问题的思考过程。即使不能立即想到最优解,通过清晰的沟通和逐步优化,也能留下好印象。
