滑动窗口的最大值与最小值更新结构

本文探讨了如何使用单调队列数据结构解决滑动窗口问题,包括固定大小窗口下求最大值的应用实例,以及可变大小窗口中满足特定条件子数组的数量计算。通过双端队列操作,实现在O(N)时间内高效找到窗口内的最大值和满足条件的子数组数量。
摘要由CSDN通过智能技术生成

滑动窗口的最大值与最小值更新结构

数据结构–单调队列

特点:队列中的元素全都是单调递增(或递减)的。

应用:解决滑动窗口问题。

实现:使用双端队列来实现单调队列的语义,在双端队列的头部和尾部都可以插入和删除元素,通过下面的插入和删除元素的逻辑来实现单调队列的语义。(注:在Java中双端队列Deque接口有两个主要的实现类:ArrayDeque和LinkedList,我们这里使用LinkedList实现类。)

  • 插入数据:在队尾插入一个元素,并把该元素前面比它小或者等于它的元素都删除掉。(为什么要删除掉前面小的元素?可以这样理解,当前插入的这个元素比前面小的元素值更大,并且还更晚过期,过期的含义是指从窗口中移出了,那选出最大值的时候肯定不可能是前面小的元素)
  • 获取最大值:队列头部就是当前窗口的最大值。
  • 删除数据:在窗口向右滑动的过程中,如果发现队列头部的数据过期了,则将其删除。

注意:双端队列中保存的是数组元素的值和位置,不能只保存数组元素的值,因为在删除数据的时候没法判断队列头部数据在数组的位置,也就没法判断数据是否过期。通过数组的索引可以在O(1)时间内定位到该位置的元素,所以在双端队列中只需要保存数组索引即可,在两个位置的元素比较大小的时候根据索引做一次映射即可比较出大小关系。

通过一个例子来了解单调队列的更新过程:

应用一:对于固定大小的窗口

题目:有一个整型数组arr和一个大小为w的窗口从数组的最左边滑到最右边,窗口每次向右边滑一个位置。例如,数组为[4,3,5,4,3,3,6,7],窗口大小为3时:

[4 3 5] 4 3 3 6 7      窗口中最大值为5
4 [3 5 4] 3 3 6 7      窗口中最大值为5
4 3 [5 4 3] 3 6 7      窗口中最大值为5
4 3 5 [4 3 3] 6 7      窗口中最大值为4
4 3 5 4 [3 3 6] 7      窗口中最大值为6
4 3 5 4 3 [3 6 7]      窗口中最大值为7

如果数组长度为n,窗口大小为w,则一共产生n-w+1个窗口的最大值。请实现一个函数。输入:整型数组arr,窗口大小为w。输出;一个长度为n-w+1的数组res,res[i]表示每一种窗口状态下的最大值。上面的例子,结果应该返回[5,5,5,4,6,7]。(类似题目:力扣239. 滑动窗口最大值

思路:使用单调队列这种数据结构,在每个窗口中都可以在O(1)时间内得到最大值。对于数组中的每个元素,最多只会进队列一次,出队列一次。所以整体的时间复杂度是O(N)。

代码如下:

import java.util.Deque;
import java.util.LinkedList;

public class SlidingWindow {
    /**
     * 从左往右输出窗口大小为w的滑动窗口的最大值组成的数组
     * @param arr 原始数组
     * @param w 窗口大小
     * @return 滑动窗口的最大值数组
     */
    public static int[] maxValuesOfWSize(int[] arr, int w) {
        if (w < 1 || w > arr.length) {
            throw new IllegalArgumentException("w is illegal");
        }
        int[] res = new int[arr.length - w + 1]; // 滑动窗口最大值数组
        Deque<Integer> deque = new LinkedList<>(); // 注:双端队列里面存储的是索引
        for (int i = 0; i < arr.length; i++) {
            // 1、往队列中添加、删除数据
            while (!deque.isEmpty() && arr[i] >= arr[deque.peekLast()]) {
                deque.pollLast();
            }
            deque.offerLast(i);
            // i-w表示被当前窗口移除出去的位置,如果下面条件成立,表示队列头部的值已经过期需要移除
            if (i - w == deque.peekFirst()) {
                deque.pollFirst();
            }

            // 2、可以构成窗口的时候,往最大值数组中写入值
            if (i >= w-1) {
                res[i-w+1] = arr[deque.peekFirst()];
            }
        }
        return res;
    }
}

应用二:对于可变大小的窗口

给定数组arr和整数num,返回共有多少个子数组满足如下情况: m a x ( a r r [ i , j ] ) − m i n ( a r r [ i , j ] ) ⩽ n u m max(arr[i,j])-min(arr[i,j])\leqslant num max(arr[i,j])min(arr[i,j])num,其中 m a x ( a r r [ i , j ] ) max(arr[i,j]) max(arr[i,j])表示子数组 a r r [ i , j ] arr[i, j] arr[i,j]中的最大值, m i n ( a r r [ i , j ] ) min(arr[i,j]) min(arr[i,j])表示子数组 a r r [ i , j ] arr[i, j] arr[i,j]中的最小值。要求:如果数组长度为N,请实现时间复杂度为O(N)的解法。

解法一:暴力解法。
思路:通过遍历到方式得到每个子数组,然后再判断子数组的最大值和最小值是否符合要求。
时间复杂度:O(n3)

代码如下:

public class Solution {
    public static int getSubArrayNum(int[] arr, int num) {
        int res = 0;
        for (int i = 0; i < arr.length; i++) {
            for (int j = i; j < arr.length; j++) {
                if (isValid(arr, i, j, num)) {
                    res++;
                }
            }
        }
        return res;
    }

    private static boolean isValid(int[] arr, int left, int right, int num) {
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = left; i <= right; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
            if (arr[i] < min) {
                min = arr[i];
            }
        }
        return max - min <= num;
    }
}

解法二:借助单调队列。
思路:使用一个最大值队列和一个最小值队列,可以在O(1)时间内得到窗口内的最大值和最小值,但是遍历出所有的子数组,依然需要O(n2)的时间复杂度。所以整体的时间复杂度就是O(n2)。

解法三:单调队列+最大值最小值的更新性质。
性质一:对于一个子数组arr[left, right],如果这个子数组满足题目要求,即 m a x ( a r r [ l e f t , r i g h t ] ) − m i n ( a r r [ l e f t , r i g h t ] ) ⩽ n u m max(arr[left, right])-min(arr[left, right])\leqslant num max(arr[left,right])min(arr[left,right])num那么这个子数组的任意一个子数组也是满足题目要求的,即 m a x ( a r r [ i , j ] ) − m i n ( a r r [ i , j ] ) ⩽ n u m max(arr[i,j])-min(arr[i,j])\leqslant num max(arr[i,j])min(arr[i,j])num其中 i ⩾ l e f t , j ⩽ r i g h t , i ⩽ j i\geqslant left, j\leqslant right, i\leqslant j ileft,jright,ij

解释:令 m a x = m a x ( a r r [ l e f t , r i g h t ] ) , m i n = m i n ( a r r [ l e f t , r i g h t ] ) max=max(arr[left, right]), min=min(arr[left, right]) max=max(arr[left,right]),min=min(arr[left,right]),并且 m a x ′ = m a x ( a r r [ i , j ] ) , m i n ′ = m i n ( a r r [ i , j ] ) {max}'=max(arr[i, j]), {min}'=min(arr[i, j]) max=max(arr[i,j]),min=min(arr[i,j]),那么因为有 m a x ′ ⩽ m a x , m i n ′ ⩾ m i n {max}'\leqslant max, {min}'\geqslant min maxmax,minmin,所以 m a x ′ − m i n ′ ⩽ n u m {max}'-{min}' \leqslant num maxminnum肯定也成立。

性质二:对于一个子数组arr[left, right],如果这个子数组不满足题目要求,即 m a x ( a r r [ l e f t , r i g h t ] ) − m i n ( a r r [ l e f t , r i g h t ] ) > n u m max(arr[left, right])-min(arr[left, right]) > num max(arr[left,right])min(arr[left,right])>num那么right位置向右扩展一个位置形成的新子数组任然是不满足题目要求的。

解释:假设扩展之后的子数组的最大值为 m a x ′ {max}' max,最小值为 m i n ′ {min}' min,那么扩展了一个元素之后,就可能会使新的最大值比原来的最大值大,新的最小值比原来的最小值小,即 m a x ′ ⩾ m a x , m i n ′ ⩽ m i n {max}'\geqslant max, {min}'\leqslant min maxmax,minmin,那么新的子数组肯定还是不满足题目要求的。

思路:根据以上性质,就可以用两个指针leftright,先确定一个满足题目要求的范围,那么这个范围内所有的子数组都不用再判断了,因为都是满足题目要求的,所以就可以直接把这个大的范围内所有的子数组数量加到结果集中就可以了。

对于数组中的每个元素,都只会进队列(最大值队列和最小值队列)一次,出队列一次,所以时间复杂度为O(n)。

代码如下:

import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Random;

public class SlidingWindow {
    /**
     * 最大值减去最小值小于等于num的子数组数量
     * @param arr
     * @param num
     * @return
     */
    public static int getSubArrayNum(int[] arr, int num) {
        Deque<Integer> max = new LinkedList<>();
        Deque<Integer> min = new LinkedList<>();
        int left = 0, right = 0;
        int res = 0;
        while (left < arr.length) {
            // 此时固定left,向右推进right
            while (right < arr.length) {
                while (!max.isEmpty() && arr[right] >= arr[max.peekLast()]) {
                    max.pollLast();
                }
                max.offerLast(right);
                while (!min.isEmpty() && arr[right] <= arr[min.peekLast()]) {
                    min.pollLast();
                }
                min.offerLast(right);
                if (arr[max.peekFirst()] - arr[min.peekFirst()] > num) {
                    break;
                }
                right++;
            }
            // left这个位置即将被窗口移除出去,所以判断是否过期
            if (left == max.peekFirst()) {
                max.pollFirst();
            }
            if (left == min.peekFirst()) {
                min.pollFirst();
            }
            // right-left是以left位置元素开头的符合条件的子数组数量
            res += right - left;
            left++;
        }
        return res;
    }
    
    // 暴力解法,用于验证上述解法的正确性
    public static int getSubArrayNumViolence(int[] arr, int num) {
        int res = 0;
        for (int i = 0; i < arr.length; i++) {
            for (int j = i; j < arr.length; j++) {
                if (isValid(arr, i, j, num)) {
                    res++;
                }
            }
        }
        return res;
    }

    private static boolean isValid(int[] arr, int left, int right, int num) {
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = left; i <= right; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
            if (arr[i] < min) {
                min = arr[i];
            }
        }
        return max - min <= num;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Random random = new Random(System.currentTimeMillis());
            int[] arr = new int[10];
            for (int j = 0; j < 10; j++) {
                arr[j] = random.nextInt(10);
            }
            int num = 5;
            if (getSubArrayNum(arr, num) != getSubArrayNumViolence(arr, num)) {
                System.out.println("ERROR");
            }
        }
        System.out.println("Success");
    }
}

参考资料:
左神算法视频;
labuladong的文章:「单调队列」数据结构解决滑动窗口问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值