单调栈和单调队列及其数组实现——Java版

前言

为什么写这篇文章呢?是因为笔者今天在写单调队列题目239. Sliding Window Maximum时,发现使用双端队列的效率有点差强人意,所以尝试利用数组来模拟单调队列从而提升代码的运行效率,其次利用数组模拟这些数据结果也能加深对栈、队列的理解。

1 单调栈

  1. 单调栈介绍:单调栈其实就是栈,只不过是人为地设置一些规则,使得只有在满足一定条件的情况下才能入栈或者出栈,这样就使得栈具有“单调性”。而这样做的好处是避免重复遍历从而提高效率。下面进入实战环节,我们以具体的题目进行讲解。
  2. 单调栈能解决的问题:求下一个更大的元素、求下一个更小的元素、求左右两边第一个更大/更小的元素:
    • 右边第一个较大的数字:逆序遍历,单调递减栈 —— 如果当前元素大于等于栈顶元素,则栈顶出栈。
    • 右边第一个较小的数字:逆序遍历,单调递增栈 —— 如果当前元素小于等于栈顶元素,则栈顶出栈。
    • 左边第一个较大的数字:正序遍历,单调递减栈 —— 如果当前元素大于等于栈顶元素,则栈顶出栈。
    • 左边第一个较小的数字:正序遍历,单调递增栈 —— 如果当前元素小于等于栈顶元素,则栈顶出栈。
  • 右边第一个较大或等于的下标(可以等于,不存在就为数组长度)
int n = nums.length;
Deque<Integer> rightStack = new LinkedList<>();
int[] rightBiggerIdx = new int[n];
for (int i = n - 1; i >= 0; i--) {
    while (!rightStack.isEmpty() && nums[i] > nums[rightStack.peek()]) {
        rightStack.pop();
    }
    rightBiggerIdx[i] = rightStack.isEmpty() ? n : rightStack.peek();
    rightStack.push(i);
}
  • 右边第一个较小的下标(不能等于,不存在就为数组长度)
int n = nums.length;
Deque<Integer> rightSmallerStack = new LinkedList<>();
int[] rightSmallerIdx = new int[n];
for (int i = n - 1; i >= 0; i--) {
    while (!rightSmallerStack.isEmpty() && nums[i] <= nums[rightSmallerStack.peek()]) {
        rightSmallerStack.pop();
    }
    rightSmallerIdx[i] = rightSmallerStack.isEmpty() ? n : rightSmallerStack.peek();
    rightSmallerStack.push(i);
}
  • 左边第一个较大的下标(不能等于,不存在就为 -1)
int n = nums.length;
Deque<Integer> leftStack = new LinkedList<>();
int[] leftBiggerIdx = new int[n];
for (int i = 0; i < n; i++) {
    while (!leftStack.isEmpty() && nums[i] >= nums[leftStack.peek()]) {
        leftStack.pop();
    }
    leftBiggerIdx[i] = leftStack.isEmpty() ? -1 : leftStack.peek();
    leftStack.push(i);
}
  • 左边第一个较小的下标(不能等于,不存在就为 -1)
int n = nums.length;
Deque<Integer> leftSmallerStack = new LinkedList<>();
int[] leftSmallerIdx = new int[n];
for (int i = 0; i < n; i++) {
    while (!leftSmallerStack.isEmpty() && nums[i] <= nums[leftSmallerStack.peek()]) {
        leftSmallerStack.pop();
    }
    leftSmallerIdx[i] = leftSmallerStack.isEmpty() ? -1 : leftSmallerStack.peek();
    leftSmallerStack.push(i);
}

1.1 下一个更大元素 I

题目链接:496. 下一个更大元素 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 。

 

提示:

    1 <= nums1.length <= nums2.length <= 1000
    0 <= nums1[i], nums2[i] <= 104
    nums1和nums2中所有整数 互不相同
    nums1 中的所有整数同样出现在 nums2 中

 

进阶:你可以设计一个时间复杂度为 O(nums1.length + nums2.length) 的解决方案吗?

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

思路:先预处理 nums2 数组,找出每个数字右边第一个比当前数字大的数字(不存在的话就是 -1),并存到 map 中。然后遍历 nums1 中的每个数字,取 map 对应的值即可(找右边第一个较大的数:逆序遍历,递减栈——如果当前数字大于等于栈顶,栈顶出栈

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int len2 = nums2.length;
        int[] monoStack = new int[len2];
        int peek = -1;
        // key-nums2中的数字,value- nums2 中右边第一个比 key 大的数字
        Map<Integer, Integer> biggerRight = new HashMap<>();
        for (int i = len2 - 1; i >= 0; i--) {
            while (peek != -1 && nums2[i] >= monoStack[peek]) {
                peek -= 1;
            }
            // 求 nums2 中每个数字右边第一个比它大的数字
            biggerRight.put(nums2[i], peek == -1 ? -1 : monoStack[peek]);
            // 当前数字入栈
            monoStack[++peek] = nums2[i];
        }

        int[] ans = new int[nums1.length];
        for (int i = 0; i < nums1.length; i++) {
            ans[i] = biggerRight.get(nums1[i]);
        }

        return ans;
    }
}

1.2 每日温度

题目链接: 739. 每日温度

给定一个整数数组 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]

 

提示:

    1 <= temperatures.length <= 105
    30 <= temperatures[i] <= 100



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

思路:跟 1.1 小节中的 496. 下一个更大元素 I 思路一致,甚至比那道题还要“纯粹”。(找右边第一个较大的数:逆序遍历,递减栈——如果当前数字大于等于栈顶,栈顶出栈)。只不过这道题单调栈中存的是下标。

代码:

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        // 求右边第一个比当前值大的值:逆序、单调递减栈
        int n = temperatures.length;
        // 存储的是下标
        int[] monoStack = new int[n];
        int peek = -1;
        int[] ans = new int[n];
        for (int i = n - 1; i >= 0; i--) {
            while (peek != -1 && temperatures[i] >= temperatures[monoStack[peek]]) {
                peek -= 1;
            }
            ans[i] = peek == -1 ? 0 : monoStack[peek] - i;
            monoStack[++peek] = i;
        }
        return ans;
    }
}

1.3 下一个更大元素 II

题目链接: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]

 

提示:

    1 <= nums.length <= 104
    -109 <= nums[i] <= 109

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/next-greater-element-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
  • 思路:找下一个更大的元素,很明显用单调栈来解决。但是本题有一点小 trick,就是这个数组是循环数组,解决循环数组的套路是在原数组后面再拼接一个原数组,如下图,这样原数组最后一个3 的下一个更大的 4 就能“顺序”找到了。而且在实现上也可以通过对原数组长度取余的方法模拟拼接数组。

在这里插入图片描述

  • 代码:
    class Solution {
        public int[] nextGreaterElements(int[] nums) {
            // 单调栈 + 循环数组
            int length = nums.length;
            // 单调栈,存储的是原数组种元素的下标
            int[] stack = new int[length * 2];
            int peek = -1;
            int[] ans = new int[length];
            Arrays.fill(ans, -1);
            for (int i = 0; i < length * 2; i++) {
                // 我们这里模拟的拼接数组
                int idxReal = i % length;
                // peek != -1  代表栈不为空 & 如果当前元素大于栈顶下标对应的元素,那么栈顶下标所对应的元素就找到了右边第一个比它大的数
                while (peek != -1 && nums[idxReal] > nums[stack[peek]]) {
                    int idxBefore = stack[peek--] % length;
                    ans[idxBefore] = nums[idxReal];
                }
                // 当前元素下标入栈
                stack[++peek] = idxReal;
            }
    
            return ans;
        }
    }
    
  • 时间复杂度:O(N) ,我们模拟 2N 个数字,每个数字最多进出栈各一次,所以总的时间复杂度为 O(N)
  • 空间复杂度:O(N),我用使用了额外的 2N 的单调栈的空间

1.4 子数组的最小值之和

题目链接:907. 子数组的最小值之和

给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。

由于答案可能很大,因此 返回答案模 10^9 + 7 。

 

示例 1:

输入:arr = [3,1,2,4]
输出:17
解释:
子数组为 [3][1][2][4][3,1][1,2][2,4][3,1,2][1,2,4][3,1,2,4]。 
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
示例 2:

输入:arr = [11,81,94,43,3]
输出:444
 

提示:

1 <= arr.length <= 3 * 104
1 <= arr[i] <= 3 * 104


来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/sum-of-subarray-minimums
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
    private final int MOD = 1000000007;
    public int sumSubarrayMins(int[] nums) {
        int n = nums.length;
        int[] monoStack = new int[n];
        int peek = -1;
        int[] left = new int[n];
        for (int i = 0; i < n; i++) {
            while (peek != -1 && nums[i] < nums[monoStack[peek]]) {
                peek--;
            }

            if (peek == -1) {
                left[i] = -1;
            } else {
                left[i] = monoStack[peek];
            }
            monoStack[++peek] = i;
        }
        peek = -1;
        int[] right = new int[n];
        for (int i = n - 1; i >= 0; i--) {
            while (peek != -1 && nums[i] <= nums[monoStack[peek]]) {
                peek--;
            }

            if (peek == -1) {
                right[i] = n;
            } else {
                right[i] = monoStack[peek];
            }

            monoStack[++peek] = i;
        }

        long res = 0L;
        for (int i = 0; i < n; i++) {
            res = (res + (long) (i - left[i]) * (right[i] - i) * nums[i]) % MOD;
        }

        return (int) res;
    }
}

1.5 柱状图中最大的矩形

题目链接:84. 柱状图中最大的矩形
示例1

示例2

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

求在该柱状图中,能够勾勒出来的矩形的最大面积。

 

示例 1:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10

示例 2:
输入: heights = [2,4]
输出: 4
 

提示:
1 <= heights.length <=105
0 <= heights[i] <= 104

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/largest-rectangle-in-histogram
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
  • 朴素思路:暴力的解法,以每个柱子的高度 heights[i] 为为宽度,然后向左右进行扩展到(第一个小于当前柱子高度的左右边界)left、right,那么 (left, right) 之间的柱子高度都是高于heights[i] 的,那么 [left + 1, right - 1] 之间的矩形都可以将 heights[i] 作为高度组成矩形。如下图所示红色边框即为 height[i] 处矩形高为宽度的最大矩形。这样的时间复杂度为 O(N^2),会超时。
    在这里插入图片描述

  • 单调栈思路:通过上述朴素解法的描述可知,我们的目的其实是求以某一个矩形高度 heights[i] 为宽度的大矩形的长,也就是找左右两边第一个高度小于当前高度的边界,自然就想到用单调栈进行优化。具体地,我们可以使用单调递增栈,如果遇到当前高度小于栈顶高度,那么栈顶右边的第一个小于它的位置找到了(即找到了 right),而栈顶左边第一个小于它的正是上一个栈顶。特别多,为了方便处理首尾两个小矩形,我们增加两个最小值作为哨兵。

    class Solution {
        public int largestRectangleArea(int[] heights) {
            // 思路:以当前矩形为宽度的矩形,其高度一定要大于等于当前矩形,所以找左右两边第一个小于当前矩形的位置,使用单调栈。
            int n = heights.length;
            int ans = 0;
            // 增加哨兵
            int[] heightsNew = new int[n + 2];
            for (int i = 0; i < n; i++) heightsNew[i + 1] = heights[i];
    
            int[] monoStack = new int[n + 2];
            int peek = -1;
            n += 2; // 将数组长度更新为添加了哨兵后的新长度
            for (int i = 0; i < n; i++) {
                // 当前栈顶右边界找到
                while (peek != -1 && heightsNew[i] < heightsNew[monoStack[peek]]) {
                    int right = i;
                    int currHeight = heightsNew[monoStack[peek--]];
                    int left = monoStack[peek];
                    // 当前矩形为宽度的面积
                    ans = Math.max(ans, currHeight * (right - left - 1));
                }
                monoStack[++peek] = i;
            }
    
            return ans;
        }
    }
    

2 单调队列

讲解单调队列之前,先简单介绍下什么是队列,以及什么是单调队列,特别地,用 Java 的同学肯定还知道优先级队列:

  • 队列:队列是一种先进先出的数据结构,一般是从队列尾部入队,从队列头部出队,非常像我们日常打饭、买火车票时排列的队伍(插队的情况除外~)
  • 单调队列:即单调递减或单调递增的队列。使用频率不高,但在有些程序中会有非同寻常的作用。(摘自百度,使用频率不高扎心了)
  • 优先级队列:也即堆这种数据结构,特点是队首是具有最高优先级的元素(这里的最高优先级通常是指最大或者最小)。注意优先级队列与单调队列的区别:优先级队列只保证队首为最值,或者父子间有一定的大小关系,但是整个队列不保证是单调有序的;而单调队列保证整个队列是单调递增或单调递减的。

了解了单调队列的定义,下面我们就以一到实际的题目来看看如何应用吧。

2.1 滑动窗口最大值

题目链接:239. 滑动窗口最大值

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

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

 

示例 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

示例 2:

输入:nums = [1], k = 1
输出:[1]

 

提示:

    1 <= nums.length <= 105
    -104 <= nums[i] <= 104
    1 <= k <= nums.length

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

分析:本题题目名称中虽然待有“滑动窗口”的字样,但是本题却不是用滑动窗口来解的。首先我们可以用暴力解法:每滑动到一个新的窗口,我们都用 O(k) 的时间复杂度去遍历当前窗口的所有值从而获得最大值。但是这样势必效率较低,因为窗口每次滑动一位,也就意味着仅有一个元素加入队尾并且仅有一个元素从队首移除,如果每一次都在仅有两个元素变动的情况下去遍历整个 k 范围的数组去寻找最值,有点过于奢侈了。如果我们仅比较新加入的元素与原来窗口的最大值,就能得到当前窗口的最大值。自然而然,我们想到使用优先级队列这种数据结构,每次都能在 O(1) 时间复杂度内获得出口内的最大值,但是这样又会引发另外一个问题:由于窗口是从左往右滑动的,我们现在是可以利用优先级队列在 O(1) 时间复杂度内获得出口内的最大值,但是如果这个最大值滑出当前窗口了怎么办?很多同学想当然地认为将其从优先级队列的队首移出即可,但是仔细想想,这样真的准确吗?如果仅仅是简单地将不在窗口内的最大值移除,那你一定能保证移除后新的队首元素(新的最大值)也一定在窗口范围内吗?所以我们需要利用一个循环,只要当前的队首(最大值)不在窗口内,就将其移除,这样我们就能保证每次获取到的最大值都是在窗口内的有效最大值。

  1. 方法一:暴力法
  • 思路:(优先级队列)本题题目名称中虽然待有“滑动窗口”的字样,但是本题却不是用滑动窗口来解的。首先我们可以用暴力解法:每滑动到一个新的窗口,我们都用 O(k) 的时间复杂度去遍历当前窗口的所有值从而获得最大值。但是这样势必效率较低,因为窗口每次滑动一位,也就意味着仅有一个元素加入队尾并且仅有一个元素从队首移除,如果每一次都在仅有两个元素变动的情况下去遍历整个 k 范围的数组去寻找最值,有点过于奢侈了。
  • 代码:
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 暴力解法
        if (nums == null || nums.length < 1 || k < 1) {
            throw new IllegalArgumentException();
        }
        int length = nums.length;
        int[] res = new int[length - k + 1];
        // i 为窗口的左边界
        for (int i = 0; i < length - k + 1; i++) {
            int max = Integer.MIN_VALUE;
            // j 从左边界开始,滑过窗口的范围
            for (int j = i; j < i + k; j++) {
                max = Math.max(max, nums[j]); // 记录窗口内的最大值
            }
            res[i] = max;
        }
        return res;
    }
}
  • 时间复杂度 : O(nk) 不出所料,超时。。。
  • 空间复杂度: O(1)
    在这里插入图片描述
  1. 方法二:优先级队列
  • 思路:方法一需要每次都比较 k 个元素,有点浪费时间,如果我们仅比较新加入的元素与原来窗口的最大值,就能得到当前窗口的最大值。自然而然,我们想到使用优先级队列这种数据结构,每次都能在 O(1) 时间复杂度内获得出口内的最大值,但是这样又会引发另外一个问题:由于窗口是从左往右滑动的,我们现在是可以利用优先级队列在 O(1) 时间复杂度内获得出口内的最大值,但是如果这个最大值滑出当前窗口了怎么办?很多同学想当然地认为将其从优先级队列的队首移出即可,但是仔细想想,这样真的准确吗?如果仅仅是简单地将不在窗口内的最大值移除,那你一定能保证移除后新的队首元素(新的最大值)也一定在窗口范围内吗?所以我们需要利用一个循环,只要当前的队首(最大值)不在窗口内,就将其移除,这样我们就能保证每次获取到的最大值都是在窗口内的有效最大值。
  • 代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 优先级队列:先将当前元素的下标加入队列(队列按照值从大到小排序下标),在记录最大值之前需要先判断当前队列头是否还在窗口范围内
        if (nums == null || nums.length < 1 || k < 1) {
            throw new IllegalArgumentException();
        }
        int length = nums.length;
        int[] res = new int[length - k + 1];
        // 优先级队列(大根堆),存储的是元素下标,按照元素大小从大到小排列
        PriorityQueue<Integer> pq = new PriorityQueue<>((o1, o2) -> nums[o2] - nums[o1]);
        for (int i = 0; i < length; i++) {
            pq.offer(i);

            // 窗口的左边界
            int left = i - k + 1;
            // 如果队列最大值不在
            while (!pq.isEmpty() && pq.peek() < left) {
                pq.poll();
            }
            // 如果窗口形成
            if (left >= 0) {
                res[left] = nums[pq.peek()];
            }         
        }
        return res;
    }
}
  • 时间复杂度:O(nlogn) 当数组是单调递增的情况下,所有元素只入队不出队,而优先级队列由于入队时需要排序,所以入队的时间复杂度为 O(logn),因此总的时间复杂度为 O(nlogn)
  • 空间复杂度:O(n)
    在这里插入图片描述
  1. 方法三:单调队列
  • 思路:我们顺着方法二的优先级队列继续优化。

    • 可以考虑这样一种情况,存在下标 ij,它们之间存在这样的关系:nums[i] <= nums[j]i < j。由于窗口是从左到右滑动的,所以下标小的 i 对应的元素先出窗口,而且由于 nums[i] <= nums[j] ,所以那也就意味着只要 nums[i]*nums[j] 都在窗口内,nums[i] 就不可能时最大值。所以当 nums[j] 出现在出口内时,我们可以将 nums[i] 永久地从出口内移除。而当 nums[i] > nums[j] 时,处在后面的 nums[j] 有可能在 nums[i] 出窗口后成为当前窗口内的最大值,所以要保留。所以整体上,我们要构造一个单调递减的队列。
    • 因此我们可以用一个单调队列存储 nums 数组的下标,队列中的下标是从小到大存储的,且这些下标对应的值在 nums 数组中严格单调递减的——在窗口滑动过程中,如果当前下标所对应的值大于队尾下标对应的值,则队尾出队,直至队列为空或者队尾下标对应的值不小于当前下标所对应的值,将当前下标入队。
    • 由于队列中的下标对应的值是严格单调递减的,因此此时队首对应的元素就是滑动窗口的最大值。且与方法二相同的是,此时最大值可能不在滑动窗口内,所以在记录最大值的时候需要先判断此最大值是否还在窗口内。
  • 代码

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 单调队列:队列中下标所对应的值在 nums 数组中严格单调递减
        if (nums == null || nums.length < 1 || k < 1) {
            throw new IllegalArgumentException();
        }
        int length = nums.length;
        int[] res = new int[length - k + 1];
        // 单调队列存储下标值,且下标对应的数值在 nums 中严格单调递减
        Deque<Integer> queue = new LinkedList<>();
        for (int i = 0; i < length; i++) {
            int left = i - k + 1;
            // 如果当前下标对应的值大于队尾下标对应的值,则队尾出队
            while (!queue.isEmpty() && nums[i] > nums[queue.peekLast()]) {
                queue.pollLast();
            }

            queue.offerLast(i);

            // 在记录最大值之前先判断最大值是否还在窗口内
            if (queue.peekFirst() < left) {
                queue.pollFirst();
            }

            // 如果窗口形成
            if (left >= 0) {
                res[left] = nums[queue.peekFirst()];
            }
        }
        return res;
    }
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(k)
  • 这里有个细节,跟优先级队列相比,为什么当单调队列头不符合要求时,只需要用 if 语句弹出一个就行,而优先级队列则需要一直弹出。其实从入队的情况大家应该也能看出,优先级队列是“宽进”的状态,上来直接将数值对应的下标入队,排序是在入队之后才做的。而单调队列则是“严进”的状态,只有当前数字不大于队尾下标所对应的元素或者队列为空时,才能入队,这种“严进” 的模式使得只要是在单调队列中的数,都是有希望成为其窗口内的最大值的,只要压制着它的前一个最大的被弹出队列,那么这个数就是最大的。而优先级队列中有很多不可能成为最大值的数值的下标,只是因为一直没有机会到达队列头的位置,从而一直未被弹出,所以优先级队列有可能需要连续弹出很多个队头。
    在这里插入图片描述

  1. 方法四:单调队列(数组实现)
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        // 单调队列:队列中下标所对应的值在 nums 数组中严格单调递减
        if (nums == null || nums.length < 1 || k < 1) {
            throw new IllegalArgumentException();
        }
        if (k == 1) return nums;
        int length = nums.length;
        int[] res = new int[length - k + 1];
        // 单调队列存储下标值,且下标对应的数值在 nums 中严格单调递减
        int[] queue = new int[length];
        // head 为队列头指针,tail 为队列尾指针
        int head = 0, tail = -1;
        
        for (int i = 0; i < length; i++) {
            // 窗口的左边界
            int left = i - k + 1;
            // 如果当前下标对应的值大于队尾对应的值,则队尾出队
            while (head <= tail && nums[i] > nums[queue[tail]]) {
                tail--;
            }
            queue[++tail] = i;

            // 如果队列头不在窗口内
            if (queue[head] < left) {
                head++;
            }

            // 如果窗口形成,则记录最大值
            if (left >= 0) {
                res[left] = nums[queue[head]];
            }

        }
        return res;
    }
}
  • 时间复杂度: O(n)
  • 空间复杂度:O(n)

在这里插入图片描述


2.2 绝对差不超过限制的最长连续子数组

题目链接:1438. 绝对差不超过限制的最长连续子数组


2.3 带限制的子序列和

题目连接:1425. 带限制的子序列和


参考

  • 5
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Python中,单调栈单调队列是两种不同的数据结构。单调栈是一个栈,它的特点是栈内的元素是单调的,可以是递增或递减的。在构建单调栈时,元素的插入和弹出都是在栈的一端进行的。与此类似,单调队列也是一个队列,它的特点是队列内的元素是单调的,可以是递增或递减的。在构建单调队列时,元素的插入是在队列的一端进行的,而弹出则是选择队列头进行的。 单调队列在解决某些问题时,能够提升效率。例如,滑动窗口最大值问题可以通过使用单调队列来解决。单调队列的结构可以通过以下代码来实现: ```python class MQueue: def __init__(self): self.queue = [] def push(self, value): while self.queue and self.queue[-1 < value: self.queue.pop(-1) self.queue.append(value) def pop(self): if self.queue: return self.queue.pop(0) ``` 上述代码定义了一个名为MQueue的类,它包含一个列表作为队列的存储结构。该类有两个方法,push和pop。push方法用于向队列中插入元素,它会删除队列尾部小于插入元素的所有元素,并将插入元素添加到队列尾部。pop方法用于弹出队列的头部元素。 总结来说,单调栈单调队列都是为了解决特定问题而设计的数据结构。单调栈在构建时元素的插入和弹出都是在栈的一端进行的,而单调队列则是在队列的一端进行的。在Python中,可以通过自定义类来实现单调队列的功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值