Sliding Window Maximum

from https://leetcode.com/problems/sliding-window-maximum/


Given an array nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position.

For example,
Given nums = [1,3,-1,-3,5,3,6,7], and k = 3.

Window position                Max
---------------               -----
[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

Therefore, return the max sliding window as [3,3,5,5,6,7].

直观的考虑以下几个问题:

  1. 不直接记录滑动过程中的最大值,记录该最大值的下标, 假设该下标为j,且值为x;因为在下一次计算中,需要知道该下标是否在当前窗口里面;然后读入下一个元素,假设为y;那么现在有三种情况需要考虑:

    a. 如果y > x; 那么y肯定是当前窗口的最大值(为什么?);

    b. 如果y < x, 且j + k >= i (当前坐标),那么x就是当前窗口的最大值;

    c. 如果y < x, 且j + k < i; 也就是说上一个最大值已经移出了当前窗口,这个时候就需要在当前窗口里面计算最大值;

    那么根据这个可以实现以下的代码, 可以看到在第三种情况的时候只是顺序查找该窗口里面的最大值,简单的分析可以得到最坏的情况下需要O(n * k)的时间;


  2. public int[] maxSlidingWindow1(int[] nums, int k) {
        int n = nums.length;
        if (n == 0) {
            return new int[0];
        }
    
    
        int[] result = new int[n - k + 1];
    
        int index = 0;
        for (int i = 1; i < k; i++) {
            if (nums[result[index]] < nums[i]) {
                result[index] = i;
            }
        }
    
        for (int i = k; i < n; i++) {
            int j = result[index++];
            int a = nums[j];
            int b = nums[i];
            if (b >= a) {
                result[index] = i;
            } else {
                if (i - j < k) {
                    result[index] = j;
                } else {
                    result[index] = j + 1;
                    for (int m = j + 2; m <= i; m++) {
                        if (nums[result[index]] < nums[m]) {
                            result[index] = m;
                        }
                    }
                }
            }
    
        }
    
        for (int i = 0; i < result.length; i++) {
            result[i] = nums[result[i]];
        }
    
        return result;
    }
  3. 当然可以有改进的地方;考虑这样一个输入(部分)5, 3, 1, 2, 1, 窗口大小为3, (结果为5, 3, 2);  然后从1开始模拟上面的算法:

    a. 读入1, 前一个最大值为5,且在窗口里面,所以当前窗口最大值为 5;

    b. 读入2, 最大值5移出了窗口,所以要从#1开始计算最大值,并得到3;

    c. 读入1, 最大值3移出了窗口,所以要从#2开始重新计算,并得到2;

    那么有没有办法可以不用对重复的处理已经被处理的输入呢?答案自然是有。考虑使用一个stack,它保存一个降序(坐标)的数列。处理到某一个位置的时候,因为stack里面处于下面的值(如果存在)肯定比当前值大;那么只要在stack里面找到最下面的,且处于当前窗口内的值,就是当前窗口的最大值;

    代码如下所示:


  4. public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        if (n == 0) {
            return new int[0];
        }
        int[] result = new int[n - k + 1];
        Stack<Integer> stack = new Stack<>(), temp = new Stack<>();
        result[0] = nums[0];
        stack.push(0);
        for (int i = 1; i < k; i++) {
            if (result[0] < nums[i]) {
                result[0] = nums[i];
            }
            int a = nums[i];
    
            while (!stack.isEmpty() && nums[stack.peek()] < a) {
                stack.pop();
            }
            stack.push(i);
        }
        int index = 1;
        for (int i = k; i < n; i++) {
            int a = nums[i];
    
            while (!stack.isEmpty() && nums[stack.peek()] < a) {
                stack.pop();
            }
    
            if (stack.isEmpty()) {
                result[index++] = a;
            } else {
                if (stack.peek() + k <= i) {
                    result[index++] = a;
                } else {
                    while (!stack.empty() && stack.peek() + k > i) {
                        temp.push(stack.pop());
                    }
                    result[index++] = nums[temp.peek()];
                    while (!temp.empty()) {
                        stack.push(temp.pop());
                    }
                }
            }
            stack.push(i);
        }
    
        return result;
    }
  5. 简单分析一下:还是以5, 3, 1, 2, 1为例;

    a. 当处理到1的时候,stack 里面为5, 3 (应该为坐标,为了看得清楚,直接用值替代);并且5在窗口里面,所以当前窗口最大值为5;stack 变为5, 3, 1;

    b. 处理2的时候,比2小的1会被pop掉,stack变为5, 3, 2; 然后3是当前窗口里面最下面的值,所以最大值是3;

    c. 处理最后一个1的时候, stack里面当前窗口最大的为2, 所以最大值是2;前面的算法在这里是需要从1开始处理的,这里只需要处理到2,就可以了。

  6. performance

    但是提交了以后,算法1的时间为500MS,算法2为700MS;sign~~~~

    简单的分析一下,算法1只使用了数组,而算法2使用了stack;而且算法2使用了两个stack,用于查找当前窗口里面最大值,事实上如果使用自己定义个数据结构,是可以只用一个;应该会有提升;算法2的时间复杂度,假设使用一个stack,那么每个元素被入栈一次,最多出栈一次,最坏情况比如一个降序的序列,那么不幸的是,时间复杂度任然是O(n * k); 

  7. 还有一个优化是,stack只保留上一个窗口的最大值和当前窗口的降序序列,查找当前窗口最大的元素可以在常数时间内完成,可以达到O(n)的复杂度;

  8. 改进后的用自定义的数据结构,且只保存当且窗口内的降序序列:


  9. class Lst<T> {
        public final int size;
        private final Object[] array;
        private int head, tail;
    
        Lst(int size) {
            this.size = size;
            this.array = new Object[size];
            this.head = 0;
            this.tail = -1;
        }
    
        public T first() {
            return (T) array[head];
        }
    
        public T popFirst() {
            return (T) array[head++];
        }
    
        public void append(T t) {
            array[++tail] = t;
        }
    
        public T last() {
            return (T) array[tail];
        }
    
        public T popLast() {
            return (T) array[tail--];
        }
    
        public boolean isEmpty() {
            return head > tail;
        }
    }
    
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        if (n == 0) {
            return new int[0];
        }
    
        Lst<Integer> lst = new Lst<>(nums.length + 1);
    
        int[] result = new int[n - k + 1];
        int index = 0;
    
        for (int i = 0; i < k; i++) {
            if (lst.isEmpty()) {
                lst.append(i);
                continue;
            }
    
            int a = nums[i];
            while (!lst.isEmpty() && nums[lst.last()] < a) {
                lst.popLast();
            }
            lst.append(i);
        }
        result[index++] = nums[lst.first()];
    
        for (int i = k; i < n; i++) {
    
            while (!lst.isEmpty() && lst.first() + k <= i) {
                lst.popFirst();
            }
            int a = nums[i];
            while (!lst.isEmpty() && nums[lst.last()] < a) {
                lst.popLast();
            }
            lst.append(i);
    
            result[index++] = nums[lst.first()];
        }
        return result;
    }

    提交以后用时也在500MS。这个算法的时间复杂度应该是O(n), 因为每个元素进栈一次,且最多出栈一次。在查找当前窗口的最大值时,是常数时间,不会因为栈的长度变化;



你可能感兴趣的:(java,LeetCode,算法)