LeetCode(239.滑動窗口的最大值

題目:

給定一個數組nums,有一個大小爲k的滑動窗口從數組的最左側移動到最右側,你只能夠看到滑動窗口內的k個數字。滑動窗口每次只向右移動一位。
返回滑動窗口中的最大值。算法

示例數組

輸入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
輸出: [3,3,5,5,6,7] 
解釋: 

  滑動窗口的位置                最大值
---------------               -----
[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

初始思路:剛開始沒想太多,C++中求數組的最大值的函數:max_element能夠解決此問題。果不其然,AC了並且時間複雜度也很高。顯然不是這個題目的正常解法,一番苦思冥想後仍是打開了題解區。臥槽!喲嘚斯內!下面我就能分析下大佬的方法。數據結構

單調隊列解法
一個普通隊列輸入什麼數那位置就是什麼數,而單調隊列在輸入一個數時會與前面的比較大小,刪除部分元素來保證隊列的數是單調遞增或遞減的。框架

這樣在這個題目中,每次向右移動都會增刪一個元素,但咱們要在線性時間找到最大值,咱們就能夠考慮單調隊列。函數

單調隊列一般有這幾個函數:ui

class MonotonicQueue {
// 在隊尾添加元素 n
void push(int n);
// 返回當前隊列中的最大值
int max();
// 隊頭元素若是是 n,刪除它
void pop(int n);
}

而後咱們假設已經有了單調隊列這個數據結構來知足咱們的要求,咱們能夠把此題的解題框架搭出來:code

vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MonotonicQueue window;
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
    if (i < k - 1) { //先把窗口的前 k - 1 填滿
        window.push(nums[i]);
    } else { // 窗口開始向前滑動
        window.push(nums[i]);
        res.push_back(window.max());
        window.pop(nums[i - k + 1]);
        // nums[i - k + 1] 就是窗口最後的元素
    }
}
return res;
}

框架實現仍是很簡單的,如今咱們主要是怎麼實現單調隊列的那些函數:排序

實現單調隊列:

在此以前咱們先認識另外一種數據結構:deque,即雙端隊列:隊列

class deque {
// 在隊頭插入元素 n
void push_front(int n);
// 在隊尾插入元素 n
void push_back(int n);
// 在隊頭刪除元素
void pop_front();
// 在隊尾刪除元素
void pop_back();
// 返回隊頭元素
int front();
// 返回隊尾元素
int back();
}

這些操做的複雜度都是O(1)。咱們能夠利用這個數據結構來實現單調隊列。element

前面咱們已經提到過單調隊列就是隊尾每增一個元素時,與前面的進行比較,把比它小的元素刪掉:

void push(int n) {
    while (!data.empty() && data.back() < n) 
        data.pop_back();
    data.push_back(n);
}

這樣就保證了隊列從頭至尾是單調遞減的。

由此咱們知道當前最大的元素確定是在隊頭,因此max()函數實現:

int max() {
return data.front();
}

最後當窗口向右移動一步時,以前最左端的數須要pop掉,此時隊列裏的數都是單調遞減的。最左端的數有兩種狀況:

1.在push時,由於比push的數小,因此已經被pop
2.若沒被pop,說明他一直比後面push的數大,因此在隊頭位置。

因此pop函數實現:

void pop(int n) {
if (!data.empty() && data.front() == n)
    data.pop_front();
}

自此咱們已將單調隊列構造出來,接下來就能夠代入上面的框架來最後完善咱們的代碼:

class MonotonicQueue {
private:
deque<int> data;
public:
void push(int n) {
    while (!data.empty() && data.back() < n) 
        data.pop_back();
    data.push_back(n);
}

int max() { return data.front(); }

void pop(int n) {
    if (!data.empty() && data.front() == n)
        data.pop_front();
}
};

vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MonotonicQueue window;
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
    if (i < k - 1) { //先填滿窗口的前 k - 1
        window.push(nums[i]);
    } else { // 窗口向前滑動
        window.push(nums[i]);
        res.push_back(window.max());
        window.pop(nums[i - k + 1]);
    }
}
return res;
}

3、複雜度分析:

乍一看push中含有while循環,時間複雜度不是O(1)啊,因此本算法時間複雜度也不是線性時間吧?

單獨看push操做確實不是O(1),可是整個算法的複雜度依然是O(N)。要這樣想,nums中每一個元素最多被push_back和pop_back一次,沒有多餘操做,因此總體複雜度仍是O(N);

空間複雜度就很簡單了,就是窗口的大小O(k)。

4、最後總結:

有些朋友會以爲「單調隊列」和「優先級隊列」比較像,實際上差異很大的。

單調隊列在添加元素的時候靠刪除元素保持隊列的單調性,至關於抽取出某個函數中單調遞增(或遞減)的部分;而優先級隊列(二叉堆)至關於自動排序,差異大了去了。

本文參考了labuladong的解法

相關文章
相關標籤/搜索