窗口结构及练习题

25 篇文章 0 订阅
11 篇文章 0 订阅

窗口的加数与减数

开始之前窗口的左右边界L和R都设为-1

  • 当R向右移动时,窗口中的元素也相应地增加;
  • 当L向右移动时,窗口中的元素也相应地减少。

此外,在任何情况下,R和L都不能回退,L也不能大于R。

窗口内的最大值

由于窗口的L和R是可能随时变动的,因此窗口内的元素是不固定的,即最大值也是不固定的。

你当然可以在每次窗口变动后遍历一下[L, R]来求最大值,但这样时间复杂度就是O(窗口的长度)。

所以要使用一种新的方式来解决上述问题

双端队列结构

对于一组数,每次只在队列尾添加数据,同时要将队尾元素和待添加元素比较,若待添加元素小于队尾元素,直接添加在队列尾部;若待添加元素大于等于队尾元素,那么就将队尾元素出队列,然后再次将新的队尾元素和待添加元素比较。

此外,队列头元素始终是最大的元素。

当移除元素时,L向右移动,此时查看队列头的索引是否小于L,若小于,则将队列头元素出队。

一个例子

有一组数:5 4 1 3 6,那么使用双端队列进行上述算法的流程应该是:

  • 初始L和R都为-1
  • R右移一位,变为0,5准备进队列,此时队列为空,直接进。目前队列状态为:[5]
  • R右移一位,变为1,4准备进队列,此时队列不为空,队尾元素是5,比4大,因此4直接放在队尾。那么队列状态为:[5 4]
  • R右移一位,变为2,1准备进队列,此时队列不为空,队尾元素是4,比1大,因此1直接放在队尾。那么队列状态为:[5 4 1]
  • R右移一位,变为3,3准备进队列,此时队列不为空,队尾元素是1,比3小,因此将1出队,并将3继续与队尾元素4比较,4更大,因此将3放在队尾。目前队列状态为[5 4 3]
  • R右移一位,变为4,6准备进队列,此时队列不为空,队尾元素是3,比6小,因此将3出队,并将6继续与队尾元素4比较,4更小,因此将4也出队,继续将6与5比较,5还是小,因此5也出队,此时队列为空,将6入队。目前队列状态为[6]
  • 若此时L右移,变为1,那么此时队首元素下标为4,大于1,说明下标为1的元素已经出队了,因此队列仍为[6]

问:为什么3进来的时候非要将1出队?6进来的时候[5 4 3]都可以出队?

因为R既然到了3这个元素,那么1就再也不可能是最大值了(L、R只能向右走,不存在先删除3再删除1的情况)。
6也是同理,既然R已经到了6且6比队列中所有的元素更大,说明到目前为止的窗口中没有谁能比6更大了,而且即便L往右走,也不会影响结果。

练习题 1

在这里插入图片描述
上题的输出结果应该是 [5 5 5 4 6 7]

public class MaxWindow {
    public int[] getMaxWindow(int[] arr, int w) {
        if (arr == null || w < 1 || arr.length < w) {
            return null;
        }
        int[] res = new int[arr.length - w + 1];
        int index = 0;

        LinkedList<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < arr.length; i++) {
            while (!linkedList.isEmpty() && arr[linkedList.peekLast()] <= arr[i]) {
                linkedList.pollLast();
            }
            linkedList.addLast(i);

            if (i - w == linkedList.peekFirst()) {
                linkedList.pollFirst();
            }

            if (i + 1 >= w) {
                res[index++] = arr[linkedList.peekFirst()];
            }
        }
        return res;
    }

    public static void main(String[] args) {
        int[] arr = new int[] {4, 3, 5, 4, 3, 3, 6, 7};
        int[] res = new MaxWindow().getMaxWindow(arr, 3);
        System.out.println(Arrays.toString(res));
    }
}

练习题 2

在这里插入图片描述
这题用暴力做当然可以,但时间复杂度太高:

public int Solution(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 boolean isValid(int[] arr, int start, int end, int num) {
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = start; i <= end; i++) {
            max = Math.max(arr[i], max);
            min = Math.min(arr[i], min);
        }
        return (max - min) <= num;
    }

下面看看用窗口怎么做这道题:

首先有如下两个事实:

  • 对于一个数组的子数组[L, R]是满足题目要求的,即里面的最大值-最小值小于等于给定的值,那么这个子数组的所有子数组肯定都满足要求。因为[L, R]的子数组里面数的范围只可能比[L, R]小,因此子数组的最大值只可能小于等于[L, R]的最大值,子数组的最小值只可能大于等于[L, R]的最小值。
  • 若一个子数组已经不满足要求,那么继续扩张也必然不满足要求。因为子数组中的最大值-最小值已经超过给定的值了,那么继续增大子数组范围只会让最大值更大,最小值更小。

基于以上两点,有以下思路:

  • 准备两个双端队列,一个队列头存当前遍历到的最大值,就像最开始介绍的那样;另一个队列存当前遍历到的最小值
  • 先将子数组左边界L设为0,然后移动右边界R,同时向两个队列中放入元素,直到两个队列头元素之差大于给定值时,R停止移动
  • 此时左边界为0,右边界停留在第一个不满足要求的位置,即从左边界到右边界前一个位置的子数组[L, R-1]为满足要求的最长子数组,那么由之前的结论:一个子数组满足要求,那么其所有的子数组肯定也满足要求,因此以左边界开头的所有子数组的数量为(R-1)-L+1,这样就将所有以L开头的数组都求出来了。
    举例:设L为0,R为4,那么[0, 3]为最长满足要求子数组,所以[0],[0, 1],[0, 1, 2],[0, 1, 2, 3]都是满足要求的。
  • 之所以只算以L开头的子数组而不算以[L, R]之间元素开头的子数组,是因为当前R位置不满足要求,这个不满足要求可能是针对L位置说的,例如有数组[2, 6, 7, 8],L为0,规定的上限值是4.也就是说超过4就不满足要求,那么R最后应该停在2位置,因为7-2=5>4,那么以2开头的子数组为[2],[2, 6],而不算以6开头的子数组是因为R仍然可以向右移,到3位置8-6=2仍然满足要求,所以如果这时求以6开头的子数组你只能得到[6],[6,7],而得不到[6, 7, 8]。因此对于每一个L,要看看R最远能扩到哪才能求出所有的子数组。

代码:

public int Solution2(int[] arr, int num) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        LinkedList<Integer> maxQ = new LinkedList<>();
        LinkedList<Integer> minQ = new LinkedList<>();

        int start = 0;
        int end = 0;
        int res = 0;
        while (start < arr.length) {
            while (end < arr.length) {
                while (!maxQ.isEmpty() && arr[maxQ.peekLast()] <= arr[end]) {
                    maxQ.pollLast();
                }
                maxQ.addLast(end);
                while (!minQ.isEmpty() && arr[minQ.peekLast()] >= arr[end]) {
                    minQ.pollLast();
                }
                minQ.addLast(end);
                if (arr[maxQ.peekFirst()] - arr[minQ.peekFirst()] > num) {
                    break;
                }
                end++;
            }

            if (start == maxQ.peekFirst()) {
                maxQ.pollFirst();
            }
            if (start == minQ.peekFirst()) {
                minQ.pollFirst();
            }
            res += end - start;
            start++;
        }
        return res;
    }

    public static void main(String[] args) {
        int[] arr = new int[] {2, 6, 7, 8, 12};
        System.out.println(new MaxMinusMin().Solution2(arr, 4));
    }

结果:
10
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值