数据结构——单调栈详解

单调栈定义:

从栈底元素到栈顶元素呈单调递增或单调递减,栈内序列满足单调性的栈。

一般用于求解,元素的左边或者右边第一个大于或者小于元素的值

这里通过几道单调栈的例题进行举例分析。

 

单调递减栈:

场景:

  • 在一个队列中针对每一个元素从它右边寻找第一个比它大的元素
  • 在一个队列中针对每一个元素从它左边寻找第一个比它大的元素(从后往前遍历)

这里采用大神labuladong的算法小抄的两个例题举例:

例1.第一个更大的元素

题目:

给你⼀个数组,返回⼀个等⻓的数组,对应索引存储着下⼀个更⼤元素,如果没有更⼤的元素,就存-1。

例: 数组 [2,1,2,4,3],返回数组 [4,2,4,-1,-1]。

解释:第⼀个 2 后⾯⽐ 2 ⼤的数是 4; 1 后⾯⽐ 1 ⼤的数是 2;第⼆个 2 后⾯⽐ 2 ⼤的数是 4; 4 后⾯没有⽐ 4 ⼤的数,填 -1;3 后⾯没有⽐ 3 ⼤的数,填 -1。

 

题目很简单,就是找右边第一个比自己大的数,找到返回找到的数字,没有就返回-1.

 

这就是一个思路,从后往前遍历,栈中存放最大元素在栈底,

 

 /*
    给你⼀个数组,返回⼀个等
    ⻓的数组,对应索引存储着下⼀个更⼤元素,如果没有更⼤的元素,就存
    -1。不好⽤语⾔解释清楚,直接上⼀个例⼦:
    给你⼀个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,-1]。
    解释:第⼀个 2 后⾯⽐ 2 ⼤的数是 4; 1 后⾯⽐ 1 ⼤的数是 2;第⼆个 2 后⾯
    ⽐ 2 ⼤的数是 4; 4 后⾯没有⽐ 4 ⼤的数,填 -1;3 后⾯没有⽐ 3 ⼤的数,填
    -1。
     */

    //单调递减栈
    public static int[] convert(int[] param){
        int[] result=new int[param.length];
        Stack<Integer> stack=new Stack<>();

        //倒着遍历
        for (int i = param.length - 1; i >= 0; i--) {
            //将最大的元素放入栈底
            while(!stack.isEmpty()&&stack.peek()<param[i]){
                stack.pop();
            }
            //while中pop出所有比param[i]小的元素,栈顶元素即是右边第一个可以满足的元素
            result[i]=stack.isEmpty()?-1:stack.peek();
            //放入,方便下一次比较
            stack.push(param[i]);
        }
        return result;
    }

 

例2.更温暖的气温

给你⼀个数组 T = [73, 74, 75, 71, 69, 72, 76, 73],这个数组存放的是近⼏天的天⽓⽓温(这⽓温是铁板烧?不是的,这⾥⽤的华⽒度)。你返回⼀个数组,计算:对于每⼀天,你还要⾄少等多少天才能等到⼀个更暖和的⽓温;如果等不到那⼀天,填 0 。
举例:给你 T = [73, 74, 75, 71, 69, 72, 76, 73],你返回 [1, 1, 4, 2, 1, 1, 0, 0]

和第一题类似,这里不做过多的解释,直接贴答案了

public static int[] convertT(int[] param){
        int[] result=new int[param.length];
        Stack<Integer> stack=new Stack<>();
        for (int i = param.length - 1; i >= 0; i--) {
            while(!stack.isEmpty()&&param[stack.peek()]<param[i]){
                stack.pop();
            }
            result[i]=stack.isEmpty()?0:stack.peek()-i;
            stack.push(i);
        }
        return result;
    }

 

例3.滑动窗口

题目

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

分析

该题也是一个递减栈的变形,我们只要同过stack存放一个递减栈,让每次取栈底元素就行了,这里使用双端队列,直接取就可以。

 

public int[] maxSlidingWindow(int[] nums, int k) {
        int[] result = new int[nums.length - k + 1];
        Deque<Integer> stack = new LinkedList<>();
        for (int i = 0; i < nums.length; i++) {
            //单调递减栈组装数据
            while(!stack.isEmpty()&&nums[stack.peekLast()]<nums[i]){
                stack.pollLast();
            }
            stack.addLast(i);

            //如果当前-栈顶元素大于个数限制,将栈顶元素出栈
            if(i-stack.peekFirst()>=k){
                stack.pollFirst();
            }
            //避免越界,当i-k+1>=0才有意义
            if (i-k+1>=0){
                result[i-k+1]=nums[stack.peekFirst()];
            }


        }
        return result;
    }

 

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sliding-window-maximum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

 

 

 

 

 

单调递增栈:

  • 在一个队列中针对每一个元素从它右边寻找第一个比它小的元素
  • 在一个队列中针对每一个元素从它左边寻找第一个比它小的元素(从后往前遍历)

 

例4.力扣真题

https://leetcode-cn.coml/problems/largest-rectangle-in-histogram/description/

穷举

其实可以想到的思路是穷举

以2作为高,计算最大面积;

以1作为高,计算最大面积;

以5作为高,计算最大面积;

以6作为高,计算最大面积;

以2作为高,计算最大面积;

以3作为高,计算最大面积;

每个的结果取大,即最终结果

于是有了如下代码:

public class Leetcode84 {
    public static void main(String[] args) {
        Leetcode84 leetcode84 = new Leetcode84();
        int[] param = {2,1,5,6,2,3};
        System.out.println(leetcode84.largestRectangleArea(param));

    }

    public int largestRectangleArea(int[] heights) {
        //遍历heights,当前元素作为构建形状的最低形状高度,由左和右分别判定可组成的长度。
        int areaMax = 0;
        for (int i = 0; i < heights.length; i++) {
            int length = 1;
            int leftIndex = i - 1;
            int rightIndex = i + 1;
            while (leftIndex >= 0 && heights[leftIndex] >= heights[i]) {
                length++;
                leftIndex--;
            }
            while (rightIndex < heights.length && heights[rightIndex] >= heights[i]) {
                length++;
                rightIndex++;
            }
            if (length * heights[i] > areaMax) {
                areaMax = length * heights[i];
            }
        }
        return areaMax;
    }

}

力扣提交,结果:

 

....

单调栈

单调栈的处理逻辑

还是以题中的数据举例,原来数据如下。

 

为了更形象的使用这个逻辑,在头和尾补上元素0,

这里补上0主要是为了遍历避免判空,方便第一位和最后一位元素的判定。

转换后如下:

比如要找寻2组成的面积,那么就需要找寻左右两边第一个小于2的元素,通过栈,将0入栈,2入栈,当遇到1时,即可以求出左遍大于1的面积,求出后将其出栈。详细分析过程如下

详细过程分析

注意,我们这里举例的stack中存储的元素的value值一直是一个递增栈

1. 源数据结构如下

 

2. 数组头和尾补0,注意这里的stack需要存储的是index值,而不是value值,因为stack用于帮我们找到宽度

3. 开始遍历数组,第一个元素为0

4. 继续遍历,处理index=1的元素

5. 遍历第三个元素,因为现在找到了元素的右边界,所以可以求出大于高度为1的元素的面积

6. 遍历第四个元素index=3

7. 遍历第五个元素index=4

8. 遍历第六个元素index=5,处理第五个元素,pop出index=4,计算高度为6的面积

9. 处理第四个元素,pop出index=3,处理高度为5的面积

10. 此时,由于第五个元素的值大于栈中arr[2]=1的值,此时index=5入栈

 

11.第七个元素入栈

12. 继续处理到第八个元素,发现不满足递增栈,即到了右边界,pop栈,计算高度为3的元素的值

 

13. 出栈后,由于0<arr[5],继续出栈,计算index为5的元素的面积

14. 由于0<arr[2],此时计算index=2的元素的面积

这样我们的所有元素的面积,就已经全部计算完毕。每个取大就可以了。

 

代码实现

package com.learn.mk.leetcode;


import java.util.Stack;

/**
 * 84. 柱状图中最大的矩形
 * 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
 * <p>
 * 求在该柱状图中,能够勾勒出来的矩形的最大面积。
 * url:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/description/
 */
public class Leetcode84 {

    
    public int largestRectangleArea(int[] heights) {
        int tempMax = 0;
        int[] heightsConvert = new int[heights.length + 2];
        System.arraycopy(heights, 0, heightsConvert, 1, heightsConvert.length - 2);
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < heightsConvert.length; i++) {
            while (!stack.isEmpty() && heightsConvert[stack.peek()] > heightsConvert[i]) {
                Integer pop = stack.pop();
                tempMax = Math.max(tempMax, heightsConvert[pop] * (i - stack.peek()-1));
            }
            stack.push(i);
        }
        return tempMax;

    }


    public static void main(String[] args) {
        Leetcode84 leetcode84 = new Leetcode84();
        int[] param = {2,1,5,6,2,3};
        System.out.println(leetcode84.largestRectangleArea(param));
    }

}

 

可以进一步优化代码,减少判定逻辑,如下:

package com.learn.mk.leetcode;


import java.util.Stack;

/**
 * 84. 柱状图中最大的矩形
 * 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
 * <p>
 * 求在该柱状图中,能够勾勒出来的矩形的最大面积。
 * url:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/description/
 */
public class Leetcode84 {


    public int largestRectangleArea(int[] heights) {
        int tempMax = 0;
        int[] heightsConvert = new int[heights.length + 2];
        System.arraycopy(heights, 0, heightsConvert, 1, heightsConvert.length - 2);
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        for (int i = 0; i < heightsConvert.length; i++) {
            while (heightsConvert[stack.peek()] > heightsConvert[i]) {
                Integer pop = stack.pop();
                tempMax = Math.max(tempMax, heightsConvert[pop] * (i - stack.peek()-1));
            }
            stack.push(i);
        }
        return tempMax;

    }


    public static void main(String[] args) {
        Leetcode84 leetcode84 = new Leetcode84();
        int[] param = {2,1,5,6,2,3};
        System.out.println(leetcode84.largestRectangleArea(param));
    }

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值