单调栈学习C++

目录

一,每日温度

二,下一个更大的元素I

三,下一个更大的元素II

四,接雨水

小结:


 单调栈是一种特殊的栈结构,里面的元素按照单调递增或者递减的顺序排列。常用于解决元素左边或者右边比它大或者小的问题。

一,每日温度

题目链接:739. 每日温度 - 力扣(LeetCode)

【题目描述】

 对于给定的一个temperatures数组,每个元素表示当天的温度。对于每天的温度,求出下一次更高的温度出现在几天后。

【算法】单调栈

我们可以维护一个栈结构,先将数组的首元素入栈,然后开始遍历这个数组,如果遍历到的数组元素比栈顶元素小,那么就入栈;如果相等,也入栈;当遍历到的数组元素比此时栈顶的元素大时,记录此时相隔的天数,然后将栈顶元素弹出,继续比较栈顶的元素和数组元素的大小,直到栈顶元素小于或者等于栈顶元素,此时将该数组元素入栈。然后接着遍历下一个数组元素,依次循环。

由于题目中求的是相隔的天数,所以我们不需要将数组元素入栈,只需入栈该元素的下标即可,这样就可以直接计算相隔的天数:数组元素对应的下标减去栈顶元素对应的下标,就是该栈顶元素与下一个更高温度相隔的天数。 

图示:(以题目中的示例1为例)

可以发现,栈中的元素(下标对应的元素)始终保持单调递减(从栈底到栈顶),因此成为单调栈。 

【代码】

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        int n=temperatures.size();
        vector<int> ans(n,0);
        stack<int> st;//存储下标
        st.push(0);
        for(int i=1;i<n;i++)
        {
            //遇到温度大于栈顶的,就一直出栈,保持递减性
            while(!st.empty()&&temperatures[st.top()]<temperatures[i])
            {
                int index=st.top();
                st.pop();
                ans[index]=i-index;
            }
            st.push(i);
        }
        return ans;
    }
};

时间复杂度O(N),每个元素最多入栈依次。空间复杂度O(N) 

二,下一个更大的元素I

题目链接:496. 下一个更大元素 I - 力扣(LeetCode)

【题目描述】

本题存在两个数组,nums1是nums2的子集,对于nums1数组的每个元素,求出这些元素在对应nums2数组中的下一个更大的元素。如果不存在,则为-1

【算法】单调栈

本题与上一题的思路一样,只不过加了一点要求。

求当前元素的下一个更大的元素,就需要维护一个单调栈(单调递减)。

本题还是求数组nums2上每个元素的下一个更大的元素是多少。所以还是在nums2数组上使用单调栈。和上题的过程一样,只不过在判断的时候,还需加上一个条件。

假设找到了当前栈顶的下一个更大元素k,还需判断栈顶元素是否在nums1中出现过,如果该元素在nums1中出现过,那么就将k记录在最终结果数组ans中,不过还需要保证和nums1数组的位置一一对应。

所以可以做下预处理工作,将数组nums1中的元素和下标使用哈希表保存起来。

初始化ans数组时,可以全部初始化为-1.

【代码】

class Solution {
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        int n=nums1.size();
        vector<int> ans(n,-1);
        unordered_map<int,int> hash;
        for(int i=0;i<n;i++)
        hash[nums1[i]]=i;
        
        stack<int> st;//单调递减
        st.push(0);
        for(int i=1;i<nums2.size();i++)
        {
            while(!st.empty()&&nums2[st.top()]<nums2[i])
            {
                //判断是否在nums1中出现过
                if(hash.count(nums2[st.top()])>0)
                {
                    //求出该元素在nums1中对应的下标
                    int index=hash[nums2[st.top()]];
                    //记录结果
                    ans[index]=nums2[i];
                }
                st.pop();
            }
            st.push(i);
        }
        return ans;
    }
};

三,下一个更大的元素II

题目链接:503. 下一个更大元素 II - 力扣(LeetCode)

【题目描述】

 求一个数组nums每一个元素的下一个更大的数,该数组是循环数组,数组最后一个元素接下来的元素就是数组的第一个元素。

【算法】单调栈

求下一个更大的元素,单调栈(单调递减栈)。

大致的方向还是使用单调栈。本题的重点是:如何处理循环数组。

方法一:是将原数组nums在后面再拼接一份。然后使用单调栈求下一个更大的元素。

将两个nums数组拼接起来,使用单调栈求出每一个元素的下一个更大值,存储在ans数组中,然后将ans数组的大小减为一半。

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        //拼接两个数组
        vector<int> nums1(nums.begin(),nums.end());
        nums.insert(nums.begin(),nums1.begin(),nums1.end());
        vector<int> ans(nums.size(),-1);

        stack<int> st;//单调递减
        st.push(0);
        for(int i=1;i<nums.size();i++)
        {
            while(!st.empty()&&nums[st.top()]<nums[i])
            {
                ans[st.top()]=nums[i];
                st.pop();
            }
            st.push(i);
        }
        //将数组减为原来的一般
        ans.resize(nums.size()/2);
        return ans;
    }
};

方法二:也可以不用扩充数组,在遍历的时候模拟走两边nums即可。

class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        int n=nums.size();
        vector<int> ans(n,-1);

        stack<int> st;//单调递减
        st.push(0);
        //模拟走两边nums
        //当遍历完最后一个元素,执行++后,再取模n,会回到数组的开始
        for(int i=1;i<2*n;i++)
        {
            while(!st.empty()&&nums[st.top()]<nums[i%n])
            {
                ans[st.top()]=nums[i%n];
                st.pop();
            }
            st.push(i%n);
        }
        return ans;
    }
};

四,接雨水

题目链接:42. 接雨水 - 力扣(LeetCode)

【解法一】双指针 

求这些柱子中一共有多少雨水。看每个柱子可以"接"多少雨水。

也就是统计每个柱子上有多少水,然后将每个柱子上的水加起来即可。

问题是如何求每个柱子上有多少水?

对于第i个柱子,求第i个柱子上有多少水,需要求出第i个柱子左边柱子最高的高度,以及第i个柱子右边柱子最高的高度,取两者的最小值,然后再减去当前柱子的高度。

【代码】

class Solution {
public:
    int trap(vector<int>& height) {
        //双指针
        int n=height.size();
        vector<int> maxLeft(n,0);
        auto maxRight=maxLeft;

        //记录左边最大值
        maxLeft[0]=height[0];
        for(int i=1;i<n;i++)
        maxLeft[i]=max(maxLeft[i-1],height[i]);

        //记录右边最大值
        maxRight[n-1]=height[n-1];
        for(int i=n-2;i>=0;i--)
        maxRight[i]=max(maxRight[i+1],height[i]);

        int ans=0;
        for(int i=0;i<n;i++)
        {
            int count=min(maxLeft[i],maxRight[i])-height[i];
            if(count>0) 
            ans+=count;
        }
        return ans;
    }
};

【解法二】单调队列

这种思路是求柱子与柱子之间的雨水量。

首先需要明白,当后一个柱子高度大于前一个柱子高度,那么一定是会有雨水的。

所以我们需要求下一个更大的元素,使用单调递减栈。

思路,与前面题的思路一致。


当遍历到的元素大于栈顶元素时:

此时的栈顶就是下图的中间柱子,下标记为mid,对应的高度为height[mid],将栈顶元素弹出。

此时的栈顶元素st.top(),就是最左边的柱子,对应的高度为height[st.top()]。

此时遍历到的元素是最右边的柱子,下标为i,柱子高度为height[i]。

最后只需计算出中间雨水的长和宽然后相乘即可。

h=min(height[st.top()],height[i])-height[mid]

w=i-st.top()-1

【代码】

class Solution {
public:
    int trap(vector<int>& height) {
        int n=height.size();
        int ans=0;

        stack<int> st;//单调递减
        st.push(0);
        for(int i=1;i<n;i++)
        {
            while(!st.empty()&&height[st.top()]<height[i])
            {
                int mid=st.top();
                st.pop();
                if(!st.empty())
                {
                    int h=min(height[st.top()],height[i])-height[mid];
                    int w=i-st.top()-1;
                    ans+=h*w;
                }
            }
            st.push(i);
        }
        return ans;
    }
};

小结:

单调栈常用于解决元素左侧或者右侧第一个更大后者更小的问题。

核心原理:

  • 单调递增栈:栈内元素从栈底到栈顶递增,用于寻找更小的元素。

  • 单调递减栈:栈内元素从栈底到栈顶递减,用于寻找更大的元素。

  • 遍历数组时,若当前元素破坏单调性,则弹出栈顶元素,直到满足单调性。

<think>嗯,用户想了解单调栈的实现和应用场景。从引用资料来看,之前已经提供了相关说明,但用户可能希望更系统化的总结。用户可能是算法学习者,正在准备面试或刷题,需要清晰且可直接应用的实例。注意到引用[1][2][3]都强调了单调栈的核心特点:保持栈内元素单调性,用于高效查找邻近更大/更小元素。引用[4][5]给出了经典题目(柱状图最大矩形)的代码实现,这些可以拆解说明。实现部分要突出三点:单调性维护(递增/递减栈)、出栈操作的含义(当前元素触发计算)、存储索引而非值的原因(方便计算边界)。应用场景需要区分典型问题类型:边界搜索、面积优化、跨度计算。用户可能特别关注“为什么用栈而不用其他结构”——需要解释后进先出特性与“最近邻”问题的适配性。代码示例选用C++和Python双版本,因为引用[4]是C++而用户历史问题含Python代码。注意在解释部分变量时保持一致性(如stk数组的top指针)。性能要明确O(n)的原因,避免用户误解退化情况。用户没提语言偏好,但上次回复含Python代码,所以优先Python示例。应用场景补充“矩阵最大矩形”的转化思路(引用[4]标题提到),这是单调栈的进阶用法,能展示该算法的扩展性。</think> 单调栈是一种特殊的数据结构,其核心在于**维护栈内元素的单调性**(单调递增或递减)。它能够高效解决一系列需查找元素“邻近边界”的问题,以下从实现和应用场景两方面详细说明: --- ### **一、单调栈实现** #### **核心思路** 1. **遍历顺序**:通常从左到右遍历数组 2. **栈内维护**:保持栈底到栈顶的单调性(增/减) 3. **出栈条件**:当新元素破坏单调性时,弹出栈顶元素并计算相关解 #### **代码示例(Python)** ```python def monotonic_stack(nums): stack = [] # 存储数组索引(通常比存值更灵活) res = [-1] * len(nums) # 初始化结果数组(例如存右侧第一个更大元素) for i in range(len(nums)): # 当栈非空且当前元素 > 栈顶元素(递减栈则用 <) while stack and nums[i] > nums[stack[-1]]: idx = stack.pop() # 弹出栈顶索引 res[idx] = nums[i] # 当前元素即为栈顶元素的"右侧更大元素" stack.append(i) # 当前元素索引入栈 return res # 示例:输入 [3, 1, 4, 2] # 输出: [4, 4, -1, -1] 表示:3右侧第一个大的是4,1右侧第一个大的是4,4和2右侧无更大元素 ``` #### **关键点** - **时间复杂为 $O(n)$**:每个元素最多入栈、出栈一次 - **栈中存储索引**:便于计算距离、区间宽度等 - **单调性选择**: - **递减栈**:找右侧第一个更大元素 - **递增栈**:找右侧第一个更小元素(如柱状图最大矩形) --- ### **二、经典应用场景** #### **1. 边界查找问题** - **下一个更大/小元素**(Next Greater Element) - 例:`[2,1,2,4,3]` → 下一个更大元素 `[4,2,4,-1,-1]` [^1][^5] - **左侧第一个更大/小元素**(类似反向遍历) #### **2. 面积优化问题** - **柱状图中最大矩形**(LeetCode 84)[^4] - **核心代码片段**(递增栈计算宽度): ```python stack = [-1] # 哨兵节点 max_area = 0 for i in range(len(heights)): while stack[-1] != -1 and heights[stack[-1]] > heights[i]: h = heights[stack.pop()] w = i - stack[-1] - 1 # 宽度 = 当前索引 - 新栈顶索引 - 1 max_area = max(max_area, h * w) stack.append(i) ``` - **最大矩形(01矩阵)**(LeetCode 85)[^4] - 转化为逐行的柱状图问题 #### **3. 跨度计算问题** - **每日温度**(LeetCode 739)[^5] - 求等待更暖天数的间隔:`[73,74,75,71,69,76]` → `[1,1,4,2,1,0]` - 解法:递减栈记录温度下标,出栈时计算天数差 #### **4. 删除重复字母(字典序最小)** - **关键点**:维护递增栈,处理字符最后出现位置 [^2] --- ### **单调栈适用条件** - 问题需找到**某一侧最近的边界元素**(更大/更小) - 计算过程需要**利用弹出元素的中间结果**(如面积、距离) - 数组遍历方向与边界查找方向一致(左→右找右边界,右→左找左边界) > 注:实际应用中,80% 单调栈问题可归结为以上场景。理解“单调性维护”和“边界触发计算”两个核心即可快速识别解法[^3][^5]。 --- **
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值