Skip to content

Latest commit

 

History

History
257 lines (186 loc) · 13.7 KB

单调栈.md

File metadata and controls

257 lines (186 loc) · 13.7 KB

单调栈

0. 概念

顾名思义,单调栈就是单调的栈序列,有单调递减栈单调递增栈两种

  • 单调递减栈:从栈底到栈顶数据都是从大到小
  • 单调递增栈:从栈底到栈顶数据都是从小到大

其实说法不是唯一,各有各的说法,有的说栈顶到栈底,这里我统一理解成:栈底到栈顶,想象一个栈水平放置,也就是从左往右看

1. 应用

NGE

单调栈主要用于在线性时间复杂度内解决 Next Greater Element 问题,对序列中每个元素找到下一个比它大的元素。

例如对于数组 [2,1,2,4,3],我们计算出 NGE 数组为 [4,2,4,-1,-1]

第一个元素 2 后面比 2 大的数是 4,第二个元素 1 后面比其大的数也是 4。以此类推,对于 4 而言后面没有比其大的数因此填 -1

其实可以将NGE 问题抽象成:数组的元素表示并列站着的人,元素大小表示人的身高,看下图:[第一个元素2] 第一个看到的人就是 [元素4],[元素1] 第一个看到的人就是 [第二个元素2]

站立视野问题

再看代码:

//【推荐写法】从左往右看是非递增的
vector<int> NGE(vector<int>& h) {
    int n = h.size();
    vector<int> ans(n, -1);	// 初始化为 -1 表示未处理的就是 -1
    stack<int> st;	// 保存下标
    st.push(0); // 初始下标入栈

    // 1. h[st.top()] > h[i] 直接入栈形成非递增
    // 2. h[st.top()] == h[i] 直接入栈
    // 3. h[st.top()] < h[i] 弹出时处理 ans
    for (int i = 1; i < n; ++ i) {
        if (h[st.top()] >= h[i]) {	// 其实可以合并到下面
            st.push(i);
        } else {
            while (!st.empty() && h[st.top()] < h[i]) {
                int id = st.top();
                st.pop();
                ans[id] = h[i];	// h[id] 的 NGE 就是 h[i]
            }
            st.push(i);
        }
    }
    return ans;
}

// 另一种倒着遍历的写法
vector<int> NGE(vector<int>& nums) {
    int n = nums.size();
    vector<int> ans(n);
    stack<int> st;
    
    // 倒着往栈里面放
    for (int i = n - 1; i >= 0; -- i) {
        while (!st.empty() && s.top() <= nums[i]) {
            st.pop();	// 矮个起开,反正被挡住了,所以直接弹出
        }
        // 这个元素后面第一个高的个子
        ans[i] = st.empty() ? -1 : st.top();
        st.push(nums[i]);
    }
    return ans;
}

按照上面的数据,最终栈剩余的数为 [4, 2]

NGE变种1

给定一个数组 T = [73, 74, 75, 71, 69, 72, 76, 73],这个数组存放的是近几天的华氏温度,返回一个数组,对于每一天计算至少等多少天才能等到一个更加暖和的温度,如果等不到那一天就直接填 0

这个问题不是简单的求下一个更大的数,而是在找下一个更大的数距离此数的距离是多少,更直观的看下图。在上面只保存 Next Greater Element 的单调栈中需要另外保存 Next Greater Element Idx

NGE.png

所以代码如下:

//【推荐写法】从左往右看是非递增的
vector<int> NGE2(vector<int>& h) {
    int n = h.size();
    vector<int> ans(n, 0);	// 初始化为 0 表示未处理的就是 0
    stack<int> st;	// 保存下标
    st.push(0); // 初始下标入栈

    // 1. h[st.top()] > h[i] 直接入栈形成非递增
    // 2. h[st.top()] == h[i] 直接入栈
    // 3. h[st.top()] < h[i] 弹出时处理 ans
    for (int i = 1; i < n; ++ i) {
        if (h[st.top()] >= h[i]) {	// 其实可以合并到下面
            st.push(i);
        } else {
            while (!st.empty() && h[st.top()] < h[i]) {
                int id = st.top();
                st.pop();
                ans[id] = i - id;	// NGE 的相隔天数
            }
            st.push(i);
        }
    }
    return ans;
}

// 另一种倒着遍历的写法
vector<int> NGE2(vector<int>& nums) {
	int n = nums.size();
    vector<int> ans(n);
    stack<pair<int,int>> st;
    
    // 倒着往栈里面放
    for (int i = n - 1; i >= 0; -- i) {
        while (!st.empty() && st.top().first <= nums[i]) {
            st.pop();	// 矮个起开,反正被挡住了,所以直接弹出
        }
        // 这里计算下一个更大元素的距离
        ans[i] = st.empty() ? 0 : st.top().second - i;
        st.push(pair<int, int>(nums[i], i));
    }
    return ans;
}

NGE变种2

考虑求环形数组的下一个更大的元素,例如给定数组 [2,1,2,4,3],返回数组 [4,2,4,-1,4]。由于拥有了环形属性,最后一个元素 3 绕了一圈后找到了比自己大的元素 4

为了得到 Next 更大的数,我们需要环形查找整个数组,也就是 Next 的概念不仅仅是当前元素的右边,而有可能是当前元素的左边

为此我们将原始数组“翻倍”,就是在后面再接一个原始数组,这样的话,按照之前“比身高”的流程,每个元素不仅可以比较自己右边的元素,而且也可以和左边的元素比较了。

代码如下:使用 % 运算循环获取元素

//【推荐写法】从左往右看是非递增的
vector<int> NGE(vector<int>& h) {
    int n = h.size();
    vector<int> ans(n, -1);	// 初始化为 -1 表示未处理的就是 -1
    stack<int> st;	// 保存下标
    st.push(0); // 初始下标入栈

    // 1. h[st.top()] > h[i] 直接入栈形成非递增
    // 2. h[st.top()] == h[i] 直接入栈
    // 3. h[st.top()] < h[i] 弹出时处理 ans
    for (int i = 1; i < 2*n; ++ i) {
        if (h[st.top()] >= h[i%n]) {	// 其实可以合并到下面
            st.push(i%n);
        } else {
            while (!st.empty() && h[st.top()] < h[i%n]) {
                int id = st.top();
                st.pop();
                ans[id] = h[i%n];	// h[id] 的 NGE 就是 h[i]
            }
            st.push(i%n);
        }
    }
    return ans;
}

// 另一种倒着遍历的写法
vector<int> nextGreaterElements(vector<int>& nums) {
    int n = nums.size();
    vector<int> res(n); // 存放结果
    stack<int> s;
    // 假装这个数组长度翻倍了
    for (int i = 2 * n - 1; i >= 0; i--) {
        while (!s.empty() && s.top() <= nums[i % n])
            s.pop();
        res[i % n] = s.empty() ? -1 : s.top();
        s.push(nums[i % n]);
    }
    return res;
}

2. LeetCode 实例

基础

题目 解析 答案
496. 下一个更大元素 I 对nums2使用单调递减栈求NGE 通过
503. 下一个更大元素 II 原数组扩一倍,然后使用%运算 通过
739. 每日温度 保存 NGE 数和对应的下标 通过
1019. 链表中的下一个更大节点 和 496 一样,只是需要先遍历链表得到数组 通过
907. 子数组的最小值之和 求 a[i] 的左右边界,两次使用单调递增栈求边界 0x3F

进阶

题目 解析 答案
42. 接雨水 栈底到栈顶是递减的,注意凹槽的位置 🔥 通过
84. 柱状图中最大的矩形 栈底到栈顶是递增的,注意凸起的位置 🔥 通过
901. 股票价格跨度 单调递减栈,先记下入栈之前数的下标 通过
402. 移掉 K 位数字 单调递增栈,从头开始遍历,最终去掉前导0 通过
1673. 找出最具竞争力的子序列 和 402 类似,不需要去掉前导0 通过
1081. 不同字符的最小子序列 316. 去除重复字母 一样,402 升级版,统计每个字符数目, 通过
321. 拼接最大数 单调递减栈,数组字典序最大(cpp造轮子),归并 通过
1124. 表现良好的最长时间段 确切说不是单调栈,思想类似,前缀和+单调栈:找递减序列并且从后往前遍历,前缀和+哈希表:记录 s <= 0 的最小下标 0x3F

正向遍历,移除元素或者保留元素使得剩下的数字最小(最大)或者剩下的序列字典序最小(最大)

单调队列

题目 解析 答案
239. 滑动窗口最大值 双端队列,很严格递减的,存储元素比较好理解 流程
862. 和至少为 K 的最短子数组 前缀和,单调递增的队列 0x3F
2373. 矩阵中的局部最大值 3*3数据量比较小可以直接遍历,如果比较大可以每行执行滑动窗口求最大,并且每列比较 图解

总结

其实下面说的很繁琐?遇到题目就自己举几个例子模拟一下就知道了!下面不用记住

判断什么时候应该用单调递增的栈,什么时候应该用单调递减的栈

  1. 往前走找第一个比自己 「大」的元素,用单调递减的栈,也就是出栈的元素要求比自身要小,也就是 st.top() <= nums[i],所以最终栈里面剩余的元素是严格递减的
  2. 往前走找第一个比自己 「小」的元素,用单调递增的栈,也就是出栈的元素要求比自身要大,也就是 st.top() >= nums[i] ,所以最终栈里面剩余的元素是严格递增的

这里严格递增/减和非严格递增/减需要按照题目分析,一般而言都是 st.top() >= nums[i]st.top() <= nums[i],也就是严格,但是也有例外,如 LC.84 的出栈条件就是 st.top() > price,也就是栈剩余元素都是非严格递增的(按照题意,得到的非严格递增栈所有元素都要出栈操作,最终栈为空~)

3. 参考