当前位置:   article > 正文

leetcode239-滑动窗口最大值(困难)_滑动窗口的最大值 坑点在哪

滑动窗口的最大值 坑点在哪


力扣链接
给你一个整数数组 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

示例 2:

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

示例 3:

输入:nums = [1,-1], k = 1
输出:[1,-1]
  • 1
  • 2

示例 4:

输入:nums = [9,11], k = 2
输出:[11]
  • 1
  • 2

示例 5:

输入:nums = [4,-2], k = 2
输出:[4]
  • 1
  • 2

提示:

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

我的解题思路

总共会有(n - k + 1)个滑动窗口,每个滑动窗口的每个元素都遍历一次,比较出最大值,放到新的数组中,
这应该是正常人能想到的方法了,但是这样做的时间复杂度为O(k(n-k+1),即O(kn),这个时间复杂度是无法通过此题的,我看了此题的测试用例,数组长度有几十万乃至上百万个元素,只有参考官方题解的更好的解法了。

代码

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        //数组长度
        int size = nums.length;
        //定义最大值数组
        int[] max = new int[size - k + 1];
        //指向左边界
        int i = 0;
        //指向右边界
        int j = k - 1;
        while (j < size) {
            max[i] = nums[i];
            for (int h = 1; h < k; h++) {
                max[i] = Math.max(max[i], nums[i + h]);
            }
            i++;
            j++;
        }
        return max;
    }
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

时间复杂度

O(kn)

官方解题思路-优先队列(PriorityQueue)

对于「最大值」,我们可以想到一种非常合适的数据结构,那就是优先队列(堆),其中的大根堆可以帮助我们实时维护一系列元素中的最大值。

对于本题而言,初始时,我们将数组nums 的前 kk 个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组nums 中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除。

我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组 (num,index),表示元素 num 在数组中的下标为 index。

代码

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        //数组的长度
        int size = nums.length;
        //声明一个优先队列,定义比较器,使该队列为大根堆(根节点最大)(即除了叶子结点外,每个节点都大于他的两个子节点)
        //堆是完全二叉树,其可以用数组来表示。
        //Comparator接口中重写方法时,我的理解是返回值时后面的减前面的就是降序,否则是升序。
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
            @Override
            public int compare(int[] a, int[] b) {
            return a[0] != b[0] ? b[0] - a[0] : b[1] - a[1];
            }
        });
        //max数组
        int[] max = new int[size - k + 1];
        //用来指向遍历的位置
        int i = 0;
        for (; i < k; i++) {
            //添加一个数组元素(数组0索引表示元素值,1索引表示元素在数组中的下标)(1索引用来判断当前根节点是否超出的滑动窗口的范围)
            pq.offer(new int[]{nums[i], i});
        }
        //得到第一个滑动窗口内元素的最大值
        max[0] = pq.peek()[0];
        for (; i < size; i++) {
            pq.offer(new int[]{nums[i], i});
            while (pq.peek()[1] < i - k + 1) {
                //当根节点(存储最大值)的元素超出滑动窗口的返回,就删掉此节点
                pq.poll();
            }
            //得到当前滑动窗口的最大值
            max[i - k + 1] = pq.peek()[0];
        }
        return max;
    }
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

在这里插入图片描述

时间复杂度

  • 时间复杂度:O(nlogn),其中 n 是数组 nums 的长度。在最坏情况下,数组 nums 中的元素单调递增,那么最终优先队列中包含了所有元素,没有元素被移除。由于将一个元素放入优先队列的时间复杂度为 O(logn),因此总时间复杂度为 O(nlogn)。

官方解题思路-单调队列

我们可以顺着方法一的思路继续进行优化。

由于我们需要求出的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标 i 和 j,其中 i 在 j 的左侧(i<j),并且 i 对应的元素不大于 j 对应的元素(nums[i]≤nums[j]),那么会发生什么呢?

当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中,这是 i 在 j 的左侧所保证的。因此,由于nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将 nums[i] 永久地移除。

因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组 nums 中对应的值是严格单调递减的。因为如果队列中有两个相邻的下标,它们对应的值相等或者递增,那么令前者为 i,后者为 j,就对应了上面所说的情况,即 nums[i] 会被移除,这就产生了矛盾。

当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果前者大于等于后者,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。

由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。但与方法一中相同的是,此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。

为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」。

代码

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        //数组的长度
        int size = nums.length;
        //使用双端队列来维护一个单调递增的元素索引(索引递减,对应的元素值递增)
        Deque<Integer> deque = new LinkedList<>();
        //存储滑动窗口最大值
        int[] max = new int[size - k + 1];
        //当前遍历元素所在索引
        int i = 0;
        for (; i < k; i++) {
            //peekLast(): 查看队列最后一个元素
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                //poolLast(): 移除队列的最后一个元素
                //当队列不为空,且左边元素小于右边时,此元素没用了,直接移除
                deque.pollLast();
            }
            //offerLast()&offer():将元素添加到队列尾部
            //将当前元素的索引放到队列尾部中
            deque.offerLast(i);
        }
        //此时是第一个滑动窗口,最头部元素所指向的索引值是当前窗口中(从第二个开始就不一定了)最大的
        max[0] = nums[deque.peekFirst()];

        for (; i < size; i++) {
             while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                 deque.pollLast();
             }
             deque.offerLast(i);
             //如果当前的头部元素已经不在当前窗口,移除该头部元素
             if (deque.peekFirst() < i - k + 1) {
                 deque.pollFirst();
             }
             max[i - k + 1] = nums[deque.peekFirst()];
        }
        return max;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

在这里插入图片描述

时间复杂度

时间复杂度:O(n),其中 n 是数组nums 的长度。每一个下标恰好被放入队列一次,并且最多被弹出队列一次,因此时间复杂度为 O(n)。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/458865
推荐阅读
相关标签
  

闽ICP备14008679号