【LeetCode】239、滑动窗口最大值(同剑指 Offer 59 - I)

一、题目

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

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

进阶:

你能在线性时间复杂度内解决此题吗?

示例:

输入: 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 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

二、解决

1、暴力破解(超时)

思路:

S1、从 i = 0 i=0 i=0 开始,遍历给定的数组;
S2、对于 i i i 开始及之后 k − 1 k-1 k1 个位置,形成一个滑动窗口;
S3、在这 k k k 个滑动窗口内,取最大值记录下来,放到返回数组内。

代码:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums.length==0 || k<=0) return new int[0];
        int[] res = new int[nums.length-k+1];
        for (int i = 0; i < nums.length+1-k; i++) {
            int max = Integer.MIN_VALUE;
            for (int j = i; j < i+k; j++) {
                max = Math.max(max, nums[j]);
            }
            res[i] = max;
        }
        return res;
    }
}

时间复杂度: O ( n k ) O(nk) O(nk)
空间复杂度: O ( n ) O(n) O(n),ans需要 n − k + 1 n-k+1 nk+1个空间。

2、最大堆法

思路:

S1:从 i=0 开始遍历数组,并将元素放入最大堆 maxHeap 中;
S2:当 i>=k-1 时,取出首元素放入返回数组中;
S3:当 i>=k 时,一边从 maxHeap 删除超过 k 个的元素,一边添加新元素,直至遍历完。

代码:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        if (n ==0 || k<=0) return new int[0];

        int[] res = new int[n-k+1];
        PriorityQueue<int[]> maxHeap = new PriorityQueue<>((a, b)->(b[0]-a[0]));
        for (int i = 0; i < n; i++) {
            maxHeap.add(new int[]{nums[i], i});
            while (i - maxHeap.peek()[1] >= k) {
                maxHeap.poll();
            }
            if (i >= k-1) {
                res[i-k+1] = maxHeap.peek()[0];
            }
        }
        return res;
    }
}

时间复杂度: O ( n l o g k ) O(nlogk) O(nlogk)
空间复杂度: O ( n ) O(n) O(n)

3、二叉搜索树

思路:

这里主要应用近似平衡二叉树–红黑树的数据结构,理解不是很难,重要的还是方法的了解,过程类似 2-最大堆

代码:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums==null || nums.length==0 || k<=0) return new int[0];

        int n = nums.length, j = 0;
        int[] res = new int[n+1-k];
        TreeMap<Integer, Integer> redBlack = new TreeMap<>();
        for (int i = 0; i < n; i++) {
            redBlack.put(nums[i], redBlack.getOrDefault(nums[i],0)+1);   // 基于红黑树--近似平衡,<2倍高度差
            if (i+1 >= k) {
                res[j++] = redBlack.lastKey();
                removeElement(redBlack, nums[i+1-k]);
            }
        }
        return res;
    }

    private void removeElement(TreeMap<Integer, Integer> redBlack, int x) {
        redBlack.put(x, redBlack.getOrDefault(x, 0)-1);
        if (redBlack.get(x) == 0) redBlack.remove(x);   // 删除key及其映射
    }
}

时间复杂度: O ( n l o g k ) O(nlogk) O(nlogk)
空间复杂度: O ( n ) O(n) O(n)

4、双边队列

思路:

过程可以参考6,这里不再赘述。

代码:

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

        // assume nums is not null
        int n = nums.length;
        if (n == 0 || k == 0) {
            return new int[0];
        }
        int[] res = new int[n - k + 1]; // number of windows
        Deque<Integer> win = new ArrayDeque<>(); // stores indices
        
        for (int i = 0; i < n; ++i) {
            // remove indices that are out of bound
            while (win.size() > 0 && i - win.peekFirst() >= k) {
            	win.pollFirst();
            }
            // remove indices whose corresponding values are less than nums[i]
            while (win.size() > 0 && nums[win.peekLast()] < nums[i]) {
            	win.pollLast();
            }
            // add nums[i]
            win.offerLast(i);
            // add to res
            if (i >= k - 1) {
            	res[i - k + 1] = nums[win.peekFirst()];
            }
        }
        return res;
    }
}

附:Deque()方法含义:

offerLast() - 将元素插入双端队列的尾部。 
peekFirst() - 从双端队列的头部检索元素。 
peekLast()  - 从双端队列的尾部检索元素。 
pollFirst() - 从双端队列中删除元素。 
pollLast()  - 从双端队列的尾部删除元素。

时间复杂度: O(n)
空间复杂度: O(k)

5、动态规划

思路:

S1:声明left[n]、right[n]数组,分别记录从左->右 && 右->左 每 k k k 个元素的最大值;
S2:遍历数组,然后比较获得left、right数组中每个元素的值,然后将结果存在left、right数组中;
S3:再从头(i=0)开始进行遍历,在 k k k 个元素滑动窗口中,将 m a x { l e f t [ i + k − 1 ] 、 r i g h t [ i ] } max\{left[i+k-1]、right[i]\} max{left[i+k1]right[i]} 结果记录进output;

  • 稍微解释下为什么是 m a x { l e f t [ i + k − 1 ] 、 r i g h t [ i ] } max\{left[i+k-1]、right[i]\} max{left[i+k1]right[i]}

1)对于任意区间 [ i , i + k − 1 ] [i,i+k-1] [ii+k1],可能是1个块,很容易理解最大值就在边界;
2)也可能跨边界,是2个块,这里区间最大值可能分布偏左边,也可能分布在偏右边,这两种情况下,最大值都会影响到最左边或最右边的最大值,可以想象是每K个元素单向传染,结果还是在边界。

S4:遍历完毕后,返回数组output。
dp239
更具体可以看参考4中的方法三。

代码:

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

        int n = nums.length;
        if (n * k == 0) return new int[0];
        if (k == 1) return nums;

        int[] left = new int[n]; left[0] = nums[0];
        int[] right = new int[n]; right[n-1] = nums[n-1];
        for (int i = 1; i < n; i++) {
            // from left to right
            if (i % k == 0) left[i] = nums[i];  // block_start
            else left[i] = Math.max(left[i - 1], nums[i]);

            // from right to left
            int j = n - i - 1;
            if ((j + 1) % k == 0) right[j] = nums[j];  // block_end
            else right[j] = Math.max(right[j + 1], nums[j]);
        }

        int[] res = new int[n - k + 1];
        for (int i = 0; i < n - k + 1; i++)
            res[i] = Math.max(left[i + k - 1], right[i]);

        return res;
    }
}

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)

三、参考

1、[Java] All Solutions (B-F, PQ, Deque, DP) with Explanation and Complexity Analysis
2、Important to talk about the solution (Brute Force vs Deque Method) in Java
3、[Java] MaxHeap & BST & Decreasing Monotonic Queue Solutions - Clean code
4、滑动窗口最大值
5、Deque示例
6、🎦【视频解析】 双端队列滑动窗口最大值
7、java PriorityQueue 最小、最大堆(正确版本)
8、Java 8 Lambda 表达式
9、java.util.TreeMap.lastKey()方法实例
10、java.util.TreeMap.remove()方法實例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值