單調棧顧名思義是一種單調遞增或者單調遞減的棧,雖然很簡單,但是的確是一種高級數據結構。
之前我寫的文章
算法-摩天大樓問題 是採用單調棧進行優化的。
算法-滑動窗口最大值則是維護了一個單調隊列來解決問題
藉助於單調棧或者單調隊列的特點,我們可以優化一些算法的時間複雜度,這裏再給出幾個單調棧問題
1、移掉K位數字,使剩下的數字保持最小
本題也是華爲2020-04-29的第二題原題
給定一個以字符串表示的非負整數 num,移除這個數中的 k 位數字,使得剩下的數字最小。
注意:
num 的長度小於 10002 且 ≥ k。
num 不會包含任何前導零。
示例 1 :
輸入: num = "1432219", k = 3
輸出: "1219"
解釋: 移除掉三個數字 4, 3, 和 2 形成一個新的最小的數字 1219。
示例 2 :
輸入: num = "10200", k = 1
輸出: "200"
解釋: 移掉首位的 1 剩下的數字爲 200. 注意輸出不能有任何前導零。
示例 3 :
輸入: num = "10", k = 2
輸出: "0"
解釋: 從原數字移除所有的數字,剩餘爲空就是0。
解題思路:
首先,從直觀上講,我們要移除數裏面最大的元素,這樣就能讓數變小,但是,以下面這個數爲例,移除最大的元素一定能讓數變小嗎?
3214
很明顯,在k=1的時候,我們移除的數字應該是3,而不是4,這樣我們才能得到最小的數。
爲什麼移除4不是最小呢?因爲我們還要考慮到移除元素的位置,簡而言之,越靠前的元素影響力越大。
我們可以用單調棧解決這個問題。
1、遍歷每個元素,維護一個包含本節點的單調遞增棧,大於當前節點的元素要彈出。
2、在1中,我們只需要移除k個元素就可以了,也就是說,當刪除元素數量超過k的時候,不需要再維護單調棧。
3、在2中,遍歷完成後,移除的元素可能沒有達到k個,也就是說,單調棧後面的元素需要彈出。
4、將3中得到的棧元素彈出。
5、4中得到的是數據的逆序,我們將後面位置連續爲0的元素刪除,再逆序,即得到最小值
public String removeKdigits(String num, int k) {
if(num==null||k>=num.length()){
return "0";
}
Stack<Character> stack=new Stack<>();
for(int i=0;i<num.length();i++){
char c=num.charAt(i);
while(k>0&&stack.size()>0&&stack.peek()>c){
stack.pop();
k--;
}
stack.push(c);
}
while(k-->0){
stack.pop();
}
StringBuilder sb=new StringBuilder();
while(stack.size()>0){
sb.append(stack.pop());
}
while(sb.length()>1&&sb.charAt(sb.length()-1)=='0'){
sb.deleteCharAt(sb.length()-1);
}
return sb.reverse().toString();
}
2、移掉K位數字,使剩下的數字保持最大
本題是我面字節的時候,面試官給的一道題。我給了一種貪婪算法,即每次保留最大值,然而面試官還是因爲我不會優化而掛了我。
和1相似,本題目只需要維持一個單調遞減棧。所以我們只需要改一下符號即可
public static String removeKdigits(String num, int k) {
if (num == null || k >= num.length()) {
return "0";
}
Stack<Character> stack = new Stack<>();
for (int i = 0; i < num.length(); i++) {
char c = num.charAt(i);
//維護一個以本節點爲棧頂的單調遞減棧(即從左到右刪掉前面元素小於後面元素的情況)
while (!stack.isEmpty() && stack.peek() < c && k > 0) {
stack.pop();
k--;
}
stack.push(c);
}
//如果沒有刪完,繼續刪掉後面的一些元素
while (k > 0) {
stack.pop();
k--;
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
while (sb.length() > 1 && sb.charAt(sb.length() - 1) == '0') {
sb.deleteCharAt(sb.length() - 1);
}
return sb.reverse().toString();
}
3、去除重複字母,使字典序最小
給你一個僅包含小寫字母的字符串,請你去除字符串中重複的字母,使得每個字母只出現一次。
需保證返回結果的字典序最小(要求不能打亂其他字符的相對位置)。
示例 1:
輸入: "bcabc"
輸出: "abc"
示例 2:
輸入: "cbacdcbc"
輸出: "acdb"
本題也可以用單調棧解決,甚至可以直接套上第一題的代碼…這裏給出兩種解法
1、統計重複字母數量k,使用題目1方式得到最小字典序
2、每當遇到一個新字母時,判斷新字母與棧頂元素的大小,如果新字母小於棧頂元素,並且棧頂元素在字符串的後面再次出現,就彈出當前棧頂元素,最後將當前元素入棧
public String removeDuplicateLetters(String s) {
Stack<Character> stack=new Stack<>();
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
if(!stack.contains(c)){
while(!stack.isEmpty()&&stack.peek()>c&&s.indexOf(stack.peek(),i)!=-1){
stack.pop();
}
stack.push(c);
}
}
StringBuilder sb=new StringBuilder();
while(!stack.isEmpty()){
sb.append(stack.pop());
}
return sb.reverse().toString();
}
不難看出,上面給出的解法時間複雜度可能會退化成 N^2,因爲在尋找棧頂元素最後出現位置的時候可能要遍歷數組。
另外,在判斷棧裏面是否存在某元素時,雖然時間複雜度是O(1),但這個O(1)有可能是O(26),所以,從這裏面我們也可以做做文章。
什麼數據結構的時間複雜度爲真正的O(1)呢?答案不言而喻,就是HashMap!
不過,即便HashMap查找時間複雜度爲O(1),其中計算索引,調用棧的開銷也是一個不可忽略的地方,由於字母數量是有限的,因此,我們可以用數組自己實現hash表。
給大家一個擊敗96.74%用戶的方法
執行用時 :
3 ms, 在所有 Java 提交中擊敗了96.74%的用戶
內存消耗 :39.4 MB, 在所有 Java 提交中擊敗了16.67%
的用戶
我們可以用一個數組存儲元素最後出現的位置
再用另一個數組存儲棧裏面是否含有某個元素
public String removeDuplicateLetters(String s) {
int[] map=new int[128];
char cs[]=s.toCharArray();
for(int i=cs.length-1;i>=0;i--){
if(map[cs[i]]==0){
map[cs[i]]=i;//標註元素最後出現的位置
}
}
int[] mark=new int[128];//標註是不是棧裏面的新元素
Stack<Character> stack=new Stack<>();
for (int i=0;i<cs.length;i++){
char c=cs[i];
if(mark[c]==0){
//當前棧頂元素大於新元素,並且棧頂元素最後出現的位置大於當前位置,那麼彈出棧頂元素
while (!stack.isEmpty()&&stack.peek()>c&&map[stack.peek()]>i){
mark[stack.peek()]=0;//彈出了,不再是新元素
stack.pop();
}
stack.push(c);
mark[c]=1;//標註已入棧
}
}
StringBuilder sb=new StringBuilder();
while (stack.size()>0){
sb.append(stack.pop());
}
return sb.reverse().toString();
}
這種方法基本上做到了最優,時間複雜度真正的控制爲O(N)。
再優化的地方可能就是我們用數組加指針,自己實現一個棧,加快訪問速度。將自己實現的hash表大小調整爲26,這些邊邊角角的修修補了。