每日一题
今天刷到一个有意思的题是去除重复字母,这是一道中等难度的题,当刚读到题目时,我还在诧异一个去重为什么是中等难度?
当正式读完提后就明白了,这道题不仅要求去重,还要求将返回结果的字典序最小,并且不能打乱原有顺序,这样就有点麻烦了。
那么像这种和左右比大小的题很大概率上都可以使用单调栈,对于这道题来说还需要一点贪心的思想。那么话不多说,我们看题。
题目要求
给你一个字符串 s
,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
示例 1:
输入:s = "bcabc"
输出"abc"
示例 2:
输入:s = "cbacdcbc"
输出:"acdb"
题目解析
这道题的关键在于我们去重时如何保证其按字典序,我的思想是使用一个单调栈。
单调栈的具体解析可以看我们另一篇博客
单调栈中字符的大小下往上递增,从头开始遍历字符串,根据不同的条件选择是将元素压入栈还是将栈顶元素弹出。
我们要保证字典序所以要让栈往上递增,因为要去重,所以在压入元素进栈时我们还要判断其是否已经存在。
因此我整个算法流程分为了两种情况,
第一种情况,是要将栈顶字符弹出的情况,如果栈顶元素大于当前字符,并且这个字符在后面还有,我们就把栈顶字符弹出,因为后面还有,所以该字符可以在后面入栈。
第二种情况:是要将字符压入栈的情况,如果当前元素小于当前字符则将字符压入栈,或者当前字符大于栈顶字符并且该字符在后面不会再出现,因为后面不会出现,所以只能此时将该字符插入。
文字描述可能还不是很清晰,下面我们用示例2,以图的形式走一遍流程。
示例二中的字符为cbacdcbc。
开始时栈中无元素,因此将c压入
随后b小于c,且c在后面还有出现,因此将c弹出,将b入栈
后面是a,和刚才一样,a小于b,并且b在后面还有,将b弹出,将a压入
随后轮到c,c大于a,将c直接压入
然后是d,一样,直接压入
然后是c,c在栈中已经存在,因此直接跳过
轮到b,b小于栈顶d,但是在后面已经没有b了。因此这里将其压入
最后一个c,已经存在,跳过,至此遍历完毕,结果出来了,为acdb.
代码实现
在实现是我用来两个数组来表示栈中已有的字符和字符串中每种字符的剩余数量。为了更好地操作,存储每个字符时其下标就是该字符-'a'。最后题解代码如下:
public static String removeDuplicateLetters(String s) {
//用来记录数量
int[] nums = new int[26];
//记录已经有了的字符
int[] hasMark = new int[26];
for (int i = 0; i < s.length(); i++) {
nums[s.charAt(i) - 'a']++;
}
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
//只有队列里面不存在的字母,才能进行单调栈比较
if (hasMark[s.charAt(i) - 'a'] == 0) {
//发现前面的字母比自己大,且后续该字母还会重复出现,那么就踢出去
while (!stack.isEmpty() && stack.peek() >= s.charAt(i) && nums[stack.peek() - 'a'] > 0) {
hasMark[stack.peek() - 'a']--;
stack.pop();
}
//入栈、记录下来已入栈
stack.push(s.charAt(i));
hasMark[s.charAt(i) - 'a']++;
}
//记录下该字符数量减1
nums[s.charAt(i) - 'a']--;
}
String res = "";
while (!stack.isEmpty()) {
res = stack.pop() + res;
}
return res;
}