大家好,我是編程熊,雙非逆襲選手,字節跳動、曠視科技前員工,ACM金牌,保研985,《ACM金牌選手講解LeetCode算法系列》作者。
公眾號:『編程熊』
上一篇文章講解了《線性表》中的數組、鏈表、棧和隊列的概念和基本應用,本文講解棧和隊列的高級應用。
- 單調棧
- 雙端隊列
- 滑動窗口
單調棧
介紹
單調棧 = 單調 + 棧,因此其同時滿足兩個特性: 單調性、棧的特點。
- 單調性: 單調棧里面所存放的數據是有序的(單調遞增或遞減)。
- 棧: 后進先出。
因其滿足單調性和每個數字只會入棧一次,所以可以在時間復雜度 O(n)
的情況下解決一些問題。
下圖是單調棧的圖解,棧內數字滿足單調性,且滿足棧的后進先出的特性。
例題
LeetCode 739. 每日溫度
題意
給定每天的溫度,求對於每一天需要等幾天才可以等到更暖和的一天。如果該天之后不存在 更暖和的天氣,則記為 0。
輸出一個一維數組,表示每天需要等待的天數。
示例
輸入: temperatures = [73,74,75,71,69,72,76,73]
輸出: [1,1,4,2,1,1,0,0]
題解
建立單調(非增)棧,棧存放每天的溫度,為了方便計算天數,棧中存儲的每天的溫度在數組中下標,可以通過下標得到對應天的溫度。
設溫度數組為 a
,從左向右依次遍歷數組 a
,假設當前遍歷到數組位置為 j
,則對應的天溫度為 a[j]
,設棧頂元素的位置為 i
,則對應的天的溫度a[i]
,分為兩種情況討論。
- 如果
a[j] > a[i]
,執行以下三步。- 表明比第
i
天更暖和的一天為第j
天,則第i
天的答案為j-i
,那么可以將棧頂元素彈出。 - 重復檢查棧頂元素,直至棧頂元素的
a[j] <= a[i]
或者 棧為空。 - 將
j
入棧。
- 表明比第
- 如果
a[j] <= a[i]
,- 表明第
i
天沒有找到更暖和的一天,無需對棧操作。 - 將
j
入棧。
- 表明第
然后繼續遍歷溫度數組 a
,考慮下一天,直至結束。
遍歷結束,若棧不為空,則說明棧內的天找不到更暖和的一天,記為 0
。
代碼
class Solution {
public int[] dailyTemperatures(int[] T) {
int[] ans = new int[T.length];
Deque<Integer> s = new LinkedList<Integer>();
for(int i = 0; i < T.length; i++) {
while(!s.isEmpty() && T[i] > T[s.peek()]) {
ans[s.peek()] = i - s.pop();
}
s.push(i);
}
return ans;
}
}
LeetCode 316. 去除重復字母
題意
給你一個字符串 s
,請你去除字符串中重復的字母,使得每個字母只出現一次。需保證 返回結果的字典序最小(要求不能打亂其他字符的相對位置)。
示例
輸入:s = "bcabc"
輸出:"abc"
題解
首先思考這個問題的一個簡單版本。給一個字符串刪除一個字符,使得字典序最小。
- 解法: 字典序就是字母的大小順序,我們想字典序最小,那應刪除滿足
s[i] > s[i+1]
的最小位置i
上的字符。
回到這個問題,我們也是想盡可能的刪除滿足 s[i] > s[i+1]
的最小位置 i
上的字符,如果每次都是遍歷一遍字符串刪除一個字符,這樣時間復雜度可能退化到 O(n^2)
。
優化方法: 單調棧。
單調棧中存放的是字符,從左往右遍歷字符串 s
, 設當前遍歷到字符串的位置 i
,棧頂字符為c
,考慮 s[i]
和 棧頂字符的大小關系、位置 i
的字符不在棧中,可分為兩種。
- 若
c > s[i]
並且 位置i
的字符不在棧中 並且 在位置i
后面還存在字符c
,那么將c
從棧中彈出。重復這個過程,直到c > s[i]
不成立 或者 棧為空。 - 不滿足上述條件,直接將
s[i]
放入棧中。
繼續遍歷字符串 s
,直至結束,最后棧中的字符就是題目要求的字典序最小的字符串。
代碼
class Solution {
public String removeDuplicateLetters(String s) {
int[] count = new int[30];
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i) - 'a']++;
}
boolean[] vis = new boolean[30];
StringBuffer ans = new StringBuffer();
for (int i = 0; i < s.length(); i++) {
int c = s.charAt(i) - 'a';
if (!vis[c]) {
while ((ans.length() > 0) && (count[ans.charAt(ans.length() - 1) - 'a'] > 0)
&& ((ans.charAt(ans.length() - 1) - 'a') > c)) {
vis[ans.charAt(ans.length() - 1) - 'a'] = false;
ans.deleteCharAt(ans.length() - 1);
}
vis[c] = true;
ans.append(s.charAt(i));
}
count[c]--;
}
return ans.toString();
}
}
習題推薦
- LeetCode 496. 下一個更大元素 I
- LeetCode 1475. 商品折扣后的最終價格
- LeetCode 503. 下一個更大元素 II
雙端隊列 & 滑動窗口
介紹
雙端隊列是普通隊列的加強版 ,區別於隊列只能從隊頭出隊,隊尾入隊;雙端隊列既可以在隊頭入隊和出隊,也可以在隊尾入隊和出隊。
下圖是雙端隊列的的圖解,可以看出,雙端隊列既可以在隊頭入隊和出隊,也可以在隊尾入隊和出隊。
例題
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
題解
滑動窗口經典題,維護一個單調的雙端隊列,為了方便,雙端隊列里面的數組的下標,從前往后遍歷數組,需要實現兩個功能。
- 若 隊頭位置下標 和 當前遍歷位置下標 的距離大於
k
,則刪除隊頭元素,保證了隊頭下標在當前滑動窗口內。 - 若 隊尾位置下標對應的值 小於 當前位置的值,則刪除隊尾元素,保證了隊頭下標對應的值是最大的。
其次將當前遍歷位置下標放入雙端隊列,然后遍歷數組的下一個位置,直至結束。
代碼
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> q = new LinkedList<>();
int ans[] = new int[nums.length - k + 1];
for (int i = 0; i < k; i++) {
while (!q.isEmpty() && nums[q.getLast()] < nums[i]) {
q.removeLast();
}
q.addLast(i);
}
ans[0] = nums[q.getFirst()];
for (int i = k; i < nums.length; i++) {
while(!q.isEmpty() && (i - q.getFirst() >= k)) {
q.removeFirst();
}
while(!q.isEmpty() && nums[q.getLast()] < nums[i]) {
q.removeLast();
}
q.addLast(i);
ans[i - k + 1] = nums[q.getFirst()];
}
return ans;
}
}
LeetCode 3. 無重復字符的最長子串
題意
給定一個字符串,請你找出其中不含有重復字符的 最長子串 的長度。
示例
輸入: s = "abcabcbb"
輸出: 3
解釋: 因為無重復字符的最長子串是 "abc",所以其長度為 3。
題解
觀察樣例,我們可以發現,依次遞增地枚舉子串的起始位置,那么合法的結束為止一定是遞增的,因為對於起始位置 i-1
,假設其不含有重復字符的最遠右位置 j
;那么對於起始位置為 i
的子串,因為 [i-1,j]
不含有重復字符,其不含有重復字符的最遠右位置一定大於等於 i
,因此我們考慮使用滑動窗口來解決本題。
我們可以固滑動窗口的右邊界,找到最遠的不含有重復字符的左邊界 ,根據上面我們觀察得到的性質可以,不含有重復字符的左邊界是非遞減的。
代碼具體實現上我們可以用 雙端隊列實現滑動窗口,輔助數組 cnt
統計窗口內每個字符出現的次數 ,來判斷窗口是否有重復的字符。
代碼
class Solution {
public int lengthOfLongestSubstring(String s) {
char[] cnt = new char[128];
LinkedList<Character> q = new LinkedList<Character>();
int ans = 0;
for (int i = 0; i < s.length(); i++) {
q.add(s.charAt(i));
cnt[s.charAt(i)]++;
while (cnt[s.charAt(i)] > 1) {
char frontC = q.pollFirst();
cnt[frontC]--;
}
ans = Math.max(ans, q.size());
}
return ans;
}
}
習題推薦
LeetCode 209. 長度最小的子數組