給定一個數組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; }
乍一看push中含有while循環,時間複雜度不是O(1)啊,因此本算法時間複雜度也不是線性時間吧?
單獨看push操做確實不是O(1),可是整個算法的複雜度依然是O(N)。要這樣想,nums中每一個元素最多被push_back和pop_back一次,沒有多餘操做,因此總體複雜度仍是O(N);
空間複雜度就很簡單了,就是窗口的大小O(k)。
有些朋友會以爲「單調隊列」和「優先級隊列」比較像,實際上差異很大的。
單調隊列在添加元素的時候靠刪除元素保持隊列的單調性,至關於抽取出某個函數中單調遞增(或遞減)的部分;而優先級隊列(二叉堆)至關於自動排序,差異大了去了。
本文參考了labuladong的解法