讀完本文,你能夠去力扣拿下以下題目:java
-----------數據結構
前文講了一種特殊的數據結構「單調棧」monotonic stack,解決了一類問題「Next Greater Number」,本文寫一個相似的數據結構「單調隊列」。框架
也許這種數據結構的名字你沒聽過,其實沒啥難的,就是一個「隊列」,只是使用了一點巧妙的方法,使得隊列中的元素單調遞增(或遞減)。這個數據結構有什麼用?能夠解決滑動窗口的一系列問題。函數
看一道 LeetCode 題目,難度 hard:spa
這道題不復雜,難點在於如何在 O(1) 時間算出每一個「窗口」中的最大值,使得整個算法在線性時間完成。在以前咱們探討過相似的場景,獲得一個結論:設計
在一堆數字中,已知最值,若是給這堆數添加一個數,那麼比較一下就能夠很快算出最值;但若是減小一個數,就不必定能很快獲得最值了,而要遍歷全部數從新找最值。code
回到這道題的場景,每一個窗口前進的時候,要添加一個數同時減小一個數,因此想在 O(1) 的時間得出新的最值,就須要「單調隊列」這種特殊的數據結構來輔助了。排序
一個普通的隊列必定有這兩個操做:隊列
class Queue { void push(int n); // 或 enqueue,在隊尾加入元素 n void pop(); // 或 dequeue,刪除隊頭元素 }
一個「單調隊列」的操做也差很少:
class MonotonicQueue { // 在隊尾添加元素 n void push(int n); // 返回當前隊列中的最大值 int max(); // 隊頭元素若是是 n,刪除它 void pop(int n); }
固然,這幾個 API 的實現方法確定跟通常的 Queue 不同,不過咱們暫且無論,並且認爲這幾個操做的時間複雜度都是 O(1),先把這道「滑動窗口」問題的解答框架搭出來:
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)。這其實不是啥稀奇的數據結構,用鏈表做爲底層結構的話,很容易實現這些功能。
「單調隊列」的核心思路和「單調棧」相似。單調隊列的 push 方法依然在隊尾添加元素,可是要把前面比新元素小的元素都刪掉:
class MonotonicQueue { private: deque<int> data; public: void push(int n) { while (!data.empty() && data.back() < n) data.pop_back(); data.push_back(n); } };
你能夠想象,加入數字的大小表明人的體重,把前面體重不足的都壓扁了,直到遇到更大的量級才停住。
若是每一個元素被加入時都這樣操做,最終單調隊列中的元素大小就會保持一個單調遞減的順序,所以咱們的 max() API 能夠能夠這樣寫:
int max() { return data.front(); }
pop() API 在隊頭刪除元素 n,也很好寫:
void pop(int n) { if (!data.empty() && data.front() == n) data.pop_front(); }
之因此要判斷 data.front() == n
,是由於咱們想刪除的隊頭元素 n 可能已經被「壓扁」了,這時候就不用刪除了:
至此,單調隊列設計完畢,看下完整的解題代碼:
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、最後總結
有的讀者可能以爲「單調隊列」和「優先級隊列」比較像,實際上差異很大的。
單調隊列在添加元素的時候靠刪除元素保持隊列的單調性,至關於抽取出某個函數中單調遞增(或遞減)的部分;而優先級隊列(二叉堆)至關於自動排序,差異大了去了。
趕忙去拿下 LeetCode 第 239 道題吧~