算法-單調棧問題合集

單調棧顧名思義是一種單調遞增或者單調遞減的棧,雖然很簡單,但是的確是一種高級數據結構。

之前我寫的文章

算法-摩天大樓問題 是採用單調棧進行優化的。

算法-滑動窗口最大值則是維護了一個單調隊列來解決問題

藉助於單調棧或者單調隊列的特點,我們可以優化一些算法的時間複雜度,這裏再給出幾個單調棧問題

1、移掉K位數字,使剩下的數字保持最小

本題也是華爲2020-04-29的第二題原題

402. 移掉K位數字

給定一個以字符串表示的非負整數 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、去除重複字母,使字典序最小

316. 去除重複字母

給你一個僅包含小寫字母的字符串,請你去除字符串中重複的字母,使得每個字母只出現一次。
需保證返回結果的字典序最小(要求不能打亂其他字符的相對位置)。

示例 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,這些邊邊角角的修修補了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章