单调栈思想及解题分析

单调栈思想及解题分析:

概念:

单调栈(Monotone Stack):一种特殊的栈。在栈的「先进后出」规则基础上,要求「从 栈顶栈底 的元素是单调递增(或者单调递减)」。其中满足从栈顶到栈底的元素是单调递增的栈,叫做「单调递增栈」。满足从栈顶到栈底的元素是单调递减的栈,叫做「单调递减栈」。

单调递增栈:只有比栈顶元素小的元素才能直接进栈,否则需要先将栈中比当前元素小的元素出栈,再将当前元素入栈。

这样就保证了:栈中保留的都是比当前入栈元素大的值,并且从栈顶到栈底的元素值是单调递增的。

单调递减栈:只有比栈顶元素大的元素才能直接进栈,否则需要先将栈中比当前元素大的元素出栈,再将当前元素入栈。

这样就保证了:栈中保留的都是比当前入栈元素小的值,并且从栈顶到栈底的元素值是单调递减的。

单调栈适用场景

单调栈可以在时间复杂度为 O(n) 的情况下,求解出某个元素左边或者右边第一个比它大或者小的元素。

所以单调栈一般用于解决一下几种问题:

寻找左侧第一个比当前元素大的元素。寻找左侧第一个比当前元素小的元素。

寻找右侧第一个比当前元素大的元素。寻找右侧第一个比当前元素小的元素。


具体求解方法:

1 寻找左侧第一个比当前元素大的元素
从左到右遍历元素,构造单调递增栈(从栈顶到栈底递增)

​ 一个元素左侧第一个比它大的元素就是将其「插入单调递增栈」时的栈顶元素。

​ 如果插入时的栈为空,则说明左侧不存在比当前元素大的元素。

2 寻找左侧第一个比当前元素小的元素
从左到右遍历元素,构造单调递减栈(从栈顶到栈底递减)

​ 一个元素左侧第一个比它小的元素就是将其「插入单调递减栈」时的栈顶元素。

​ 如果插入时的栈为空,则说明左侧不存在比当前元素小的元素。

3 寻找右侧第一个比当前元素大的元素
从左到右遍历元素,构造单调递增栈(从栈顶到栈底递增)

​ 一个元素右侧第一个比它大的元素就是将其「弹出单调递增栈」时即将插入的元素。

​ 如果该元素没有被弹出栈,则说明右侧不存在比当前元素大的元素。

从右到左遍历元素,构造单调递增栈(从栈顶到栈底递增)

​ 一个元素右侧第一个比它大的元素就是将其「插入单调递增栈」时的栈顶元素。

​ 如果插入时的栈为空,则说明右侧不存在比当前元素大的元素。

4 寻找右侧第一个比当前元素小的元素
从左到右遍历元素,构造单调递减栈(从栈顶到栈底递减):

​ 一个元素右侧第一个比它小的元素就是将其「弹出单调递减栈」时即将插入的元素。

​ 如果该元素没有被弹出栈,则说明右侧不存在比当前元素小的元素。

从右到左遍历元素,构造单调递减栈(从栈顶到栈底递减):

​ 一个元素右侧第一个比它小的元素就是将其「插入单调递减栈」时的栈顶元素。

​ 如果插入时的栈为空,则说明右侧不存在比当前元素小的元素。
————————————————

以上内容参考原文链接:https://blog.csdn.net/zy_dreamer/article/details/131036101


经典例题:

1475. 商品折扣后的最终价格 (简单)
496. 下一个更大元素 I (简单)
739. 每日温度 (中等)
503. 下一个更大元素 II (中等)
239. 滑动窗口最大值 (困难)
42. 接雨水 (困难)
84. 柱状图中最大的矩形(困难)

解答:(个人感觉倒序遍历方式简单一点)

简单:

1475. 商品折扣后的最终价格

倒序遍历:stack中储存右边第一个最大值

class Solution {//java
    public int[] finalPrices(int[] prices) {
        Stack<Integer> st = new Stack();
        int[] res = new int[prices.length];
        for (int i = prices.length - 1; i >= 0; i--) {
            while (!st.isEmpty() && st.peek() > prices[i]) {
                st.pop();
            }
            res[i] = st.isEmpty() ? prices[i] : prices[i] - st.peek();
            st.push(prices[i]);
        }
        return res;
    }
}

正序遍历:栈中储存下标

class Solution {//java
    public int[] finalPrices(int[] prices) {
        Stack<Integer> st = new Stack();
        int[] res = new int[prices.length];
        for (int i = 0; i < prices.length; i++) {
            while (!st.isEmpty() && prices[i] <= prices[st.peek()]) {
                res[st.peek()] = prices[i];//先记录每件商品折扣
                st.pop();
            }
            st.push(i);
        }
        //计算折扣之后的价格
        for (int i = 0; i < prices.length; i++) {
            res[i] = prices[i] - res[i];
        }
        return res;
    }
}
496. 下一个更大元素 I

思想:单调栈+哈希

倒序遍历:stack中储存右边第一个最大值

class Solution {//java
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();//hash储存对应元素及其右边第一个更大的值
        Stack<Integer> st = new Stack();
        for (int i = nums2.length - 1; i >= 0; i--) {
            int num = nums2[i];
            while (!st.isEmpty() && num >= st.peek()) {
                st.pop();
            }
            map.put(num, st.isEmpty() ? -1 : st.peek());
            st.push(num);
        }
        int[] res = new int[nums1.length];
        for (int i = 0; i < nums1.length; i++) {
            res[i] = map.get(nums1[i]);
        }
        return res;

    }
}

正序遍历:栈中储存下标

class Solution {//c++
public:
    vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
        stack<int> st;
        vector<int> res(nums1.size(), -1);
        unordered_map<int, int> umap;//key:下标元素,value:下标
        for (int i = 0; i < nums1.size(); i++) {
            umap[nums1[i]] = i;
        }
        for (int i = 0; i < nums2.size(); i++) {
            while (!st.empty() && nums2[i] > nums2[st.top()]) {//找到右侧第一个最大值下标
                if (umap.find(nums2[st.top()]) != umap.end()) {//找到栈口在Nums1中
                    int index = umap.find(nums2[st.top()])->second;
                    res[index] = nums2[i];
                }
                st.pop();
            }
            st.push(i);//栈里存放的是下标
        }
        return res;
    }
};

中等:

739. 每日温度

倒序遍历:栈中存放右边第一个最大值下标方便计算天数

class Solution {//java
    public int[] dailyTemperatures(int[] temperatures) {
        Stack<Integer> st = new Stack();
        int[] res = new int[temperatures.length];
        for (int i = temperatures.length - 1; i >= 0; i--) {
            while(!st.isEmpty() && temperatures[st.peek()] <= temperatures[i]) {
                st.pop();
            }
            if (!st.isEmpty()) {
                res[i] = st.peek() - i;
            } else {
                res[i] = 0;
            }
            st.push(i);
        }
        return res;
    }
}

正序遍历:栈中储存下标

class Solution {//java
    public int[] dailyTemperatures(int[] temperatures) {
        Stack<Integer> st = new Stack();
        int[] res = new int[temperatures.length];//栈初始即为0
        for (int i = 0; i < temperatures.length; i++) {
            //栈里存放遍历过的元素下标
            while (!st.isEmpty() && temperatures[i] > temperatures[st.peek()]) {
                res[st.peek()] = i - st.peek();
                st.pop();
            }

            st.push(i);
        }
        return res;
    }
}
503. 下一个更大元素 II

本题给定循环数组判断下一个最大元素,只需将循环数组拉直再扩充一倍即可判断,操作时只需在处理时对下标取模即可。

倒序遍历:栈中存放右边第一个最大值

class Solution {//java
    public int[] nextGreaterElements(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        Stack<Integer> st = new Stack();
        for (int i = 2 * n - 1; i >= 0; i--) {
            while (!st.isEmpty() && nums[i % n] >= st.peek()) {
                st.pop();
            }
            res[i % n] = st.isEmpty() ? -1 : st.peek();
            st.push(nums[i % n]);
        }
        return res;
    }
}

正序遍历:栈中储存下标

class Solution {//c++
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        vector<int> res(nums.size(), -1);
        stack<int> st;
        for (int i = 1; i < nums.size() * 2; i++) {
            //模拟遍历两遍nums,注意一下都是用i % nums.size()来操作
            while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) {
                res[st.top()] = nums[i % nums.size()];
                st.pop();
            }
            st.push(i % nums.size());
        }
        return res;
    }
};

class Solution {//java
    public int[] nextGreaterElements(int[] nums) {
        int n = nums.length;
        int[] ret = new int[n];
        Arrays.fill(ret, -1);//此方法给数组复制操作需记住
        Deque<Integer> stack = new LinkedList<Integer>();
        for (int i = 0; i < n * 2 - 1; i++) {
            while (!stack.isEmpty() && nums[stack.peek()] < nums[i % n]) {
                ret[stack.pop()] = nums[i % n];
            }
            stack.push(i % n);
        }
        return ret;
    }
}


困难:

239. 滑动窗口最大值

思想:单调队列,双端队列(队列头部储存当前最大值下标,且头部下标最小)

class Solution {//java
    public int[] maxSlidingWindow(int[] nums, int k) {
        Deque<Integer> dq = new LinkedList<>();//双端队列
        int[] res = new int[nums.length -  k + 1];
        int j = 0;
        for (int i = 0; i < nums.length; i++) {
            while (!dq.isEmpty() && nums[i] >= dq.peekLast()) {
                dq.pollLast();
            }
            dq.offer(i);//储存最大值下标,在队列头部,其下标也最小
            if (i - dq.peek() >= k) {
                //队列中元素多了说明队头元素不在滑动窗口范围内了
                dq.pollLast();
            }
            if (i + 1 >= k) {
                res[j++] = nums[dq.peek()];
            }
        }
        return res;
    }
}
42. 接雨水

单调栈:维护一个单调栈,单调栈存储的是下标,满足从栈底到栈顶的下标对应的数组中的元素递减。

class Solution {
    public int trap(int[] height) {
        int res = 0;
        Stack<Integer> st = new Stack();
        int n = height.length;
        for (int i = 0; i < n; i++) {
            while (!st.isEmpty() && height[i] > height[st.peek()]) {
                int top = st.pop();
                if (st.isEmpty()) {
                    break;
                }
                int left = st.peek();
                int curW = i - left - 1;
                int curH = Math.min(height[left], height[i]) - height[top];
                res += curW * curH;
            }
            st.push(i);
        }
        return res;
    }
}
84. 柱状图中最大的矩形

思路:同接雨水,但实际处理有些区别,这里单调栈中元素存放下标,栈顶到栈底递减。

class Solution {
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        //数组扩容,在头和尾各加入一个元素
        int [] newHeights = new int[n + 2];
        newHeights[0] = 0;
        newHeights[newHeights.length - 1] = 0;
        for (int i = 0; i < n; i++) {
            newHeights[i + 1] = heights[i];
        }
        heights = newHeights;
        n = heights.length;
        Stack<Integer> st = new Stack();
        int res = 0;
        st.push(0);
        for (int i = 0; i < n; i++) {
            while (!st.isEmpty() && heights[st.peek()] > heights[i]) {
                int mid = st.pop();
                if (!st.isEmpty()) {
                    int w = i - st.peek() - 1;
                    int h = heights[mid];
                    res = Math.max(res, h * w);
                }
            }

            st.push(i);
        }
        return res;
    }
}
  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值