算法之单调栈常见题目

什么时候需要使用单调栈?
通常是一维数组,要寻找任意一个右边或者左边第一个比自己大或小的元素的位置,此时我们就想到可以使用单调栈了
单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。所以时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢?
更直白来说,就是用一个栈来记录我们遍历过的元素,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。
leedcode739. 每日温度
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:

输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]
示例2:
输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]
示例 3:

输入: temperatures = [30,60,90]
输出: [1,1,0]

在这里插入图片描述

class Solution {
  //单调栈就是解决找到第一个比原来的数大或小的数,然后进行出栈并通过相减求出之前的坐标
    public int[] dailyTemperatures(int[] temperatures) {

        //创建一个栈,存储单调递增,存的是下标
        Stack<Integer> stack=new Stack<>();
        //第一个存储的元素默认是第一个数组元素下标
        stack.push(0);
        //存放结果的result数组
        int[] result=new int[temperatures.length];
        //默认都是0,因为后面可以没有再比他大的数那么温度不会再升高,就是0
        Arrays.fill(result,0);
        for(int i=1;i<temperatures.length;i++){

            if(temperatures[i]<=temperatures[stack.peek()]){
                stack.push(i);
            }
            while(!stack.isEmpty()&&temperatures[i]>temperatures[stack.peek()]){//比较到最后可能栈已经弹完,需要进行判空
                result[stack.peek()]=i-stack.pop();
            }
            //比较完才加入当前下标
            stack.push(i);

        }
        return result;

    }
}

leedcode496. 下一个更大元素 I
nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。

给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。

对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。

返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。

示例 1:

输入:nums1 = [4,1,2], nums2 = [1,3,4,2].
输出:[-1,3,-1]
解释:nums1中每个值的下一个更大元素如下所述:

  • 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
  • 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
  • 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。

示例 2:
输入:nums1 = [2,4], nums2 = [1,2,3,4].
输出:[3,-1]
解释:nums1 中每个值的下一个更大元素如下所述:

  • 2 ,用加粗斜体标识,nums2 = [1,2,3,4]。下一个更大元素是 3 。
  • 4 ,用加粗斜体标识,nums2 = [1,2,3,4]。不存在下一个更大元素,所以答案是 -1 。

注意题目中说是两个没有重复元素 的数组 nums1 和 nums2。
没有重复元素,我们就可以用map来做映射了。根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。
从题目示例中我们可以看出最后是要求nums1的每个元素在nums2中下一个比当前元素大的元素,那么就要定义一个和nums1一样大小的数组result来存放结果。


    public int[] nextGreaterElement(int[] nums1, int[] nums2) {

        //建立一个单调栈
        Deque<Integer> stack = new LinkedList<>();
        Map<Integer, Integer> map = new HashMap();
        for (int i = 0; i < nums1.length; i++) {
            map.put(nums1[i], i);
        }
        //存放数组
        int[] result = new int[nums1.length];
        Arrays.fill(result, -1);
        //默认栈中第一个元素的下标是0
        stack.push(0);
        for (int i = 1; i < nums2.length; i++) {
            //情况一,栈顶元素和当前遍历元素相等
            if (nums2[i] == nums2[stack.peek()]) {
                stack.push(i);
            }
            //情况二,栈顶元素大于当前遍历元素
            if (nums2[i] < nums2[stack.peek()]) {
                stack.push(i);
            }
            //情况三,栈顶元素小于当前遍历元素
            //对栈中元素进行判空,并比较结果
            while (!stack.isEmpty() && nums2[i] > nums2[stack.peek()]) {
                //因为主要求得是num1每个值对应的下一个元素,所以判断当前map是否存在nums1中的元素
                if (map.containsKey(nums2[stack.peek()])) {
                    Integer index = map.get(nums2[stack.peek()]);
                    result[index] = nums2[i];//存放比当前nums1大的下一个元素
                }
                stack.pop();
            }
            stack.push(i);
        }
        return result;
    }

503. 下一个更大元素 II
给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。

数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。

示例 1:

输入: nums = [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2; 数字 2找不到下一个更大的数; 第二个 1 的下一个最大的数需要循环搜索,结果也是 2。

示例 2:

输入: nums = [1,2,3,4,3]
输出: [2,3,4,-1,4]

对于成环的题目可以考虑使用取模

  //单调栈成环,一般用模%
    public int[] nextGreaterElements(int[] nums) {

        //栈
        Deque<Integer> stack=new LinkedList<>();
        int[] result=new int[nums.length];
        if(nums.length==0){
            return result;
        }
        Arrays.fill(result,-1);
        stack.push(0);
        //遍历长度为原来的两倍
        for (int i=1;i<(nums.length*2);i++){
            if(nums[i % nums.length]<nums[stack.peek()]){
                stack.push(i%nums.length);
            }
            if(nums[i%nums.length]==nums[stack.peek()]){
                stack.push(i%nums.length);//放栈下标的时候也要记得取模,不然nums[stack.peek()]可能会越界
            }
            while(!stack.isEmpty()&&nums[i%nums.length]>nums[stack.peek()]){
                result[stack.peek()]=nums[i%nums.length];
                stack.pop();
            }
            stack.push(i);
        }
        return result;
    }

leedcode42. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
在这里插入图片描述

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。

因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
在这里插入图片描述
在这里插入图片描述
从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。

因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
在这里插入图片描述

   public int trap(int[] height) {

        //创建单调栈,找到左右两边第一个比它大的数,单调递增栈
        Deque<Integer> stack=new LinkedList<>();
        int result=0;
        //刚开始放首个元素
        stack.push(0);
        for(int i=1;i<height.length;i++) {
        //如果当前遍历的元素(柱子)高度小于栈顶元素的高度,就把这个元素加入栈中,因为栈里本来就要保持从小到大的顺序(从栈头到栈底)。
            if (height[i] < height[stack.peek()]) {
                stack.push(i);
            }
            //如果当前遍历的元素(柱子)高度等于栈顶元素的高度,要跟更新栈顶元素,因为遇到相相同高度的柱子,需要使用最右边的柱子来计算宽度。
           if(height[i]==height[stack.peek()]){
                stack.pop();
                stack.push(i);
            }
            //如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了
            while (!stack.isEmpty()&&height[i] > height[stack.peek()]) {
                int mid = stack.pop();
                if (!stack.isEmpty()) {
                    int left = stack.peek();
                    int right = i;
                    //计算宽
                    int w = right - left - 1;
                    //计算高
                    int h = Math.min(height[left], height[right]) - height[mid];
                    result += w * h;
                }

            }
            stack.push(i);
        }
        return result;
    }

leedcode84. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。
在这里插入图片描述
本题和接雨水拿到题很像,只不过是找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!
在这里插入图片描述
只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。
主要有三种情况:
情况一:当前遍历的元素heights[i]大于栈顶元素heights[stack.peek()]的情况
情况二:当前遍历的元素heights[i]等于栈顶元素heights[stack.peek()]的情况
情况三:当前遍历的元素heights[i]小于栈顶元素heights[stack.peek()]的情况
需要注意的是要在原来的数组里面首部和尾部加0
首先来说末尾为什么要加元素0?
如果数组本身就是升序的,例如[2,4,6,8],那么入栈之后 都是单调递减,一直都没有走情况三 计算结果的哪一步,所以最后输出的就是0了。 如图:
在这里插入图片描述
那么结尾加一个0,就会让栈里的所有元素,走到情况三的逻辑。

开头为什么要加元素0?
如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),rigt(6),但是得不到 left。
在这里插入图片描述
计算过程
在这里插入图片描述

class Solution {
 public int largestRectangleArea(int[] heights) {

           // 数组扩容,在头和尾各加入一个元素
        int [] newHeights = new int[heights.length + 2];
        newHeights[0] = 0;
        newHeights[newHeights.length - 1] = 0;
        for (int index = 0; index < heights.length; index++){
            newHeights[index + 1] = heights[index];
        }

        heights = newHeights;
         int result=0;
        Deque<Integer> stack=new LinkedList<>();
        stack.push(0);
        //单调递减栈
        for(int i=1;i<heights.length;i++){
            if(heights[i]>heights[stack.peek()]){
                stack.push(i);
            }
            if(heights[i]==heights[stack.peek()]){
                stack.pop();
                stack.push(i);
            }
            while(!stack.isEmpty()&&heights[i]<heights[stack.peek()]){
                //中间元素
                int mid=stack.pop();
                if(!stack.isEmpty()){
                    //左边,右边找最大
                    int height=heights[mid];
                    int width=i-stack.peek()-1;
                    result=Math.max(result,height*width);
                }
             
            }
            stack.push(i);
        }
        return result;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值