大家好,我是程式設計熊,雙非逆襲選手,位元組跳動、曠視科技前員工,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. 長度最小的子陣列