算法-单调栈问题合集

单调栈顾名思义是一种单调递增或者单调递减的栈,虽然很简单,但是的确是一种高级数据结构。

之前我写的文章

算法-摩天大楼问题 是采用单调栈进行优化的。

算法-滑动窗口最大值则是维护了一个单调队列来解决问题

借助于单调栈或者单调队列的特点,我们可以优化一些算法的时间复杂度,这里再给出几个单调栈问题

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,这些边边角角的修修补了。

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