算法笔记——单调栈

借鉴——单调栈总结/牛客网左神算法进阶班

基本问题

对于一个数组arr, 针对每个数,寻找它和它左 / 右边第一个比它大 / 小的数的值,以及相距多少个数。

基本思路:

  • 创建一个栈,将数组中的元素下标传进去。

  • 遍历arr数组,如果栈空,则入栈

  • 如果栈不为空,且当前栈顶元素值大于当前arr遍历到的位置的值,则直接入栈。

  • 如果栈不为空,且当前栈顶元素值小于当前arr遍历到的位置的值,则栈顶元素出栈,栈顶元素的左边第一个比它大以及右边第一个比他大的元素就可以确定了

  • 如果栈不为空,且当前栈定元素值等于当前arr遍历到的位置的值,将具有相同值得数组索引下标压入一个链表或者数组。

  • 可以确定得是,只有当元素出栈得时候,我们才可以获取到当前元素左边以及右边得信息。

  • 这里分别提供找出某个位置的右边第一个大的数和左边第一个大的数的方法。

代码:

    // 问题一:给定一个数组,请你返回每个元素右边第一个大于它的元素,如果不存在则返回-1
    public static int[] getFirstRightMax(int[] arr) {
        if(arr == null || arr.length == 0) return null;
        // 创建一个栈结构
        Stack stack = new Stack();
        int[] res = new int[arr.length];
        int i = 0;
        while(i < arr.length) {
            if(stack.isEmpty() || arr[stack.peek()] >= arr[i]) {
                stack.push(i);
                i++;
            }else{
                res[stack.pop()] = arr[i];
            }
        }
        // 遍历走完之后,栈中可能还剩余数组
        while(!stack.isEmpty()){
            res[stack.pop()] = -1;
        }
        return res;
    }

    // 问题二:给定一个数组,请你返回每个元素左边第一个大于它的元素,如果不存在则返回-1
    public static int[] getFirstLeftMax(int[] arr) {
        if(arr == null || arr.length == 0) return null;
        Stack stack = new Stack();
        int[] res = new int[arr.length];
        int i = 0;
        while(i < arr.length) {
            if(stack.isEmpty() || arr[stack.peek()] >= arr[i]) {
                stack.push(i);
                i++;
            }else{
                res[stack.pop()] = stack.isEmpty() ? -1 : arr[stack.peek()];
            }
        }
        while(!stack.isEmpty()){
            res[stack.pop()] = stack.isEmpty() ? -1 : arr[stack.peek()];
        }
        return res;
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1,5,3,6,4,8,9,10};
        int[] res1 = getFirstLeftMax(arr);
        int[] res2 = getFirstRightMax(arr);
        System.out.println(Arrays.toString(res1));
        System.out.println(Arrays.toString(res2));
    }

衍生问题一

LeeCode | 84. 柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。


柱状图中最大的矩形

基本思路:

  • 使用一个单调栈存储一个单调递增的序列,如果遇到大于等于栈顶元素的数组,则入栈,否则出栈,那么这么做的原因是什么尼?
  • 首先考虑出栈的过程,只有当前元素小于栈顶元素或者当前高度数组已经遍历完了才会出栈,那么出栈也就意味着最大面积maxArea的更新,这也满足单调栈出栈更新的思想。具体更新思想可能看图更容易明白。


    1
2
3

代码如下:

    //LeeCode84 借用单调栈的思想求解柱状图中的最大矩形面积
    public static int largestRectangleArea(int[] heights) {
        // 首先进行边界条件判端
        if(heights == null || heights.length == 0) return 0;
        if(heights.length == 1) return heights[0];
        // 创建一个单调栈
        Stack stack = new Stack();
        int index = 0;
        int maxArea = 0;
        while(index < heights.length){
            if(stack.isEmpty()){
                stack.push(index++);
            }else{
                if(heights[stack.peek()] <= heights[index]){
                    stack.push(index++);
                }else{
                    // 取出当前栈顶元素对应的高度
                    int topHeight = heights[stack.pop()];
                    // 获取左边一个数据的索引,这个数据和当前的index之间肯定可以构成构成高度为height的矩阵
                    int leftIndex = stack.isEmpty() ? -1 : stack.peek();
                    // 更新maxArea
                    maxArea = Math.max(maxArea,(index - leftIndex - 1) * topHeight);
                }
            }
        }
        // 循环遍历结束时,index == arr.length; 还需要将栈中剩余的数据全部考虑进去
        while (!stack.isEmpty()) {
            int topHeight = heights[stack.pop()];
            int leftIndex = stack.isEmpty() ? -1 : stack.peek();
            maxArea = Math.max(maxArea,(index - leftIndex - 1) * topHeight);
        }
        return maxArea;
    }

衍生问题二——求二维数组中的最大矩形面积

LeetCode | 85. 最大矩形

题目描述:

最大矩形面积

基本思路:

  • 此题的基本思路就是就是建立在前一题的基础上求解的。将二维数组中每一行看作直方图的底,不断更新直方图中每个位置矩形的高度,按照前一题的求解思路即可。

代码如下:

// 给定二维数组,求二维数组中最大的矩形面积
// LeeCode85题
public class Code_04_MaxArea {

    public int maximalRectangle(char[][] matrix) {
        if(matrix == null || matrix.length == 0) return 0;
        int row = matrix.length;
        int col = matrix[0].length;
        int[] heights = new int[col];
        int maxArea = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                heights[j] = matrix[i][j] == '0' ? 0 : heights[j] + 1;
            }
            maxArea = Math.max(maxArea,getMaxArea(heights));
        }
        return maxArea;
    }

    /**
     * 给定一个直方图的数组,求该直方图中最大的矩形面积
     * @param heights
     * @return
     */
    private int getMaxArea(int[] heights) {
        // 这里不进行边界判断了,交给主函数
        // 创建单调栈
        Stack stack = new Stack();
        int index = 0;
        int maxArea = 0;
        while(index < heights.length){
            while(!stack.isEmpty() && heights[stack.peek()] >= heights[index]){
                int h = stack.pop();
                int l = stack.isEmpty() ? -1 : stack.peek();
                maxArea = Math.max(maxArea,(index - l - 1) * heights[h]);
            }
            stack.push(index++);
        }
        while(!stack.isEmpty()){
            int h =stack.pop();
            int l = stack.isEmpty() ? -1 : stack.peek();
            maxArea = Math.max(maxArea,(index - l - 1) * heights[h]);
        }
        return maxArea;
    }
}


衍生问题三 —— 构造数组的MaxTree

定义二叉树的节点如下:

public class Node{
    public int value;
    public Node left;
    public Node right;

    public Node(int value){
        this.value = value;
    }
}

一个数组的maxTree定义如下:

  1. 数组中必须没有重复元素;
  2. maxTree是一颗二叉树,数组中每一个值对应一个二叉树中的节点。
  3. 包括maxTree在内且在其中的每一颗子树上,值最大的节点都是当前树的头节点。
  4. 指定没有重复元素的数组arr,写出这个数组的maxTree生成函数,要求如果数组长度为N,则时间复杂度为O(N), 空间复杂度为O(N).

基本思路:

  • 方法一: 将数组arr生成一个大顶堆,然后构造出一个完全二叉树。

  • 方法二: 单调栈的思想,这里着重描述单调栈在这种问题中的求解思路。

  • 首先通过单调栈求出每个位置上左边第一个比它大的元素,右边第一个比他大的元素。

  • 如果某个位置上左右比他大的元素都没有,则此位置的元素的值作为maxTree的根节点。

  • 如果某个位置上左右两边有一个为null, 那么当前的元素作为此处参数信息非null的那个节点的子节点。

  • 如果某个位置上左右两边的参数都存在,那么当前的元素作为左右两边较大的那个值的子节点。


衍生题四——最大可见山峰对

问题描述:

指定一个数组arr;
数组的中元素的含义为一个环形山脉每座山的高度;
规定,相邻的两座山脉之间可以彼此看见;
如果两座山脉不相邻,两座山脉之间的所有山脉不大于这两座山脉的最小值时,这两座山脉可以互相看见。
现在希望你可以求出可以彼此看见的山脉总对数。

思路分析:

  • 首先找到数组中的最大元素压入单调栈中。
  • 创建一个单调递减的栈结构
  • 如果待压入元素不满足单调性栈,则从栈中弹出元素,更新结果集
  • 直到单调栈中元素更新结束,返回最终的结果集res。
1

2

3

代码如下:

/**
 *
 * 左神算法进阶班第三课————单调栈
 * 难度——顶尖
 * 问题描述:
 *      指定一个数组arr;
 *      数组的中元素的含义为一个环形山脉每座山的高度;
 *      规定,相邻的两座山脉之间可以彼此看见;
 *      如果两座山脉不相邻,两座山脉之间的所有山脉不大于这两座山脉的最小值时,这两座山脉可以互相看见。
 *      现在希望你可以求出可以彼此看见的山脉总对数。
 *
 */
public class Code_03_CircleMountain {

    static class Pair{
        int value;    // 存放当前山峰的高度,就是数组中某个位置的值
        int times = 1;    // 当前山峰已经出现的次数,也就是数组中某个位置元素出现的次数,初始化为1

        Pair(int value) {
            this.value = value;
        }
    }

    /**
     * 主函数入口,获取彼此可以相互看见的最大山峰对
     * @param arr
     * @return
     */
    public static long communications(int[] arr) {
        if(arr == null || arr.length == 0) {
            return 0;
        }
        int size = arr.length;
        //-----更新出数组中最大元素的索引-----
        int maxIndex = 0;
        for (int i = 1; i < arr.length; i++) {
            maxIndex = arr[maxIndex] < arr[i] ? i : maxIndex;
        }
        int value = arr[maxIndex];
        int index = nextIndex(size,maxIndex);
        long res = 0L;
        //-----创建单调栈的结构-----
        Stack stack = new Stack();
        //----首先将数组中最大的元素扔进栈中----
        stack.push(new Pair(value));
        while(index != maxIndex){
            value = arr[index];
            // 如果栈不为空,且当前遍历到的元素大于栈顶的元素值,那么此处要更新res,可以增加山峰对了
            while(!stack.isEmpty() && stack.peek().value < value) {
                int times = stack.pop().times;
                res += getInternalSum(times) + (2 * times);
            }
            if(!stack.isEmpty() && stack.peek().value == value){
                stack.peek().times++;
            }else{
                stack.push(new Pair(value));
            }
            index = nextIndex(size,index);
        }
        // 当栈不为null时,继续遍历
        while(!stack.isEmpty()){
            int times = stack.pop().times;
            res += getInternalSum(times);
            if(!stack.isEmpty()){
                // 首先加上一个times
                res += times;
                if(stack.size() > 1){
                    //如果栈中剩余并不是最大的值,那么再加一个times,左右都可以找到最大的山峰
                    res += times;
                } else{
                    // 否则,如果最大的山峰只有一个,那么只需要加一次times即可
                    res += stack.peek().times > 1 ? times : 0;
                }
            }
        }
        return res;
    }

    /**
     * 获取环中当前位置的下一元素的索引
     * @param size
     * @param maxIndex
     * @return
     */
    private static int nextIndex(int size, int maxIndex) {
        return maxIndex == size - 1 ? 0 : maxIndex + 1;
    }

    /**
     * 获取连续times相等的山峰可以构成有效山峰对的数量
     * @param times
     * @return
     */
    private static long getInternalSum(int times) {
        return times == 1L ? 0 : (long)times * (long)(times - 1) / 2L;
    }

    // 测试函数
    public static void main(String[] args) {
        int[] arr = {1,2,3,3,3,4,4,5,5,6};
        System.out.println(communications(arr));
    }
}

你可能感兴趣的:(算法笔记——单调栈)