Sliding Window Maximum

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


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].數據結構

直觀的考慮如下幾個問題:app

  1. 不直接記錄滑動過程當中的最大值,記錄該最大值的下標, 假設該下標爲j,且值爲x;由於在下一次計算中,須要知道該下標是否在當前窗口裏面;而後讀入下一個元素,假設爲y;那麼如今有三種狀況須要考慮:優化

    a. 若是y > x; 那麼y確定是當前窗口的最大值(爲何?);this

    b. 若是y < x, 且j + k >= i (當前座標),那麼x就是當前窗口的最大值;spa

    c. 若是y < x, 且j + k < i; 也就是說上一個最大值已經移出了當前窗口,這個時候就須要在當前窗口裏面計算最大值;code

    那麼根據這個能夠實現如下的代碼, 能夠看到在第三種狀況的時候只是順序查找該窗口裏面的最大值,簡單的分析能夠獲得最壞的狀況下須要O(n * k)的時間;orm


  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), 由於每一個元素進棧一次,且最多出棧一次。在查找當前窗口的最大值時,是常數時間,不會由於棧的長度變化;

相關文章
相關標籤/搜索