剑指offer:JZ64 滑动窗口的最大值

描述

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。

例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个:{[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}

窗口大于数组长度的时候,返回空

示例

input:
[2,3,4,2,6,2,5,1],3
output:
[4,4,6,6,6,5]

思路

1、暴力求解:逐窗口遍历

该方法比较容易想到,将每一个滑动窗口遍历,并且依次找到最大值即可。假设滑动窗口大小为m、数组长度为n,那么时间、空间复杂度分别为:O(mn)O(n)

import java.util.*;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size) {
        ArrayList<Integer> arr = new ArrayList<>();
        if(num.length == 0 || size > num.length || size <= 0)
            return arr;
        for(int i=0; i<num.length+1-size; i++)
            arr.add(getMax(num, i, i+size-1));
        return arr;
    }
    
    public int getMax(int[] num, int left, int right){
        int max = num[left++];
        while(left <= right){
            if(max < num[left]) max = num[left];
            left++;
        }
        return max;
    }
}

在这里插入图片描述

2、改进:最大堆

考虑构建一个大小为size的最大堆,每次从堆顶取最大值,滑动一次则需要判断堆顶元素是否包含在窗口中。
那么每次插入元素时时间复杂度为 O ( l o g 2 ( s i z e ) ) O(log_2(size)) O(log2(size))、最外层循环了n-size次;而代码中只定义了一个ArrayList(内置数组来存储元素,默认的初始大小为10,每次以1.5倍扩容)和PriorityQueue(内置Object数组组成的二叉树来存储元素,默认初始大小为11,每次以1.5倍扩容.
因此,时间、空间复杂度分别为为 O ( n l o g 2 ( s i z e ) ) O(nlog_2(size)) O(nlog2(size))O(n)

import java.util.*;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size) {
        ArrayList<Integer> arr = new ArrayList();
        if(num == null || num.length == 0 || size <= 0 || num.length < size)
            return arr;
        //构建大顶堆,堆顶存储的下标对应的数组元素最大
        PriorityQueue<Integer> max_que = new PriorityQueue<>(new Comparator<Integer>(){
            @Override
            public int compare(Integer o1, Integer o2){
                return num[o2] - num[o1];
            }
        });
        //先让最大堆加入第一次滑动窗口大小的元素
        for(int i = 0; i < size; i++)
            max_que.offer(i);
        //对后续的元素进行筛选
        for(int i=size; i<num.length; i++){
            int top = max_que.peek();//获取堆顶元素,即当前窗口最大值的下标
            arr.add(num[top]);//当前窗口最大值加入结果list中
            //堆中最大值下标(即最左边)与待加元素下标之差大于滑动窗口长度:表示最大值失效;
            //由于循环中需要先poll再peek,因此保证堆中元素个数大于1
            if(top + size <= i && max_que.size() > 1){
                max_que.poll();
                top = max_que.peek();
            }
            max_que.offer(i);//将当前元素的下标加入最大堆中
        }
        //由于循环中先调整再加元素,因此需要加入最后一个滑动窗口的最大值
        arr.add(num[max_que.poll()]);
        return arr;
    }
}

3、线性时间复杂度的解法

由于暴力求解的时间复杂度较高,因此自然而然想到能否将时间复杂度降低至O(n)。在常见的数据结构中,有下列几种解法:

——双端(单调)队列

这个方法主要需要保证:每一次的移动后,队列中的元素从左到右(从前到后)是严格递减的。主要思路:
(1)若当前元素比左侧元素大时,则去掉前面比它小的元素,直到遇到前面比它大的元素或者队列为空
(2)将当前元素加入到队列尾部(即后边)
(3)如果队列最后的元素下标和最前的元素下标超出滑动窗口长度,那么需要移除最前面的元素
(4)确保队列中最前面的元素下标比滑动窗口大,才会有最大值
由于本题中给定的是数组,可以通过下标直接取值,因此队列只需要存储数组的下标就可以了。
以本题中的{2,3,4,2,6,2,5,1}为例

滑动次数滑动窗口队列中的元素最大值对应的下标说明
122滑动窗口长度不够,无最大值
22,33滑动窗口长度不够,无最大值;当前元素3比队列中的2大,因此舍弃前面的元素
32,3,4442当前元素4比队列中的3大,因此舍弃前面的元素
43,4,24,242当前元素2比队列中的4小,直接加入队列尾部
54,2,6664当前元素6比队列中的(4,2)大,因此舍弃前面的元素
62,6,26,264当前元素2比队列中6小,直接加入队列尾部
76,2,56,564当前元素5处于队列(6,2)的中间,因此去掉前面的2,然后加入队列尾部
82,5,15,156当前元素1比队列中的(6,5)小,加入队列尾部,此时队列(6,5,1);但是此处队首元素6下标为4,队尾元素1下标为7,差值7-4+1>3,需去掉队首!
import java.util.*;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size) {
        ArrayList<Integer> arr = new ArrayList();
        if(num == null || num.length == 0 || size <= 0 || num.length < size)
            return arr;
        Deque<Integer> deque = new LinkedList();
        for(int i=0; i<num.length; i++){
            //当前值插入队列尾部前,先从右到左删除尾部小的元素
            while(!deque.isEmpty() && num[deque.peekLast()] <= num[i])
                deque.pollLast();
            deque.offerLast(i);//当前值的下标加入到队列尾部
            //若当前值加入后,队列最右边元素和最左边元素超过窗口长度,则左边出队列
            if(deque.peekFirst() + size == i)
                deque.pollFirst();
            //当前元素的下标大于或等于窗口长度时,才会有最大值
            if(i >= size-1) arr.add(num[deque.peekFirst()]);
        }
        return arr;
    }
}

在这里插入图片描述

——动态规划

思路见:动态规划 滑动窗口最大值
该方法的思路是以滑动窗口的大小m将数组划为n/m块,left数组将记录每一个块中当前元素与其左侧元素的最大值,即每一个块内保持从左到右的递增right数组则记录每一个块中当前元素与其右侧元素的最大值,即每一个块内保持从右到左的递增。以数组{2,3,4,2,6,2,5,1}为例
(1)由于m = 3,因此数组将被分为3块[2, 3, 4], [2, 6, 2], [5, 1]
(2)left数组取值为:[2, 3, 4], [2, 6, 6], [5, 5]
(3)right数组取值为:[4, 4, 4], [6, 6, 2], [5, 1]
滑动窗口可能在一个块内,也可能在不同块里面,那么
(1)滑动窗口在一个块内时,如[0, m-1],由于left数组块内最右侧值最大,right数组块内左侧值最大,因此,滑动窗口的最大值为left[m-1]或者right[0]
(2)滑动窗口落在两个块中时,如[k, m+k-1] = [k, m-1] + [m, m+k-1] (k>0)。由于left数组块内最右侧值最大,right数组块内左侧值最大,因此[k, m-1]的最大值可以取right[k][m, m+k-1]的最大值可以取left[m+k-1],因此滑动窗口的最大值为max{ right[k], left[m+k-1] }
(3)综上述,可知滑动窗口为[i, j]时,最大值为max{ left[j], right[i] }

import java.util.*;
import java.util.Comparator;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size) {
        ArrayList<Integer> arr = new ArrayList();
        if(num == null || num.length == 0 || size <= 0 || num.length < size)
            return arr;
        
        int n = num.length, j;
        int[] left = new int[n];
        int[] right = new int[n];
        
        for(int i=0; i<n; i++){
            //i表示left数组的索引,从左到右,块内递增
            if(i%size == 0) left[i] = num[i];
            else left[i] = Math.max(left[i-1], num[i]);
            //j表示right数组的索引,从右到左,块内递增
            j = n - i - 1;
            if(j%size == 0 || j == n-1) right[j] = num[j];
            else right[j] = Math.max(right[j+1],num[j]);
        }
        for(int i=0; i<=n-size; i++){
            arr.add(Math.max(left[i+size-1], right[i]));
        }
        return arr;
    }
}

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值