单调栈顾名思义是一种单调递增或者单调递减的栈,虽然很简单,但是的确是一种高级数据结构。
之前我写的文章
算法-摩天大楼问题 是采用单调栈进行优化的。
算法-滑动窗口最大值则是维护了一个单调队列来解决问题
借助于单调栈或者单调队列的特点,我们可以优化一些算法的时间复杂度,这里再给出几个单调栈问题
本题也是华为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();
}
本题是我面字节的时候,面试官给的一道题。我给了一种贪婪算法,即每次保留最大值,然而面试官还是因为我不会优化而挂了我。
和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();
}
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,这些边边角角的修修补了。