【算法精讲】单调栈解决 “每日温度” 问题:从暴力到线性时间优化

7 阅读6分钟

在算法面试和日常开发中,“下一个更大元素”类问题是高频考点,“每日温度”就是这类问题的经典代表。本文将从暴力解法的痛点出发,一步步拆解单调栈的核心思路、实现逻辑,并通过实战代码让你彻底掌握这种高效的解题方法。

一、问题描述

给定一个整数数组 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. 复杂度分析

  • 时间复杂度O(n2)O(n^2)。最坏情况下(数组严格递减),每个元素都要遍历到数组末尾,总操作数为 n+(n1)+...+1=n(n+1)/2n+(n-1)+...+1 = n(n+1)/2
  • 空间复杂度O(n)O(n)(仅存储结果数组)。

4. 问题所在

当输入数组长度很大(比如 n=105n=10^5)时,O(n2)O(n^2) 的时间复杂度会导致超时——这也是暴力解法的致命缺陷,我们需要更高效的算法。

三、优化方案:单调栈(Monotonic Stack)

1. 核心思想

单调栈是一种利用栈的特性维护元素单调性的技巧,核心是:用栈记录“未找到下一个更大元素的下标”,遍历数组时,遇到更大的元素就弹出栈顶并计算结果,最终栈中剩余元素的结果为0。 对于“每日温度”问题,我们维护一个单调递减栈(栈内下标对应的温度值严格递减): - 栈中存储的是「温度数组的下标」(而非温度值),方便计算天数差; - 遍历到第 i 天时,若当前温度 > 栈顶下标对应的温度,说明栈顶下标对应的那天找到了“下一个更高温度”,弹出栈顶并计算天数差; - 重复上述过程直到栈为空,或当前温度 ≤ 栈顶温度,再将当前下标压入栈; - 遍历结束后,栈中剩余下标对应的结果保持0(无更高温度)。

2. 可视化理解

temperatures = [73,74,75,71,69,72,76,73] 为例,模拟单调栈的执行过程:

遍历下标 i温度 temp [i]栈状态(存储下标)操作说明结果数组 result
073[] → [0]栈空,压入 0[0,0,0,0,0,0,0,0]
174[0] → [] → [1]74>73,弹出 0,result [0]=1-0=1;压入 1[1,0,0,0,0,0,0,0]
275[1] → [] → [2]75>74,弹出 1,result [1]=2-1=1;压入 2[1,1,0,0,0,0,0,0]
371[2] → [2,3]71<75,压入 3[1,1,0,0,0,0,0,0]
469[2,3] → [2,3,4]69<71,压入 4[1,1,0,0,0,0,0,0]
572[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]
676[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]
773[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. 复杂度分析

  • 时间复杂度O(n)O(n)。每个下标仅入栈和出栈一次,总操作数为 2n2n,属于线性时间。
  • 空间复杂度O(n)O(n)。最坏情况下(数组严格递减),栈会存储所有下标,空间开销为 O(n)O(n);结果数组也为 O(n)O(n)

四、单调栈的适用场景与扩展

1. 核心适用场景

单调栈主要解决“下一个更大/更小元素”类问题,常见场景: - 每日温度(下一个更高温度); - 接雨水(找左右第一个更高的柱子); - 柱状图中最大的矩形(找左右第一个更小的柱子); - 股票的最佳买卖时机(找下一个更高的价格)。

2. 关键技巧总结

  • 栈中存什么:优先存储「下标」而非「值」,方便计算距离/索引差;
  • 单调性选择
  • 找「下一个更大元素」→ 维护单调递减栈
  • 找「下一个更小元素」→ 维护单调递增栈
  • 初始化处理:结果数组直接初始化为默认值(如0),减少条件判断;
  • 循环逻辑:遍历过程中,通过while循环“清空”栈中比当前元素小/大的元素,再压入当前元素。 ## 五、总结 1. 暴力解法解决“每日温度”问题的时间复杂度为 O(n2)O(n^2),在大数据量下会超时; 2. 单调栈通过维护「未匹配下标」的单调性,将时间复杂度优化到 O(n)O(n),是解决“下一个更大元素”类问题的最优解; 3. 单调栈的核心是“用栈记录待处理元素,遍历过程中动态匹配结果”,关键在于选择栈的单调性(递增/递减)和存储内容(下标/值)。 掌握单调栈不仅能解决“每日温度”这类具体问题,更能建立“用空间换时间”的算法思维——这也是算法优化的核心思路之一。
  • 建议结合本文示例手动模拟栈的执行过程,加深对单调栈的理解。