在算法面试和日常开发中,“下一个更大元素”类问题是高频考点,“每日温度”就是这类问题的经典代表。本文将从暴力解法的痛点出发,一步步拆解单调栈的核心思路、实现逻辑,并通过实战代码让你彻底掌握这种高效的解题方法。
一、问题描述
给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 代替。
示例:
输入:temperatures = [73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]
解释:
- 第0天温度73,第1天74(更高),间隔1天 → answer[0]=1
- 第2天温度75,后续直到第6天才出现76(更高),间隔4天 → answer[2]=4
- 第6天温度76,后续无更高温度 → answer[6]=0
二、暴力解法:
思路简单但效率低下
1. 核心思路
对每一个元素 temperatures[i],向后遍历数组,找到第一个比它大的元素 temperatures[j],计算 j-i 作为结果;若遍历完都没找到,结果为0。
2. 代码实现
#include <vector>
using namespace std;
class Solution { public: vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> result(n, 0); // 遍历每一天
for (int i = 0; i < n; ++i) { // 向后找更高温度
for (int j = i + 1; j < n; ++j) {
if (temperatures[j] > temperatures[i]) {
result[i] = j - i; break; // 找到第一个就退出
}
}
}
return result;
}
};
3. 复杂度分析
- 时间复杂度:。最坏情况下(数组严格递减),每个元素都要遍历到数组末尾,总操作数为 。
- 空间复杂度:(仅存储结果数组)。
4. 问题所在
当输入数组长度很大(比如 )时, 的时间复杂度会导致超时——这也是暴力解法的致命缺陷,我们需要更高效的算法。
三、优化方案:单调栈(Monotonic Stack)
1. 核心思想
单调栈是一种利用栈的特性维护元素单调性的技巧,核心是:用栈记录“未找到下一个更大元素的下标”,遍历数组时,遇到更大的元素就弹出栈顶并计算结果,最终栈中剩余元素的结果为0。 对于“每日温度”问题,我们维护一个单调递减栈(栈内下标对应的温度值严格递减): - 栈中存储的是「温度数组的下标」(而非温度值),方便计算天数差; - 遍历到第 i 天时,若当前温度 > 栈顶下标对应的温度,说明栈顶下标对应的那天找到了“下一个更高温度”,弹出栈顶并计算天数差; - 重复上述过程直到栈为空,或当前温度 ≤ 栈顶温度,再将当前下标压入栈; - 遍历结束后,栈中剩余下标对应的结果保持0(无更高温度)。
2. 可视化理解
以 temperatures = [73,74,75,71,69,72,76,73] 为例,模拟单调栈的执行过程:
| 遍历下标 i | 温度 temp [i] | 栈状态(存储下标) | 操作说明 | 结果数组 result |
|---|---|---|---|---|
| 0 | 73 | [] → [0] | 栈空,压入 0 | [0,0,0,0,0,0,0,0] |
| 1 | 74 | [0] → [] → [1] | 74>73,弹出 0,result [0]=1-0=1;压入 1 | [1,0,0,0,0,0,0,0] |
| 2 | 75 | [1] → [] → [2] | 75>74,弹出 1,result [1]=2-1=1;压入 2 | [1,1,0,0,0,0,0,0] |
| 3 | 71 | [2] → [2,3] | 71<75,压入 3 | [1,1,0,0,0,0,0,0] |
| 4 | 69 | [2,3] → [2,3,4] | 69<71,压入 4 | [1,1,0,0,0,0,0,0] |
| 5 | 72 | [2,3,4] → [2,5] | 72>69 → 弹出 4,result [4]=5-4=1;72>71 → 弹出 3,result [3]=5-3=2;72<75 → 压入 5 | [1,1,0,2,1,0,0,0] |
| 6 | 76 | [2,5] → [] → [6] | 76>72 → 弹出 5,result [5]=6-5=1;76>75 → 弹出 2,result [2]=6-2=4;栈空,压入 6 | [1,1,4,2,1,1,0,0] |
| 7 | 73 | [6] → [6,7] | 73<76,压入 7 | [1,1,4,2,1,1,0,0] |
3. 代码实现
#include <vector>
#include <stack>
using namespace std;
class Solution {
public: vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> result(n, 0); // 初始化结果为0,省去后续判断
stack<int> st; // 单调栈:存储未找到更高温度的下标
for (int i = 0; i < n; ++i) { // 核心:当前温度 > 栈顶下标对应的温度 → 弹出栈顶并计算结果
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
int prev_idx = st.top(); // 取出未找到更高温度的那天的下标
st.pop();
result[prev_idx] = i - prev_idx; // 计算天数差
}
st.push(i); // 压入当前下标,等待后续匹配更高温度
}
return result;
}
};
4. 复杂度分析
- 时间复杂度:。每个下标仅入栈和出栈一次,总操作数为 ,属于线性时间。
- 空间复杂度:。最坏情况下(数组严格递减),栈会存储所有下标,空间开销为 ;结果数组也为 。
四、单调栈的适用场景与扩展
1. 核心适用场景
单调栈主要解决“下一个更大/更小元素”类问题,常见场景: - 每日温度(下一个更高温度); - 接雨水(找左右第一个更高的柱子); - 柱状图中最大的矩形(找左右第一个更小的柱子); - 股票的最佳买卖时机(找下一个更高的价格)。
2. 关键技巧总结
- 栈中存什么:优先存储「下标」而非「值」,方便计算距离/索引差;
- 单调性选择:
- 找「下一个更大元素」→ 维护单调递减栈;
- 找「下一个更小元素」→ 维护单调递增栈;
- 初始化处理:结果数组直接初始化为默认值(如0),减少条件判断;
- 循环逻辑:遍历过程中,通过while循环“清空”栈中比当前元素小/大的元素,再压入当前元素。 ## 五、总结 1. 暴力解法解决“每日温度”问题的时间复杂度为 ,在大数据量下会超时; 2. 单调栈通过维护「未匹配下标」的单调性,将时间复杂度优化到 ,是解决“下一个更大元素”类问题的最优解; 3. 单调栈的核心是“用栈记录待处理元素,遍历过程中动态匹配结果”,关键在于选择栈的单调性(递增/递减)和存储内容(下标/值)。 掌握单调栈不仅能解决“每日温度”这类具体问题,更能建立“用空间换时间”的算法思维——这也是算法优化的核心思路之一。
- 建议结合本文示例手动模拟栈的执行过程,加深对单调栈的理解。