1. 题目背景与问题描述
"739.每日温度"是一道经典的算法题目,通常出现在编程面试和算法练习中。题目要求我们根据每日气温列表,计算需要等待多少天才能观测到更高的温度。如果气温在这之后都不会升高,则在该位置用0来代替。
这个问题在实际生活中有很多应用场景,比如:
- 农业领域预测作物生长周期
- 能源行业规划电力需求
- 旅游业安排最佳出行时间
- 商业决策中的季节性商品库存管理
2. 问题分析与解决思路
2.1 暴力解法分析
最直观的解决方法是使用双重循环:
- 外层循环遍历每一天的温度
- 内层循环从当前天的下一天开始,寻找第一个比当前温度高的天数
- 计算两者之间的天数差作为结果
这种方法的时间复杂度是O(n²),在数据量较大时效率较低。
2.2 优化思路 - 单调栈
更高效的解决方案是使用单调栈(Monotonic Stack):
- 维护一个栈,用于存储尚未找到更高温度的日期索引
- 遍历温度列表,对于每一天的温度:
- 当栈不为空且当前温度大于栈顶那天的温度时:
- 弹出栈顶元素
- 计算当前索引与弹出索引的差值,作为结果存入结果数组
- 将当前索引压入栈中
- 当栈不为空且当前温度大于栈顶那天的温度时:
- 最后栈中剩余的元素对应那些后面没有更高温度的日子,结果设为0
这种方法的时间复杂度是O(n),因为每个元素最多入栈和出栈一次。
3. 详细实现步骤
3.1 算法实现代码(Python)
python复制def dailyTemperatures(temperatures):
n = len(temperatures)
result = [0] * n
stack = []
for i in range(n):
while stack and temperatures[i] > temperatures[stack[-1]]:
prev_index = stack.pop()
result[prev_index] = i - prev_index
stack.append(i)
return result
3.2 代码解析
- 初始化结果数组result,长度与输入温度数组相同,初始值全为0
- 创建一个空栈stack,用于存储温度的索引
- 遍历温度数组:
- 当栈不为空且当前温度大于栈顶索引对应的温度时:
- 弹出栈顶索引prev_index
- 计算当前索引i与prev_index的差值,存入result[prev_index]
- 将当前索引i压入栈中
- 当栈不为空且当前温度大于栈顶索引对应的温度时:
- 遍历结束后,返回result数组
3.3 示例分析
以输入温度[73,74,75,71,69,72,76,73]为例:
初始状态:
stack = []
result = [0,0,0,0,0,0,0,0]
处理过程:
i=0, temp=73: stack=[0]
i=1, temp=74 > 73: result[0]=1-0=1, stack=[1]
i=2, temp=75 > 74: result[1]=2-1=1, stack=[2]
i=3, temp=71 < 75: stack=[2,3]
i=4, temp=69 < 71: stack=[2,3,4]
i=5, temp=72 > 69: result[4]=5-4=1, stack=[2,3]
temp=72 > 71: result[3]=5-3=2, stack=[2]
temp=72 < 75: stack=[2,5]
i=6, temp=76 > 72: result[5]=6-5=1, stack=[2]
temp=76 > 75: result[2]=6-2=4, stack=[]
stack=[6]
i=7, temp=73 < 76: stack=[6,7]
最终结果:[1,1,4,2,1,1,0,0]
4. 复杂度分析与优化
4.1 时间复杂度
- 每个元素最多被压入和弹出栈一次
- 总操作次数为2n次
- 时间复杂度为O(n)
4.2 空间复杂度
- 最坏情况下,所有元素都被压入栈中
- 空间复杂度为O(n)
4.3 优化技巧
- 可以使用元组同时存储温度和索引,避免多次访问原数组
- 对于某些特定分布的温度数据,可以提前终止循环
- 可以使用数组代替栈结构,在某些语言中可能获得更好的性能
5. 实际应用与扩展
5.1 实际应用场景
- 股票价格分析:计算等待股价上涨的天数
- 资源调度:预测资源需求高峰
- 生产计划:安排最佳生产时间窗口
- 气象研究:分析温度变化趋势
5.2 问题变种
- 计算等待更冷温度的天数
- 找到温度波动最大的时间段
- 计算温度持续高于某阈值的最长连续天数
- 结合湿度等其他气象因素的综合分析
6. 常见问题与解决方案
6.1 边界条件处理
注意处理空输入和单元素输入的情况:
- 空列表应返回空列表
- 单元素列表应返回[0]
6.2 性能优化
对于特别大的输入数据:
- 考虑使用更高效的数据结构
- 可以尝试并行处理
- 对于特定分布的数据,可以设计专门的优化算法
6.3 调试技巧
- 使用小规模数据手动验证
- 打印栈的状态和中间结果
- 检查索引计算是否正确
- 特别注意循环的终止条件
7. 不同语言实现对比
7.1 Java实现
java复制public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] result = new int[n];
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int prevIndex = stack.pop();
result[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return result;
}
7.2 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[i] > temperatures[s.top()]) {
int prev_index = s.top();
s.pop();
result[prev_index] = i - prev_index;
}
s.push(i);
}
return result;
}
7.3 JavaScript实现
javascript复制function dailyTemperatures(temperatures) {
const n = temperatures.length;
const result = new Array(n).fill(0);
const stack = [];
for (let i = 0; i < n; i++) {
while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const prevIndex = stack.pop();
result[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return result;
}
8. 算法可视化与理解
为了更好地理解单调栈的工作方式,我们可以将处理过程可视化:
- 初始状态:栈为空
- 遇到比栈顶温度高的天数时,弹出栈顶元素并计算结果
- 将当前天数压入栈中
- 重复上述过程直到遍历完所有天数
这种"后进先出"的处理方式确保了我们可以高效地找到第一个更高温度的天数。
9. 测试用例设计
全面的测试用例应该包括:
-
普通情况测试:
- 输入:[73,74,75,71,69,72,76,73]
- 预期输出:[1,1,4,2,1,1,0,0]
-
边界测试:
-
空列表输入:[]
-
预期输出:[]
-
单元素列表:[30]
-
预期输出:[0]
-
-
极端情况测试:
-
温度持续上升:[30,31,32,33,34]
-
预期输出:[1,1,1,1,0]
-
温度持续下降:[34,33,32,31,30]
-
预期输出:[0,0,0,0,0]
-
-
随机波动测试:
- 输入:[55,38,53,81,61,93,97,32,43,78]
- 预期输出:[3,1,1,2,1,1,0,1,1,0]
10. 性能对比实验
为了验证单调栈算法的效率优势,我们可以设计对比实验:
- 暴力解法与单调栈解法在不同数据规模下的运行时间对比
- 内存使用情况分析
- 最坏情况下(如单调递减温度序列)的性能表现
实验结果表明,随着数据规模增大,单调栈算法的优势更加明显。对于n=10^5量级的数据,暴力解法可能需要数秒甚至更长时间,而单调栈解法仍能在毫秒级完成。
