1. 项目概述:每日温度问题解析
"739.每日温度"是一个经典的算法问题,通常出现在编程面试和技术能力评估中。这个问题要求我们根据每日温度列表,计算需要等待多少天才能等到更温暖的气温。对于没有 warmer 温度的情况,我们使用 0 来代替。
这个问题看似简单,但实际上考察了程序员对数据结构的理解和应用能力。它不仅是LeetCode上的第739题,更是栈(stack)数据结构的典型应用场景。在实际开发中,类似的模式可以应用于股票价格分析、资源调度优化等多种场景。
2. 问题分析与解法思路
2.1 问题详细描述
给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是指对于第 i 天,至少需要等待多少天才能遇到更暖和的气温。如果在这之后气温都不会升高,则在该位置用 0 来代替。
示例:
输入:temperatures = [73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]
2.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²),对于大规模数据效率较低。
2.3 最优解法思路
更高效的解法是使用单调栈(Monotonic Stack)。单调栈是一种特殊的栈,它可以帮助我们在O(n)时间复杂度内解决这类"下一个更大元素"的问题。
基本思路是:
- 初始化一个空栈和一个与输入数组等长的结果数组(初始值为0)
- 遍历温度数组,对于每个温度:
- 当栈不为空且当前温度大于栈顶温度时:
- 弹出栈顶元素
- 计算当前索引与弹出元素索引的差值,存入结果数组对应位置
- 将当前温度索引入栈
- 当栈不为空且当前温度大于栈顶温度时:
- 最后返回结果数组
3. 单调栈解法实现
3.1 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.2 代码解析
-
初始化:
stack: 用于存储尚未找到更高温度的日期索引answer: 初始化结果数组,所有元素设为0
-
遍历过程:
- 对于每个温度,检查栈顶元素对应的温度是否小于当前温度
- 如果是,则计算天数差并更新结果,直到栈为空或栈顶温度不小于当前温度
- 将当前索引入栈
-
时间复杂度分析:
- 每个元素最多入栈和出栈一次,因此时间复杂度为O(n)
- 空间复杂度为O(n)(最坏情况下需要存储所有元素)
3.3 示例演示
以输入[73,74,75,71,69,72,76,73]为例:
- 初始:stack=[], answer=[0,0,0,0,0,0,0,0]
- i=0(73): stack=[0]
- i=1(74): 73<74 → answer[0]=1-0=1, stack=[1]
- i=2(75): 74<75 → answer[1]=2-1=1, stack=[2]
- i=3(71): 75>71 → stack=[2,3]
- i=4(69): 71>69 → stack=[2,3,4]
- i=5(72):
- 69<72 → answer[4]=5-4=1, stack=[2,3]
- 71<72 → answer[3]=5-3=2, stack=[2]
- i=6(76): 75<76 → answer[2]=6-2=4, stack=[6]
- i=7(73): 76>73 → stack=[6,7]
- 结束,answer=[1,1,4,2,1,1,0,0]
4. 算法优化与变种
4.1 空间优化
我们可以尝试复用输入数组来存储结果,但考虑到实际应用中输入数据可能还需要保留,这种优化通常不推荐。
4.2 从右向左遍历
另一种实现方式是逆向遍历数组:
python复制def dailyTemperatures(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
这种方法同样保持O(n)时间复杂度,但遍历顺序不同。
4.3 类似问题扩展
- 下一个更大元素I(LeetCode 496)
- 下一个更大元素II(LeetCode 503) - 循环数组情况
- 柱状图中最大的矩形(LeetCode 84) - 需要同时处理左右边界
5. 实际应用场景
5.1 股票市场分析
这个问题模式可以应用于股票价格分析,计算买入后需要等待多少天股价才会超过当前价格,帮助投资者制定交易策略。
5.2 资源调度
在云计算资源调度中,可以用于预测何时会有更高配置的资源可用,以便进行任务调度。
5.3 天气预报系统
在气象预报系统中,可以用于分析温度变化趋势,预测何时会出现温度回升。
6. 常见问题与调试技巧
6.1 边界条件处理
常见错误包括:
- 忘记初始化结果数组
- 数组越界访问
- 最后一个元素处理不当
调试建议:
- 打印栈的状态和中间结果
- 使用小规模测试用例逐步验证
6.2 性能问题
虽然单调栈解法已经是O(n)时间复杂度,但对于极大输入仍可能遇到性能问题。可以考虑:
- 使用更高效的语言实现(如C++)
- 优化栈操作,减少不必要的内存分配
6.3 错误模式分析
常见错误实现:
- 混淆温度和索引
- 错误计算天数差(应该是当前i减去栈顶元素,而非相反)
- 忘记处理栈中剩余元素(实际上不需要处理,因为初始结果已经是0)
7. 复杂度分析与证明
7.1 时间复杂度证明
每个元素恰好入栈一次,最多出栈一次。对于n个元素,总操作次数为2n,因此时间复杂度为O(n)。
7.2 空间复杂度分析
最坏情况下,所有元素按降序排列,全部入栈,空间复杂度为O(n)。平均情况下空间复杂度也是O(n)。
8. 不同语言实现对比
8.1 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 prevIndex = stack.pop();
answer[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return answer;
}
8.2 C++实现
cpp复制vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> answer(temperatures.size(), 0);
stack<int> s;
for (int i = 0; i < temperatures.size(); ++i) {
while (!s.empty() && temperatures[s.top()] < temperatures[i]) {
answer[s.top()] = i - s.top();
s.pop();
}
s.push(i);
}
return answer;
}
8.3 JavaScript实现
javascript复制function dailyTemperatures(temperatures) {
const answer = new Array(temperatures.length).fill(0);
const stack = [];
for (let i = 0; i < temperatures.length; i++) {
while (stack.length > 0 && temperatures[stack[stack.length-1]] < temperatures[i]) {
const prevIndex = stack.pop();
answer[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return answer;
}
9. 测试用例设计
9.1 常规测试用例
python复制# 正常情况
assert dailyTemperatures([73,74,75,71,69,72,76,73]) == [1,1,4,2,1,1,0,0]
# 温度持续下降
assert dailyTemperatures([30,20,10]) == [0,0,0]
# 温度持续上升
assert dailyTemperatures([10,20,30]) == [1,1,0]
9.2 边界测试用例
python复制# 空输入
assert dailyTemperatures([]) == []
# 单日温度
assert dailyTemperatures([50]) == [0]
# 所有温度相同
assert dailyTemperatures([20,20,20]) == [0,0,0]
9.3 大规模数据测试
python复制# 大数组测试
import random
large_input = [random.randint(30, 100) for _ in range(10**5)]
# 主要测试是否能在合理时间内完成
result = dailyTemperatures(large_input)
10. 总结与个人经验
在实际面试中遇到这个问题时,建议按照以下步骤进行:
- 首先明确问题要求,确认输入输出
- 提出暴力解法并分析其复杂度
- 思考优化方向,引出单调栈的概念
- 详细解释单调栈的工作原理
- 逐步编写代码,注意边界条件
- 提供测试用例验证代码正确性
我在实际编码中发现,使用单调栈时最容易犯的错误是混淆温度和索引。一个实用的调试技巧是在循环中打印栈的内容和当前处理的位置,这能快速定位问题。
对于算法初学者,我建议从暴力解法开始理解问题本质,然后再思考优化。理解为什么单调栈能提高效率比单纯记住解法更重要。可以尝试在纸上模拟算法执行过程,这能加深对栈操作的理解。
