题目来源
题目描述
class Solution {
public:
string removeDuplicateLetters(string s) {
}
};
题目解析
思路
题目:
- 要求一、要去重。
- 要求二、去重字符串中的字符顺序不能打乱 s 中字符出现的相对顺序。
- 要求三、在所有符合上一条要求的去重字符串中,字典序最小的作为最终结果。
上述三条要求中,要求三可能有点难理解,举个例子。
比如说输入字符串 s = “babc”,去重且符合相对位置的字符串有两个,分别是 “bac” 和 “abc”,但是我们的算法得返回 “abc”,因为它的字典序更小。
按理说,如果我们想要有序的结果,那就得对原字符串排序对吧,但是排序后就不能保证符合 s 中字符出现顺序了,这似乎是矛盾的。
现在我们只实现要求一和要求二
怎么实现呢?
class Solution {
public:
string removeDuplicateLetters(string s) {
std::stack<char> stk; // 存放去重的结果
// 布尔数组初始值为 false,记录栈中是否存在某个字符
// 输入字符均为 ASCII 字符,所以大小 256 够用了
std::vector<bool> inStack(256, false);
for(char c : s){
if(inStack[c]){
continue;
}
stk.push(c);
inStack[c] = true;
}
std::string sb;
while (!stk.empty()){
sb.insert(sb.begin(), stk.top()); stk.pop();
}
return sb;
}
};
此时已经满足要求一和要求二了,如果输入 s = “bcabc”,这个算法会返回 “bca”,已经符合要求一和要求二了,但是题目希望要的答案是 “abc” 对吧。
那我们想一想,如果想满足要求三,保证字典序,需要做些什么修改?
在向栈stk中插入字符a
的那一刻,我们的算法需要知道,字符a
的字典序和之前的两个字符b
和c
相比,谁大谁小。
如果当前字符a
比之前的字符小,可能需要把前面的字符pop出栈,让a
排到前面,如下:
class Solution {
public:
string removeDuplicateLetters(string s) {
std::stack<char> stk; // 存放去重的结果
// 布尔数组初始值为 false,记录栈中是否存在某个字符
// 输入字符均为 ASCII 字符,所以大小 256 够用了
std::vector<bool> inStack(256, false);
for(char c : s){
if(inStack[c]){
continue;
}
// 插入之前,和之前的元素比较一下大小
// 如果字典序比前面的小,pop 前面的元素
while (!stk.empty() && stk.top() > c){
inStack[stk.top()] = false;
stk.pop();
}
stk.push(c);
inStack[c] = true;
}
std::string sb;
while (!stk.empty()){
sb.insert(sb.begin(), stk.top()); stk.pop();
}
return sb;
}
};
这段代码也好理解,就是插入了一个 while 循环,连续 pop 出比当前字符小的栈顶字符,直到栈顶元素比当前元素的字典序还小为止。只是不是有点「单调栈」的意思了?
这样,对于输入 s = “bcabc”,我们可以得出正确结果 “abc” 了。
但是,如果我改一下输入,假设 s = “bcac”,按照刚才的算法逻辑,返回的结果是 “ac”,而正确答案应该是 “bac”,分析一下这是怎么回事?
很容易发现,因为 s 中只有唯一一个 ‘b’,即便字符 ‘a’ 的字典序比字符 ‘b’ 要小,字符 ‘b’ 也不应该被 pop 出去。
那问题出在哪里?
我们的算法在 stk.top() > c才会 pop 元素,其实这时候应该分两种情况:
- 情况一、如果 stk.peek() 这个字符之后还会出现,那么可以把它 pop 出去,反正后面还有嘛,后面再 push 到栈里,刚好符合字典序的要求。
- 情况二、如果 stk.peek() 这个字符之后不会出现了,前面也说了栈中不会存在重复的元素,那么就不能把它 pop 出去,否则你就永远失去了这个字符。
回到 s = “bcac” 的例子,插入字符 ‘a’ 的时候,发现前面的字符 ‘c’ 的字典序比 ‘a’ 大,且在 ‘a’ 之后还存在字符 ‘c’,那么栈顶的这个 ‘c’ 就会被 pop 掉。
while 循环继续判断,发现前面的字符 ‘b’ 的字典序还是比 ‘a’ 大,但是在 ‘a’ 之后再没有字符 ‘b’ 了,所以不应该把 ‘b’ pop 出去。
那么关键就在于,如何让算法知道字符 ‘a’ 之后有几个 ‘b’ 有几个 ‘c’ 呢?如下:
class Solution {
public:
string removeDuplicateLetters(string s) {
std::stack<char> stk; // 存放去重的结果
// 布尔数组初始值为 false,记录栈中是否存在某个字符
// 输入字符均为 ASCII 字符,所以大小 256 够用了
std::vector<bool> inStack(256, false);
// 维护一个计数器记录字符串中字符的数量
std::vector<int> count(256);
for(char c : s){
count[c]++;
}
for(char c : s){
// 每遍历过一个字符,都将对应的计数减一
count[c]--;
if(inStack[c]){
continue;
}
// 插入之前,和之前的元素比较一下大小
// 如果字典序比前面的小,pop 前面的元素
while (!stk.empty() && stk.top() > c){
// 若之后不存在栈顶元素了,则停止 pop
if(count[stk.top()] == 0){
break;
}
// 若之后还有,则可以 pop
inStack[stk.top()] = false;
stk.pop();
}
stk.push(c);
inStack[c] = true;
}
std::string sb;
while (!stk.empty()){
sb.insert(sb.begin(), stk.top()); stk.pop();
}
return sb;
}
};
我们用了一个计数器 count,当字典序较小的字符试图「挤掉」栈顶元素的时候,在 count 中检查栈顶元素是否是唯一的,只有当后面还存在栈顶元素的时候才能挤掉,否则不能挤掉。
至此,这个算法就结束了,时间空间复杂度都是 O(N)。
你还记得我们开头提到的三个要求吗?我们是怎么达成这三个要求的?
- 要求一、通过 inStack 这个布尔数组做到栈 stk 中不存在重复元素。
- 要求二、
- 我们顺序遍历字符串 s,通过「栈」这种顺序结构的 push/pop 操作记录结果字符串,保证了字符出现的顺序和 s 中出现的顺序一致。
- 这里也可以想到为什么要用「栈」这种数据结构,因为先进后出的结构允许我们立即操作刚插入的字符,如果用「队列」的话肯定是做不到的。
- 要求三、我们用类似单调栈的思路,配合计数器 count 不断 pop 掉不符合最小字典序的字符,保证了最终得到的结果字典序最小。
思路
class Solution {
public:
string removeDuplicateLetters(string s) {
std::string stk;
// 首先,我们要记录字符串中每个字符出现的次数(在遍历中,每次容器移除字母时,需要减去相应字母的出现次数)。
std::vector<int> cnt(26);
for(auto a: s){
++cnt[a - 'a'];
}
for(auto a : s){
// 遇到一个新字符 如果比栈顶小 并且在新字符后面还有和栈顶一样的 就把栈顶的字符抛弃了
if (stk.find(a) == std::string::npos){
// b
while(!stk.empty() && cnt[stk.back()-'a'] > 0 && stk.back() > a)
{
stk.pop_back();
}
stk.push_back(a);
}
// 当前遍历的字母在容器中已经出现过,将该字母次数减1, 进行下一次遍历。
--cnt[a - 'a'];
}
return stk;
}
};
类似题目
题目 | 思路 |
---|---|
leetcode:316.移除重复字母 Remove Duplicate Letters | |
leetcode:321. 拼接最大数 Create Maximum Number | |
leetcode:402. 去掉K位数字 Remove K Digits | |
leetcode:1081. 不同字符的最小子序列 smallest-subsequence-of-distinct-characters |