算法题解 —— 栈(6-9)

内容来源于自己的刷题笔记,对一些题目进行方法总结,用 java 语言实现。

6. 生成窗口最大值数组:
  1. 题目描述:

    有一个整型数组 arr 和一个大小为 w 的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。

    例如,数组为 [4,3,5,4,3,3,6,7],窗口大小为3时:

    [4 3 5] 4 3 3 6 7 窗口中最大值为5

    4 [3 5 4] 3 3 6 7 窗口中最大值为5

    4 3 [5 4 3] 3 6 7 窗口中最大值为5

    4 3 5 [4 3 3] 6 7 窗口中最大值为4

    4 3 5 4 [3 3 6] 7 窗口中最大值为6

    4 3 5 4 3 [3 6 7] 窗口中最大值为7

    如果数组长度为 n,窗口大小为 w,则一共产生 n-w+1 个窗口的最大值。

    请实现一个函数:

    • 输入:整型数组 arr,窗口大小为 w
    • 输出:一个长度为 n-w+1 的数组 res,res[i] 表示每一种窗口状态下的最大值

    以本题为例,结果应该返回 [5,5,5,4,6,7]。

  2. 解题思路:

    用双端队列来实现窗口最大值的更新,首先生成双端队列 qmax,qmax 中存储数组中 arr 中的下标。

    假设遍历到 arr[i],qmax 的放入规则为:

    1. 如果 qmax 为空,直接把下标 i 放进 qmax,放入过程结束。
    2. 如果 qmax 不为空,取出当前 qmax 队尾存放的下标,假设为 j。
      1. 如果 arr[j] > arr[i],直接把下标 i 放进队尾存放的下标,假设为 j。
      2. 如果 arr[j] <= arr[i],把 j 从 qmax 中弹出,重复 qmax 的放入规则。

    这样就维护了一个长度为 w 的子数组的最大值更新的结构。

  3. 代码实现:

    public static int[] getMaxWindow(int[] arr,int w){
            if (arr == null || arr.length <= 0){
                return null;
            }
            LinkedList<Integer> qmax = new LinkedList<>();
            int[] res = new int[arr.length - w + 1];
            int index = 0;
            for (int i = 0;i < arr.length;i++){
                //如果队列不为空,并且队尾存储序号的元素小于等于要放入序号的元素,就将队尾序号弹出,重复队列的放入规则
                while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[i]){
                    qmax.pollLast();
                }
                //队列为空放入队列中,队列不为空但队尾存储序号的元素大于要放入序号的元素,放入队列尾部,两者的逻辑都是一样的
                qmax.addLast(i);
                //队列首部索引过期,直接弹出
                if (qmax.peekFirst() == i - w){
                    qmax.pollFirst();
                }
                //当元素的索引大于等于窗口大小时,可以开始存储窗口数组
                if (i >= w - 1){
                    res[index++] = arr[qmax.peekFirst()];
                }
            }
            return res;
        }
    
7. 单调栈结构:
  1. 题目描述:

    基础问题:给定一个不含有重复值的数组 arr,找到每一个 i 位置左边和右边离 i 位置最近且值比 arr[i] 小的位置。返回所有位置相应的信息。

    举例:

    arr={3,4,1,5,6,2,7}

    返回如下二维数组作为结果:

    {

    ​ {-1,2},

    ​ {0,2},

    ​ {-1,-1},

    ​ {2,5},

    ​ {3,5},

    ​ {2,-1},

    ​ {5,-1}

    }

    -1表示不存在。

    进阶问题:给定一个可能含有重复值的数组 arr,找到每一个 i 位置左边和右边离 i 位置最近且值比 arr[i] 小的位置。返回所有位置相应的信息。

  2. 解题思路:

    两个问题都可以使用单调栈的思路进行求解:

    准备一个栈,记为 stack<Integer>,栈中放的元素是数组的位置,开始时 stack 为空。如果找到每一个 i 位置左边和右边离 i 位置最近且值比 arr[i] 小的位置,那么需要让 stack 从栈顶到栈底的位置所代表的值是严格递减的;如果找到每一个 i 位置左边和右边离 i 位置最近且只比 arr[i] 大的位置,那么需要让 stack 从栈顶到栈底的位置所代表的值是严格递增的。

    无重复:在单调栈中,如果 x 位置被弹出,在栈中位于 x 位置下面的位置就是 x 位置离左边 x 位置最近且值比 arr[x] 小的位置;当前遍历到的位置就是 x 位置右边离 x 位置最近且值比 arr[x] 小的位置。假设 stack 当前栈顶位置是 x,值是5;x 下面是 i 位置,值是1;当前遍历到 j 位置,值是4。

    如果是有重复的数组,栈中存储的不是单个索引而是链表结构的索引,其他逻辑类似,但此时离左边 x 位置最近且值比 arr[x] 小的位置应该是链表结构中最晚加入的值,即 stack.peek().get(stack.peek().size() - 1)。

  3. 代码实现:

    /**
         * 不重复的数组
         * @param arr
         * @return
         */
        public int[][] getNearLessNoRepeat(int[] arr){
            int[][] res = new int[arr.length][2];
            Stack<Integer> stack = new Stack<>();
            for (int i = 0;i < arr.length;i++){
                while (!stack.isEmpty() && arr[stack.peek()] > arr[i]){
                    int popIndex = stack.pop();
                    int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
                    res[popIndex][0] = leftLessIndex;
                    res[popIndex][1] = i;
                }
                stack.push(i);
            }
            while (!stack.isEmpty()){
                int popIndex = stack.pop();
                int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
                res[popIndex][0] = leftLessIndex;
                res[popIndex][1] = -1;
            }
            return res;
        }
    
        /**
         * 重复数组
         * @param arr
         * @return
         */
        public int[][] getNearLess(int[] arr){
            int[][] res = new int[arr.length][2];
            Stack<List<Integer>> stack = new Stack<>();
            for (int i = 0;i < arr.length;i++){
                while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]){
                    List<Integer> popIs = stack.pop();
                    //取位于下面位置的列表中,最晚加入的那个
                    int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
                    for (Integer popi : popIs){
                        res[popi][0] = leftLessIndex;
                        res[popi][1] = i;
                    }
                }
                if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]){
                    stack.peek().add(i);
                }else {
                    ArrayList<Integer> list = new ArrayList<>();
                    list.add(i);
                    stack.push(list);
                }
            }
            while (!stack.isEmpty()){
                List<Integer> popIs = stack.pop();
                //取位于下面位置的列表中,最晚加入的那个
                int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
                for (Integer popi : popIs){
                    res[popi][0] = leftLessIndex;
                    res[popi][1] = -1;
                }
            }
            return res;
        }
    
8. 求最大子矩阵的大小:
  1. 题目描述:

    给定一个整型矩阵 map,其中的值只有0和1两种,求其中全是1的所有矩形区域中,最大的矩形区域为1的数量。

    例如:

    1 1 1 0

    其中,最大的矩阵区域有3个1,所以返回3。

    1 0 1 1

    1 1 1 1

    1 1 1 0

    其中,最大的矩阵区域有6个1,所以返回3。

  2. 解题思路:

    1. 矩阵的行数为 N,以每一行做切割,统计以当前行作为底的情况下,每个位置往上(包括 j 位置)1的数量。使用高度数组 height 表示。
    2. 对于每一次切割,都利用更新后的 height 数组来求出当前行为底的情况下,最大的矩形是什么。该步骤类似于在一个直方图内求出最大矩形面积。生成一个栈,记为 stack,从左到右遍历 height 数组,只有当前 i 位置的值 height[i] 大于当前栈顶位置所代表的值(height[stack.peek()]),则 i 位置才可以压入 stack;如果当前 i 位置的值 height[i] 小于或等于当前栈顶位置所代表的值(height[stack.peek()]),则把栈中存的位置不断弹出,直到某一个栈顶所代表的值小于 height[i],再把位置 i 压入,并在此期间进行如下操作:
      1. 假设当前弹出的栈顶位置记为位置 j,弹出栈顶之后,新的栈顶记为 k。然后开始考虑位置 j 的柱子向右和向左最远能扩到哪里。
      2. 对位置 j 的柱子来说,向右最远能扩到的位置:如果 height[j] > height[i],那么 i - 1 就是向右能扩到的最远距离;如果 height[j] == height[i],那么 i - 1 位置不一定是向右能扩到的最远距离,只是起码能扩到的位置,但是不要紧,这种情况下,i 位置的柱子向左也必然可以扩到 j 位置。
      3. 对位置 j 的柱子来说,向左最远能扩到的位置:根据单调栈的性质,k 位置的值是 j 位置的值左边离 j 位置最近的比 j 位置的值小的位置,所以 j 位置的柱子向左最远可以扩到 k + 1 位置。
      4. 综上所述,j 位置的柱子能扩出来的最大矩形是 (i - k - 1) * height[j]。
  3. 代码实现:

    public int maxRecSize(int[][] map){
            if (map == null || map.length == 0 || map[0].length == 0){
                return 0;
            }
            int maxArea = 0;
            int[] height = new int[map[0].length];
            for (int i = 0;i < map.length;i++){
                for (int j = 0;j < map[0].length;j++){
                    height[j] = map[i][j] == 0 ? 0 : height[j]+1;
                }
                maxArea = Math.max(maxRecFromBottom(height),maxArea);
            }
            return maxArea;
        }
    
        public int maxRecFromBottom(int[] height){
            if (height == null || height.length == 0){
                return 0;
            }
            int maxArea = 0;
            Stack<Integer> stack = new Stack<>();
            for (int i = 0;i < height.length;i++){
                while (!stack.isEmpty() && height[i] <= height[stack.peek()]){
                    int j = stack.pop();
                    int k = stack.isEmpty() ? -1 : stack.peek();
                    int curArea = (i - k -1)*height[j];
                    maxArea = Math.max(maxArea,curArea);
                }
                stack.push(i);
            }
            while (!stack.isEmpty()){
                int j = stack.pop();
                int k = stack.isEmpty() ? -1 : stack.peek();
                int curArea = (height.length - k -1)*height[j];
                maxArea = Math.max(maxArea,curArea);
            }
            return maxArea;
        }
    
9. 最大值减去最小值小于或等于 num 的子数组数量:
  1. 题目描述:

    给定数组 arr 和整数 num,共返回有多少个子数组满足如下情况:
    max(arr[i…j]) - min(arr[i…j]) <= num

    max(arr[i…j]) 表示子数组 arr[i…j] 中的最大值,min(arr[i…j]) 表示子数组 arr[i…j] 中的最小值。

  2. 解题思路:

    首先分析题目中的条件,得出两个结论:

    1. 如果子数组 max(arr[i…j]) - min(arr[i…j]) <= num,那么 arr[i…j] 中的每一个子数组都满足条件:因为子数组只有可能范围更小或者不变,不可能变大。
    2. 如果子数组 arr[i…j] 不满足条件,则所有包含 arr[i…j] 的子数组都满足条件:因为包含子数组的数组只有可能范围更大或者不变,不可能变小。

    生成两个双端队列 qmax 和 qmin,生成两个整型变量 i 和 j,表示子数组的范围,即 arr[i…j],生成整型变量 res,表示所有满足条件的子数组数量。

    首先,j 不断向右移动,如果满足条件持续向右生长,一旦发现不满足,停止生长,此时,arr[i…j]、arr[i…j-1]…arr[i…i],一共 j - i 个子数组符合,此时左边必须是固定的,不然会与后面的计数重复;然后 i 开始生长,直到满足条件停止生长。

  3. 代码实现:

    public static int getNum(int[] arr,int num){
            if (arr == null || arr.length == 0 || num < 0){
                return 0;
            }
            LinkedList<Integer> qmin = new LinkedList<>();
            LinkedList<Integer> qmax = new LinkedList<>();
            int i = 0;
            int j = 0;
            int res = 0;
            while (i < arr.length){
                while (j < arr.length){
                    if (qmin.isEmpty() || qmin.peekLast() != j){
                        while (!qmin.isEmpty() && arr[qmin.peekLast()] >= arr[j]){
                            qmin.pollLast();
                        }
                        qmin.addLast(j);
                        while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[j]){
                            qmax.pollLast();
                        }
                        qmax.addLast(j);
                    }
                    if (arr[qmax.getFirst()] - arr[qmin.getFirst()] > num){
                        break;
                    }
                    j++;
                }
                res += j - 1;
                if (qmin.peekFirst() == i){
                    qmin.pollFirst();
                }
                if (qmax.peekFirst() == i){
                    qmax.pollFirst();
                }
                i++;
            }
            return res;
        }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值