1. 题目解析与解题思路
LeetCode 739题"每日温度"是一道经典的单调栈应用题目。给定一个温度列表,要求返回一个列表,表示需要等待多少天才能等到更高的温度。这道题在面试中经常出现,因为它能很好地考察候选人对数据结构的理解和应用能力。
1.1 问题描述
给定一个整数数组temperatures,表示每天的温度,返回一个数组answer,其中answer[i]是指在第i天之后,才会有更高的温度。如果气温在这之后都不会升高,则在该位置用0来代替。
示例:
输入:temperatures = [73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]
1.2 暴力解法分析
最直观的解法是使用双重循环:
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
这种方法的时间复杂度是O(n²),在LeetCode上会超时,显然不是最优解。
2. 单调栈解法详解
2.1 单调栈的概念
单调栈是一种特殊的栈结构,它保证栈内元素始终保持单调递增或单调递减的顺序。在本题中,我们使用单调递减栈,即栈顶元素是最小的。
2.2 算法步骤
- 初始化一个空栈和一个与输入数组等长的结果数组,初始值全为0
- 遍历温度数组中的每一个元素:
a. 当前温度比栈顶温度高时,弹出栈顶元素,并计算天数差存入结果数组
b. 重复上述过程直到栈为空或当前温度不大于栈顶温度
c. 将当前温度的索引入栈 - 返回结果数组
2.3 Python实现代码
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.1 时间复杂度
虽然代码中有嵌套循环,但每个元素最多入栈和出栈各一次,所以时间复杂度是O(n),比暴力解法高效很多。
3.2 空间复杂度
最坏情况下,所有元素都会入栈,所以空间复杂度是O(n)。
4. 算法正确性证明
单调栈解法的正确性基于以下观察:
- 当遇到一个更高的温度时,它可以解决之前所有比它低的温度的等待天数问题
- 栈中保持单调递减的顺序,确保了我们总是处理最近的、尚未找到更高温度的日期
- 每个元素入栈时,它前面的所有温度都比它高,所以它只需要等待后面出现更高的温度
5. 边界条件与特殊测试用例
5.1 边界情况处理
- 空输入数组:应返回空数组
- 单元素数组:应返回[0]
- 所有温度相同:应返回全0数组
- 温度持续下降:应返回全0数组
5.2 测试用例示例
python复制assert dailyTemperatures([]) == []
assert dailyTemperatures([30]) == [0]
assert dailyTemperatures([30,30,30]) == [0,0,0]
assert dailyTemperatures([30,20,10]) == [0,0,0]
assert dailyTemperatures([30,40,50]) == [1,1,0]
6. 算法优化与变种
6.1 从右向左遍历的解法
除了从左向右使用单调栈,还可以从右向左遍历,使用类似动态规划的思想:
python复制def dailyTemperatures(temperatures):
n = len(temperatures)
answer = [0] * n
for i in range(n-2, -1, -1):
j = i + 1
while j < n and temperatures[j] <= temperatures[i]:
if answer[j] > 0:
j += answer[j]
else:
j = n
if j < n:
answer[i] = j - i
return answer
6.2 类似题目扩展
- LeetCode 496 - 下一个更大元素 I
- LeetCode 503 - 下一个更大元素 II
- LeetCode 84 - 柱状图中最大的矩形
- LeetCode 42 - 接雨水
7. 实际应用场景
每日温度问题的解法在实际中有广泛的应用:
- 股票价格分析:计算等待多少天后股价会超过当前价格
- 生产计划:确定原材料价格何时会高于当前采购价
- 气象预测:分析温度变化趋势
- 资源调度:预测何时资源需求会超过当前供给
8. 面试技巧与注意事项
8.1 面试常见问题
- 如何想到使用单调栈解法?
- 为什么单调栈的时间复杂度是O(n)?
- 如何处理没有更高温度的情况?
- 如何修改算法来返回温度值而不仅是天数?
8.2 解题思路表达
在面试中解释这道题时,建议按照以下步骤:
- 先描述暴力解法及其缺点
- 引入单调栈的概念和优势
- 详细说明算法步骤
- 分析时间复杂度和空间复杂度
- 讨论边界条件和特殊情况
8.3 常见错误与修正
-
错误:在栈中存储温度值而非索引
修正:存储索引可以同时访问温度和计算天数差 -
错误:忘记处理栈不为空但当前温度不更高的情况
修正:确保只在当前温度更高时才弹出栈顶元素 -
错误:结果数组初始化不正确
修正:初始化为全0数组,这样未处理的元素自动为0
9. 不同语言实现对比
9.1 Java实现
java复制public int[] dailyTemperatures(int[] temperatures) {
Stack<Integer> stack = new Stack<>();
int[] answer = new int[temperatures.length];
for (int i = 0; i < temperatures.length; i++) {
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
int prevIndex = stack.pop();
answer[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return answer;
}
9.2 C++实现
cpp复制vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> s;
vector<int> answer(temperatures.size(), 0);
for (int i = 0; i < temperatures.size(); ++i) {
while (!s.empty() && temperatures[s.top()] < temperatures[i]) {
int prev_index = s.top();
s.pop();
answer[prev_index] = i - prev_index;
}
s.push(i);
}
return answer;
}
10. 性能测试与优化
10.1 大规模数据测试
对于长度为10^5的温度数组,单调栈解法能在毫秒级完成,而暴力解法会超时。
10.2 优化建议
- 使用数组模拟栈可以减少栈操作的开销
- 对于特定范围的数据(如温度在[30,100]之间),可以使用桶排序的思路进一步优化
11. 可视化理解
为了更好地理解单调栈的工作过程,可以绘制温度曲线图:
- 将温度数组绘制成折线图
- 标记栈操作的位置
- 用箭头表示每个温度找到下一个更高温度的过程
这种可视化方法能帮助直观理解为什么单调栈能高效解决这个问题。
12. 数学原理深入
单调栈解法的背后是寻找每个元素的下一个更大元素,这类问题在数学上可以建模为:
- 对于序列中的每个元素x,找到最小的j>i使得T_j > T_i
- 这类似于在部分有序集合中搜索特定模式
单调栈有效地维护了这种部分有序性,使得我们不需要重复比较已经比较过的元素。
13. 实际编码中的调试技巧
- 打印栈状态:在每次操作前后打印栈内容
- 验证结果:手动计算几个位置的预期结果
- 边界测试:特别关注数组开头和结尾的处理
- 单步调试:使用调试器逐步执行观察变量变化
14. 单元测试编写建议
完整的单元测试应该包含:
python复制import unittest
class TestDailyTemperatures(unittest.TestCase):
def test_empty(self):
self.assertEqual(dailyTemperatures([]), [])
def test_single(self):
self.assertEqual(dailyTemperatures([30]), [0])
def test_all_same(self):
self.assertEqual(dailyTemperatures([30,30,30]), [0,0,0])
def test_increasing(self):
self.assertEqual(dailyTemperatures([30,40,50]), [1,1,0])
def test_decreasing(self):
self.assertEqual(dailyTemperatures([50,40,30]), [0,0,0])
def test_example(self):
self.assertEqual(dailyTemperatures([73,74,75,71,69,72,76,73]), [1,1,4,2,1,1,0,0])
if __name__ == '__main__':
unittest.main()
15. 学习路径建议
要掌握这类单调栈问题,建议的学习顺序:
- 理解栈的基本操作
- 学习单调栈的概念
- 练习简单单调栈问题(如LeetCode 496)
- 解决每日温度问题
- 挑战更复杂的单调栈应用(如LeetCode 84)
16. 代码风格与最佳实践
- 变量命名:使用有意义的变量名如stack、answer等
- 注释:对算法关键步骤添加简要注释
- 函数拆分:如果逻辑复杂,可以将栈操作拆分为辅助函数
- 异常处理:考虑输入为None等特殊情况
17. 相关数据结构比较
与单调栈类似的还有单调队列,它们的主要区别:
- 单调栈:后进先出,适合解决下一个更大元素问题
- 单调队列:先进先出,适合解决滑动窗口最值问题
理解这两者的区别有助于选择正确的数据结构解决问题。
18. 历史与背景
单调栈最早在1980年代被提出,用于解决几何和字符串匹配问题。后来发现它在处理序列中的顺序相关问题时非常高效,逐渐成为算法竞赛和面试中的常客。
19. 进阶挑战
对于已经掌握基本解法的同学,可以尝试以下挑战:
- 空间复杂度优化到O(1)(不使用额外空间)
- 处理循环数组的情况(如LeetCode 503)
- 同时计算前后更大的温度
- 处理三维温度数据(如每日不同城市的温度)
20. 个人心得
在实际编码和面试中,我发现以下几点特别重要:
- 先写暴力解法,再思考优化,这样思路更清晰
- 画图辅助理解单调栈的工作过程
- 特别注意索引边界条件,这是最容易出错的地方
- 多练习类似题目,培养对单调栈适用场景的敏感度