优先队列专项

170 阅读3分钟

滑动窗口的最大值

题目

版本1 采用ArrayQueue作为双端队列 正确 有冗余操作

    public int[] maxSlidingWindow(int[] nums, int k) {

        // 滑动窗口的最大值
        // nums中, 一个长度为k的滑动窗口, 从左向右移动, 输出每个时刻, 滑动窗口的最大值

        // 这道题从目的上类似求有限制的子序列和那道题, 窗口k就是限制, 对于每个元素, 寻找距离k范围内的最大值
        // 因此可以使用单调队列来实现, 队列中维护的是索引值i, 对应的顺序按照nums[i]从大到小排列

        // 采用双端队列
        ArrayDeque<Integer> queue = new ArrayDeque<>();

        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < nums.length; i ++) {
            int preMax = Integer.MIN_VALUE;

            // 弹出元素 弹出时, 需要校验索引是否在距离k之内
            if (i >= k - 1 && !queue.isEmpty()) {
                // 注意i - queue.peekFirst()是索引距离, 因此
                while (!queue.isEmpty() && i - queue.peekFirst() + 1> k) {
                    queue.pollFirst();
                }
                if (!queue.isEmpty()) {
                    preMax = nums[queue.peek()];
                }
            }

            if (i >= k - 1) {
                // 需要记录最大值的结果
                ans.add(Math.max(preMax, nums[i]));
            }

            // 添加元素到队列中
            if (queue.isEmpty()) {
                queue.offer(i);
            } else {
                if (nums[i] > nums[queue.peek()]) {
                    // 添加到队头
                    queue.addFirst(i);
                } else {
                    // 从队尾开始比较
                    while (nums[queue.peekLast()] < nums[i]) {
                        queue.pollLast();
                    }
                    queue.offerLast(i);
                }
            }

        }

        int [] ansArray = new int[ans.size()];
        for (int i = 0; i < ans.size(); i ++) {
            ansArray[i] = ans.get(i);
        }

        return ansArray;
    }

正确的原因

(1) 对于每个元素, 当窗口包括当前元素的时候, 之前窗口的最大值, 由双端队列头部元素得到

(2) 每个元素, 需要添加到双端队列中, 如果大于头部元素, 直接添加到队列头, 否则从队列尾部, 比较弹出所有比当前元素小的值, 然后添加到队列尾部

缺点

(1) 添加元素的时候无需从头部添加, 直接从队尾开始判断清空元素即可, 这样队列中就不用维护比较多的元素

(2) 用一个list来存储每次的结果, 最后再转成数组, 比较耗时

版本2 用的Deque 比较好的版本

    public int[] maxSlidingWindow(int[] nums, int k) {

        Deque<Integer> deque = new LinkedList<Integer>();

        // 结果的大小可以直接计算出来
        int [] ans = new int[nums.length - k + 1];
        for (int i = 0; i < nums.length; i ++) {
            // 每次都从队列尾部开始比较, 将比当前值小的元素全部弹出, 队列中最少维护一个最近最大的元素即可
            // 每次先添加当前元素, 这样直接获取队列的头部元素, 就是最大值了, 无需再比较了
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }
            deque.offerLast(i);

            // 每次弹出前, 清理不在窗口中的索引
            while (!deque.isEmpty() && i - deque.peekFirst() + 1 > k) {
                deque.pollFirst();
            }

            if (i + 1 >= k) {
                // 记录最大值
                ans[i - k + 1] = nums[deque.peekFirst()];
            }

        }
        return ans;
    }

正确的原因

(1) 每次循环 先添加元素, 此时队列头部的元素就是结果, 无需比较一次

(2) 最终结果数组的大小就是nums.length - k + 1, 可以直接赋值

带限制的子序列和

题目

image.png

版本1 dp + 单调栈 正确

    public int constrainedSubsetSum(int[] nums, int k) {

        // 带限制的子序列的和, 构成结果的子序列, 任意两个数字之间的索引距离不能大于k
        // 采用dp + 单调队列的解法

        // dp[i] 表示以索引i对应的元素作为子序列结尾的, 子序列的和的最大值
        int [] dp = new int[nums.length];

        // base case
        dp[0] = nums[0];

        // 构造一个单调队列, 来维护一个索引
        // 在队列中, 索引i的顺序是由dp[i]的大小决定的, 从大到小排列
        Deque<Integer> queue = new LinkedList<>();
        queue.offer(0);

        // 遍历一遍nums, 记录以任何i结尾的子序列的最大值
        // 注意这里最大值要设为第一个元素, 不然第一个元素就不会参与到最大值的比较了
        int max = dp[0];
        for (int i = 1; i < nums.length; i ++) {

            // 弹出不符合索引条件的索引
            // 这里题目要求 索引差值 <= k 即可
            while (!queue.isEmpty() && i - queue.peekFirst() > k) {
                queue.pollFirst();
            }

            // k最小是1, 那么队列中一定会有元素
            // 队列头部的元素就是符合索引条件的之前的子序列和最大值
            // 注意这里dp[queue.peekFirst()]可能是负值, 因此需要和0比较大小了再相加
            // 也就是之前子序列的和, 可能不会使用
            dp[i] = Math.max(0, dp[queue.peekFirst()]) + nums[i];
            max = Math.max(max, dp[i]);

            // 添加当前元素到队列中
            while (!queue.isEmpty() && dp[queue.peekLast()] < dp[i]) {
                queue.pollLast();
            }
            queue.offerLast(i);

        }

        return max;

    }

正确的原因

(1) 注意int max = dp[0]; 如果单独把dp[0]挑出来作为base case, 然后循环的时候从1开始, 那么最大值应该设置为dp[0], 不然dp[0]就无法参与最大值的比较

(2) i - queue.peekFirst() > k, 题目要求的是索引差值 <= k, 因此要注意范围

(3) dp[i] = Math.max(0, dp[queue.peekFirst()]) + nums[i]; 需要注意, 根据dp数组的定义, 如果之前dp的最大值是小于0的, 是可以不实用之前的值的

最大宽度坡

题目

image.png

版本1 单调栈

    public int maxWidthRamp(int[] A) {
        if (A.length <= 1) {
            return 0;
        }

        // 求数组中元素的宽度坡, 其实求得就是对于每个i, 找到距离它最远的j, 同时满足A[i] <= A[j]
        // 那么对于每个i, 是不是都需要去找一次呢? 假设我当前A[i1] = 2, 那么A[i2] = 3, i2 > i1
        // 此时i2的值需要去寻找j吗, 答案是不需要, 因为你找到的j 能满足A[j] > A[i2], 那么一定满足A[j] > A[i1]
        // 并且i1得到的宽度, 显然比i2得到的宽度宽
        // 那么我们可以采用一个单调栈, 从索引0开始存储数组中的降序的索引, 这样栈里的所有索引就是必须要计算一次宽度的
        // 这样就大大减少了我们需要计算的i的数目
        // 因此也就是如果在数组左边有比自己小的元素, 那么自己这个位置就不需要计算, 只需要计算左边最小的那个元素的宽度即可
        // 栈中第一个元素一定是索引0

        Stack<Integer> stack = new Stack<>();
        stack.push(0);


        for (int i = 1; i < A.length; i ++) {
            // 单调栈中保存元素值降序的索引
            if (A[i] < A[stack.peek()]) {
                stack.push(i);
            }
        }

        // 然后从尾部开始, 计算宽度, 即寻找对应的j
        int max = Integer.MIN_VALUE;
        for (int i = A.length - 1; i >= 0; i --) {
            if (!stack.isEmpty() && A[i] >= A[stack.peek()] && i >= stack.peek()) {
                max = Math.max(i - stack.pop(), max);
                // 这里的i需要自增一下, 来抵消i -- 的效果, 让当前的元素重新进行一次判断
                i ++;
            }
        }

        return max;
    }

正确的原因

(1) 通过单调栈 减少了可能的i

(2) 然后再从数组尾部遍历, 寻找可能的j, 注意在计算一次宽度后, 需要将当前元素再次和栈顶元素计算一次, 因为这种情况, 可能依旧满足A[j] >= A[栈顶元素]