经典结构 窗口最大值更新结构

内容:

1、窗口内最大值更新结构

2、窗口移动

3、求达标的子数组个数

1、窗口内最大值更新结构

窗口:数组中的一系列数

  • L与R之间的数就是窗口内的数 
  • L和R的初始位置为数组的左边,可表示为-1
  • L和R都只能右移,不可后退;且L不可超过R

窗口内最大值更新结构实质上就是一个双端队列(双向链表实现),可以从头部放入数据、弹出数据,也可以从尾部放入数据、弹出数据(均是O(1))

双端队列中每个元素由value和index组成,value是值,index是时间戳(数组下标)

双向队列加数策略:R右移,则向双向队列中加数;如果尾部元素比要加入的数小,则弹出尾部的数,一直弹,

直到尾部的数大于新加入的数,或者队列为空。

解释:如果新加入的数据,比之前的数大,那么之前的数就不可能是窗口内最大值,可以舍弃掉。

双向队列减数策略: L右移,则需要处理,如果刚被移出窗口的数是队列头部的数则把队列头部的数弹出,这就是为什么要保存数组下标的原因

窗口内最小值类似:双向队列从头到尾依次递增,加数如果尾部数比要加的数大则弹出,减数也是过期就弹出;

代码:

 1 // 最大值窗口更新结构
 2 public class MaxValueWindow {
 3     
 4     private LinkedList<Integer> queue;
 5     public MaxValueWindow(){
 6         this.queue = new LinkedList<Integer>();
 7     }
 8     
 9     // 更新窗口最大值
10     public void add(int i){
11         while(!queue.isEmpty() && queue.getLast() <= i){
12             queue.pollLast();    
13         }
14         queue.addLast(i);        
15     }
16     
17     // 获取窗口最大值
18     public int getMax(){
19         if(!queue.isEmpty()){
20             return queue.peekFirst();    
21         }
22         return Integer.MIN_VALUE;
23     }
24     
25     // 使窗口最大值过期
26     public void expireMaxValue(){
27         if(!queue.isEmpty()){
28             queue.pollFirst();
29         }
30     }
31     
32     public static void main(String[] args) {
33         MaxValueWindow window = new MaxValueWindow();
34         window.add(6);
35         window.add(4);
36         window.add(9);
37         window.add(8);
38         System.out.println(window.getMax());    // 9
39         window.expireMaxValue();
40         System.out.println(window.getMax());    // 8
41     }
42 }

2、窗口移动

题目描述:

有一个整形数组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]

思路:

前面介绍的窗口大值更新结构的特性是,先前放入的数如果还存在于结构中,那么该数一定比后放入的数都大。

此题窗口移动的过程就是从窗口中减一个数和增一个数的过程

代码:

 1 public class SlidingWindowMaxArray {
 2     public static int[] getMaxWindow(int[] arr, int w) {
 3         if (arr == null || w < 1 || arr.length < w) {
 4             return null;
 5         }
 6         LinkedList<Integer> qmax = new LinkedList<Integer>();
 7         int[] res = new int[arr.length - w + 1];
 8         int index = 0;
 9         for (int i = 0; i < arr.length; i++) {
10             while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[i]) {
11                 qmax.pollLast();
12             }
13             qmax.addLast(i);
14             if (qmax.peekFirst() == i - w) {
15                 qmax.pollFirst();
16             }
17             if (i >= w - 1) {
18                 res[index++] = arr[qmax.peekFirst()];
19             }
20         }
21         return res;
22     }
23     
24     public static void main(String[] args) {
25         int[] arr = {4, 3, 5, 4, 3, 3, 6, 7};
26         System.out.println(Arrays.toString(getMaxWindow(arr, 3)));
27     }
28 }

3、求达标的子数组个数

题目描述:求最大值减去最小值小于或等于num的子数组数量

给定数组arr和整数num,共返回有多少个子数组满足如下情况:

max(arr[i...j]) - min(arr[i..j])  <=  num

max(arr[i...j])表示子数组arr[i...j]中的最大值,min(arr[i...j])表示子数组arr[i...j]中的最小值

要求:实现时间复杂度O(N)的算法

暴力思路:

遍历每个元素,再遍历以当前元素为首的所有子数组,再遍历子数组找到其中的大值和小值以判断其是否达标。

很显然这种方法的时间复杂度为 o(N^3) ,但如果使用窗口最大值更新结构,则能实现 O(N) 级别的解

代码: 

 1     // 暴力方法(O(N^3))
 2     public static int getNum1(int[] arr, int num) {
 3         int res = 0;
 4         // 遍历所有子数组
 5         for (int start = 0; start < arr.length; start++) {
 6             for(int end=start;end<arr.length;end++){
 7                 if(isValid(arr, start, end, num)){
 8                     res++;
 9                 }
10             }
11         }
12         return res;
13     }
14     
15     public static boolean isValid(int[] arr, int start, int end, int num){
16         // 判断子数组最大值和最小值是否小于num
17         int max = Integer.MIN_VALUE;
18         int min = Integer.MAX_VALUE;
19         for(int i=start; i<=end;i++){
20             max = Math.max(max, arr[i]);
21             min = Math.min(min, arr[i]);
22         }
23         return max - min <= num;
24     }

窗口最大值更新结构思路:

基本思想:

  • 前提条件:用 L 和 R 两个指针指向数组的两个下标,且 L 在 R 的左边,下面两条是必然发生的
  • 当 L~R 这一子数组达标时,可以推导出以 L 开头的长度不超过 R-L+1 的所有子数组都达标
  • 当 L~R 这一子数组不达标时,无论 L 向左扩多少个位置或者 R 向 右扩多少个位置, L~R 还是不达标

O(N) 的解对应的算法是:

L 和 R 都从0开始, R 先向右移动, R 每右移一个位置就使用窗口最大值更新结构和窗口最小值更新结构

记录一下 L~R 之间的大值和小值的下标,当 R 移动到如果再右移一个位置 L~R 就不达标了时停 止,这时以当前 L开头

的长度不超过 R-L+1 的子数组都达标;然后 L 右移一个位置,同时更新一下结构( L-1 下标过期),

再右移 R 至 R 如果右移一个位置 L~R 就不达标了停止(每右移 R 一次也更新结构)……;直到

L 到达数组尾元素为止。将每次 R 停止时, R-L+1 的数量累加起来就是 O(N) 的解,因为 L 和 R 都只向右移动,

并且每次 R 停止时,以 L 开头的达标子串的数量直接通过 R-L+1 计算,所以 时间复杂度就是将数组遍历了一遍即 O(N)

代码:

 1     // O(N)的解法
 2     public static int getNum(int[] arr, int num) {
 3         if (arr == null || arr.length == 0) {
 4             return 0;
 5         }
 6         LinkedList<Integer> qmin = new LinkedList<Integer>();
 7         LinkedList<Integer> qmax = new LinkedList<Integer>();
 8         int L = 0;
 9         int R = 0;
10         int res = 0;
11         while (L < arr.length) {
12             while (R < arr.length) {
13                 // R往右扩 扩到不能扩停
14                 while (!qmin.isEmpty() && arr[qmin.peekLast()] >= arr[R]) {
15                     qmin.pollLast();
16                 }
17                 qmin.addLast(R);
18                 while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[R]) {
19                     qmax.pollLast();
20                 }
21                 qmax.addLast(R);
22                 if (arr[qmax.getFirst()] - arr[qmin.getFirst()] > num) {
23                     // 不达标的情况
24                     break;
25                 }
26                 R++;
27             }
28             // 更新最大最小值
29             if (qmin.peekFirst() == L) {
30                 qmin.pollFirst();
31             }
32             if (qmax.peekFirst() == L) {
33                 qmax.pollFirst();
34             }
35             // 加上已统计的结果
36             res += R - L + 1;
37             // L右扩
38             L++;
39         }
40         return res;
41     }    
原文地址:https://www.cnblogs.com/wyb666/p/10276317.html