單調棧、單調隊列:刷題必備系列
個人認爲,僅僅以刷題數量作爲算法掌握程度的標準是不準確的,甚至是偏激的。衡量刷題效果的其實是兩點,一是真正掌握的算法技巧數量,二是語言的熟練應用的實戰能力(搞個輸入輸出搞半天,肯定是不行的)。掌握了一種解題技巧,其實對一系列的題都是手到擒來。本文介紹的單調棧、單調隊列就是兩種不可或缺的刷題技巧
單調棧
單調棧,概念其實很簡單,一個棧,裏面的元素的大小按照他們所在棧內的位置,滿足一定的單調性(遞增或者遞減)。
單調棧有一個重要的性質:**可以找到左邊第一個比當前元素大的元素或者左邊第一個比當前元素小的元素。**文字蒼白無力,下面兩個舉個例子
對於數組
-
遞減棧,可以找到左邊第一個比當前元素大的元素
開始棧空、將5、3、1依次入棧,當入4的時候,發生衝突,棧頂元素小於當前元素,依次出棧,直到無衝突發生,才入棧。出棧時,出棧元素的左邊第一個比其大的元素爲出棧後的棧頂元素。即出棧1時,1的左邊第一個比其大的元素一定是出棧後的棧頂元素3
-
遞減棧,可以找到左邊第一個比當前元素小的元素
開始棧空,5入棧,3入棧,發生衝突,棧頂元素大於當前元素,依次出棧,直到無衝突發生,才入棧,出棧時,出棧元素的左邊第一個比其小的元素爲出棧後的棧頂元素。即4出棧時,4的左邊比其小的元素爲出站後的棧頂元素1。
強調兩點
- 數組從左向右遍歷或者從右向左遍歷,單調棧均有相應的性質
- 所有元素僅僅會出棧一次和入棧一次,所以時間複雜度爲O(n)
大顯身手
熟悉了基本性質,下面就是我們大顯身手的時候了。
每日溫度
import java.util.Stack;
class Solution {
public int[] dailyTemperatures(int[] T) {
//找右邊第一個比其大的元素
//從右往左遍歷,構造遞減棧
if(T == null || T.length == 0) return new int[0];
int n = T.length;
Stack<Integer> stack = new Stack<>();
int []res = new int[n];
for(int i = n-1; i >= 0; i--) {
while(!stack.empty() && T[i] >= T[stack.peek()]) {
int popIndex = stack.pop();
if(stack.empty()) continue;
res[popIndex] = stack.peek() - popIndex;
}
stack.push(i);
}
while(!stack.empty()) {
int popIndex = stack.pop();
if(stack.empty()) continue;
res[popIndex] = stack.peek() - popIndex;
}
return res;
}
}
下一個更大元素
import java.util.*;
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
//從右向左遍歷
//構建單調減棧
//多了一個hashMap
HashMap<Integer, Integer> map = new HashMap<>();
int res[] = new int[m];
for(int i = 0; i < m; i++) {map.put(nums1[i], i); res[i] = -1;}
Stack<Integer> stack = new Stack<>();
for(int i = n-1; i >= 0; i--) {
while(!stack.empty() && nums2[i] >= stack.peek()) {
int popValue = stack.pop();
if(stack.empty()) continue;
if(map.containsKey(popValue)) res[map.get(popValue)] = stack.peek();
}
stack.push(nums2[i]);
}
while(!stack.empty()) {
int popValue = stack.pop();
if(stack.empty()) continue;
if(map.containsKey(popValue)) res[map.get(popValue)] = stack.peek();
}
return res;
}
}
下一個更大元素II
接雨水
單調隊列
單調隊列,和單調棧的概念相似,一個隊列,裏面的元素的大小按照他們所在隊列內的位置,滿足一定的單調性(遞增或者遞減)。
單調隊列性質是:遞增隊列,隊首元素一定是當前隊列最小值,遞減隊列,隊首元素一定是當前隊列最大值
下面以遞減隊列舉例,還是數組
5、3、1依次入隊列,4入隊列的時候發生衝突,當前元素大於隊尾元素,循環出隊列(從隊尾刪除)直到無衝突。
對於上面解釋兩點,一是從隊尾出隊列,這裏所說的隊列是雙端隊列,兩邊都可以出隊,入隊。
二是,這單調隊列和單調棧有什麼區別,核心區別在於,單調隊列,可以獲取隊首元素,而棧無法直接獲取棧底元素
牛刀小試
滑動窗口最大值
class Solution {
//單調遞減隊列
class QueueImp{
LinkedList<Integer> queue = new LinkedList<>();
//添加指定值
public void add(int value) {
while(queue.size() != 0 && queue.peekLast() < value) {queue.pollLast();}
queue.offer(value);
}
//刪除指定值
public void delete(int value) {
if(queue.size() != 0 && queue.peek()== value)
queue.poll();
}
//返回隊列最大值
public int max() {
return queue.peek();
}
}
public int[] maxSlidingWindow(int[] nums, int k) {
if(k <= 0 || nums == null || nums.length == 0) return new int[0];
QueueImp queue = new QueueImp();
int []res = new int[nums.length-k+1];
int t = 0;
//初始化
for(int i = 0; i < k; i++) queue.add(nums[i]);
res[t++] = queue.max();
for(int i = k; i < nums.length; i++) {
//刪除
queue.delete(nums[i-k]);
//添加
queue.add(nums[i]);
res[t++] = queue.max();
}
return res;
}
}