1. 问题背景与核心需求
今天想和大家分享一道经典的LeetCode中等难度题目——739.每日温度。这道题看似简单,但背后蕴含着单调栈这一精妙的数据结构应用。作为面试中的高频考点,掌握这道题的解法不仅能帮助我们应对技术面试,更能提升对栈这一基础数据结构的理解深度。
题目要求我们:给定一个整数数组temperatures,表示每天的温度,返回一个数组answer,使得answer[i]表示在第i天之后需要等待多少天才能遇到更高的温度。如果之后没有更高的温度,则answer[i]为0。
举个例子:
- 输入:[73,74,75,71,69,72,76,73]
- 输出:[1,1,4,2,1,1,0,0]
这个输出表示:
- 第0天温度73,第二天74就升高了,所以等待1天
- 第2天温度75,需要等待4天(到第6天76)才遇到更高温度
- 最后两天后面没有更高温度,所以结果为0
2. 暴力解法与性能瓶颈
2.1 直观解法分析
最直观的解法是对于每一天i,向后遍历查找第一个比temperatures[i]大的温度:
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²),因为对于每个元素都要进行线性扫描。当n=10⁵时,这样的解法在LeetCode上会超时。
2.2 性能瓶颈分析
暴力解法的核心问题在于重复计算。比如对于温度序列[30,31,32,33,...,100],每个温度都需要一直扫描到最后才能找到更高的温度,导致时间复杂度退化到最坏的O(n²)。
提示:在算法设计中,当我们发现暴力解法存在大量重复计算时,通常可以考虑使用空间换时间的策略,通过额外的数据结构存储中间结果来优化性能。
3. 单调栈解法详解
3.1 单调栈的核心思想
单调栈是一种特殊的栈结构,它在普通栈的基础上维护栈内元素的单调性(递增或递减)。对于本题,我们使用单调递减栈:
- 栈底到栈顶:温度值递减
- 栈中存储的是温度的索引而非温度值本身
为什么选择单调递减栈?因为我们需要找到"下一个更高温度",而递减栈可以保证栈顶元素是最小的,也就是最容易被"打破"的。
3.2 算法流程拆解
让我们用一个具体例子来理解算法流程。假设输入温度为[73,74,75,71,69,72,76,73]:
- 初始化:answer = [0,0,0,0,0,0,0,0],stack = []
- i=0, temp=73:
- 栈空,直接入栈:stack = [0]
- i=1, temp=74:
- 74 > 73(temperatures[stack[-1]]),弹出0,answer[0]=1-0=1
- 栈空,入栈1:stack = [1]
- i=2, temp=75:
- 75 > 74,弹出1,answer[1]=2-1=1
- 栈空,入栈2:stack = [2]
- i=3, temp=71:
- 71 ≤ 75,直接入栈:stack = [2,3]
- i=4, temp=69:
- 69 ≤ 71,直接入栈:stack = [2,3,4]
- i=5, temp=72:
- 72 > 69,弹出4,answer[4]=5-4=1
- 72 > 71,弹出3,answer[3]=5-3=2
- 72 ≤ 75,停止弹出,入栈5:stack = [2,5]
- i=6, temp=76:
- 76 > 72,弹出5,answer[5]=6-5=1
- 76 > 75,弹出2,answer[2]=6-2=4
- 栈空,入栈6:stack = [6]
- i=7, temp=73:
- 73 ≤ 76,直接入栈:stack = [6,7]
- 最终answer = [1,1,4,2,1,1,0,0]
3.3 为什么单调栈高效
单调栈的高效性体现在:
- 每个元素最多入栈一次、出栈一次
- 通过维护单调性,可以批量处理多个等待中的温度
- 将时间复杂度从O(n²)优化到O(n)
4. 代码实现与语言特性
4.1 Python实现详解
python复制from typing import List
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
answer = [0] * n
stack = [] # 存储索引而非温度值
for current_day, current_temp in enumerate(temperatures):
# 当前温度比栈顶温度高时,更新答案
while stack and current_temp > temperatures[stack[-1]]:
previous_day = stack.pop()
answer[previous_day] = current_day - previous_day
stack.append(current_day)
return answer
Python实现要点:
- 使用enumerate同时获取索引和温度值
- while循环持续处理所有可以被当前温度"解决"的栈顶元素
- 栈中存储的是天数索引,便于计算天数差
4.2 Java实现特点
java复制class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] answer = new int[n];
Deque<Integer> stack = new ArrayDeque<>();
for (int currentDay = 0; currentDay < n; currentDay++) {
while (!stack.isEmpty() &&
temperatures[currentDay] > temperatures[stack.peek()]) {
int previousDay = stack.pop();
answer[previousDay] = currentDay - previousDay;
}
stack.push(currentDay);
}
return answer;
}
}
Java实现注意事项:
- 使用Deque接口的ArrayDeque实现作为栈
- 注意泛型类型为Integer而非int
- peek()查看栈顶而不弹出,pop()移除并返回栈顶
4.3 Rust实现特性
rust复制impl Solution {
pub fn daily_temperatures(temperatures: Vec<i32>) -> Vec<i32> {
let n = temperatures.len();
let mut answer = vec![0; n];
let mut stack: Vec<usize> = Vec::new();
for (current_day, ¤t_temp) in temperatures.iter().enumerate() {
while let Some(&previous_day) = stack.last() {
if current_temp > temperatures[previous_day] {
stack.pop();
answer[previous_day] = (current_day - previous_day) as i32;
} else {
break;
}
}
stack.push(current_day);
}
answer
}
}
Rust实现关键点:
- 使用Vec作为栈结构
- while let语法处理Option类型
- 显式类型转换(current_day - previous_day) as i32
- 借用检查器确保安全访问
5. 复杂度分析与边界情况
5.1 时间复杂度分析
- 每个元素最多入栈一次、出栈一次
- 内层while循环的总次数不超过n次
- 因此整体时间复杂度为O(n)
5.2 空间复杂度分析
- 最坏情况下,所有温度递减,栈需要存储所有索引
- 因此空间复杂度为O(n)
5.3 边界情况处理
需要特别注意的边界情况:
- 空输入:应返回空数组
- 单元素输入:应返回[0]
- 完全递减的温度序列:所有answer[i]=0
- 完全递增的温度序列:所有answer[i]=1
6. 单调栈的变种与应用
6.1 相关LeetCode题目
单调栈可以解决一系列类似问题:
- 496.下一个更大元素I
- 503.下一个更大元素II(循环数组)
- 84.柱状图中最大的矩形
- 42.接雨水
6.2 单调栈的变种
根据问题需求,单调栈可以有多种变体:
- 单调递增栈:用于寻找下一个更小元素
- 存储值或索引:根据是否需要计算位置差决定
- 循环数组处理:通过两次遍历或取模运算
6.3 实际应用场景
单调栈在以下场景中有实际应用:
- 股票分析:寻找股价突破点
- 天气预报:温度变化趋势分析
- 数据库查询:范围查询优化
- 编译器设计:语法分析中的符号匹配
7. 常见错误与调试技巧
7.1 常见错误类型
- 栈中存储温度值而非索引:导致无法计算天数差
- 循环条件错误:遗漏栈空检查或温度比较方向错误
- 结果数组未初始化:导致部分位置结果不正确
- 边界条件处理不当:如空输入或单元素输入
7.2 调试技巧
- 小规模测试用例:先用简单例子验证基本逻辑
- 打印栈状态:在每次操作后打印栈内容辅助理解
- 可视化流程:画图辅助理解温度比较过程
- 单元测试:编写针对各种边界情况的测试
注意:在实现单调栈时,建议先用纸笔模拟一个小例子,确保完全理解算法流程后再开始编码,这样可以避免很多低级错误。
8. 性能优化与进阶思考
8.1 可能的优化方向
- 空间优化:某些语言中栈的实现可能有优化空间
- 并行处理:对于超大数组,可以考虑分段处理
- 预处理:对于多次查询的情况,可以预处理结果
8.2 算法选择考量
选择单调栈而非暴力解法时需要考虑:
- 数据规模:n较大时单调栈优势明显
- 查询需求:是否需要多次查询类似问题
- 扩展性:是否需要解决更复杂的变种问题
8.3 数学视角理解
从数学角度看,单调栈实际上是在维护一个函数的极值点序列。对于温度序列T,我们寻找的是每个T[i]的"下一个极大值点",这与数学中的极值分析有密切联系。