题目:
给你一个字符串 s
,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
解法一(哈希表):
根据字典序最小的原理,应尽可能保证最小的字典序的字符出现在字符串前面(即字符串第一位尽可能小,字符串第二位在剩余字符串中也应尽可能小以满足题目要求),因此定义vector<char>保存出现的所有字符串并将其进行排序,定义unordered_map<char, int>保持出现所有字符的数量,用于确定满足题目要求的最小字典序的字符出现的位置。
在vector<char>排序字符中,先确定最小的字符是否满足题目要求(即在s中第一次出现时的位置,并保证vector<char>剩余包含的字符在后续中s中均可以查找到),通过哈希表计数,当s后续中不存在vector<char>剩余包含的字符时,该最小字典序不能作为最小字符,查找vector<char>第二小的字符,以此类推.....当最小字典序满足要求时,记录在s的索引位置(后续遍历查找均在该索引后进行),并在vector<char>删除满足要求的字符,进入下一次循环直至vector<char>中无剩余字符停止迭代。如下为笔者代码:
class Solution {
public:
int getindex(string& s, unordered_map<char, int> hashTable1, char c, int index, vector<char> kyk){
int length = s.size();
int result=-1;
int length_kyk = kyk.size();
for(int i=0;i<index;i++){
hashTable1[s[i]]--;
}
for(int i=index;i<length;i++){
if(s[i]!=c){
hashTable1[s[i]]--;
if(hashTable1[s[i]]==0){
return -1;
}
}
else{
return i;
}
}
return result;
}
string removeDuplicateLetters(string s) {
string result1="";
vector<char> kyk;
unordered_map<char, int> hashTable1;
for(char c:s){
auto it = hashTable1.find(c);
hashTable1[c]++;
if(it==hashTable1.end()){
kyk.push_back(c);
}
}
int length_kyk = kyk.size();
int length = s.size();
sort(kyk.begin(),kyk.end());
int index=0;
for(int i=0; i<length_kyk; i++){
int a = getindex(s, hashTable1, kyk[i], index, kyk);
if(a==-1){
continue;
}
else{
result1=result1+kyk[i];
hashTable1.erase(kyk[i]);
kyk.erase(kyk.begin()+i);
length_kyk--;
index = a;
i=-1;
}
}
return result1;
}
};
解法二(贪心 + 单调栈):
首先考虑一个简单的问题:给定一个字符串s,如何去掉其中一个字符ch,使得得到的字符串字典序最小呢?答案是:找出最小的满足s[i]>s[i+1]的下表i,并去除字符s[i]。为了叙述方便,下面称这样的字符为【关键字符】。
我们从前向后扫描原字符串。每扫描到一个位置,我们就尽可能地处理所有的【关键字符】。假定在扫描位置s[i-1]之前的所有【关键字符】都已经被去除完毕,在扫描字符s[i]时,新出现的【关键字符】只可能出现在s[i]或者其后面的位置。
于是,我们使用单调栈来维护去除【关键字符】后得到的字符串,单调栈满足栈底到栈顶的字符递增。如果栈顶字符大于当前字符s[i],说明栈顶字符为【关键字符】,故应当被去除。去除后,新的栈顶字符就与s[i]相邻了,我们继续比较新的栈顶字符与s[i]的大小。重复上述操作,直到栈为空或者栈顶字符不大于s[i]。
我们还遗漏了一个要求:原字符串s中的每个字符都需要出现在新字符串中,且只能出现一次,为了让新字符串满足该要求,之前讨论的算法需要进行一下两点的更改:
1、在考虑字符s[i]时,如果它已经存在于栈中,则不能加入字符s[i]。为此,需要记录每个字符是否出现在栈中。
2、在弹出栈顶字符时,如果字符串在后面的位置上再也没有这一字符,则不能弹出栈顶字符。为此,需要记录每个字符的剩余数量,当这个值为0时,就不能弹出栈顶字符。
如下为利用贪心算法+单调栈数据结构实现的代码:
class Solution {
public:
string removeDuplicateLetters(string s) {
//num记录26个英文字符出现的次数
//vis为记录每个字符是否出现在栈中(初始值为均0,1为在栈中,0为不在栈中)
vector<int> vis(26), num(26);
for (char ch : s) {
num[ch - 'a']++;
}
//定义单调栈stk(string类型)
string stk;
for (char ch : s) {
//判断vis站中是否存在相同的字符,如果栈中已出现,则不能加入字符ch
if (!vis[ch - 'a']) {
while (!stk.empty() && stk.back() > ch) {
if (num[stk.back() - 'a'] > 0) {
vis[stk.back() - 'a'] = 0;
stk.pop_back();
} else {
break;
}
}
vis[ch - 'a'] = 1;
stk.push_back(ch);
}
num[ch - 'a'] -= 1;
}
return stk;
}
};
时间复杂度:O(N),其中 N 为字符串长度。代码中虽然有双重循环,但是每个字符至多只会入栈、出栈各一次。空间复杂度:O(∣Σ∣),其中 Σ 为字符集合,本题中字符均为小写字母,所以 ∣Σ∣=26。由于栈中的字符不能重复,因此栈中最多只能有 ∣Σ∣ 个字符,另外需要维护两个数组,分别记录每个字符是否出现在栈中以及每个字符的剩余数量
笔者小记:
1、提示中s均为26个小写英文字符,则可以定义vector<int>数组用于计数和判断在某一(另一个数据结构)中存储出现情况。
2、相比利用哈希表,利用单调栈(string类型的)和vector<int>(技术和判断是否出现情况)可以更好的实现低时间复杂度的一次遍历。
3、vector<int> vis(26) 表示创建数组名为vis,数组大小为26的数组。