给你一个字符串 s
,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小 (要求不能打乱其他字符的相对位置)。
示例 1:
**输入:**s = "bcabc"
**输出:**"abc"
示例 2:
**输入:**s = "cbacdcbc"
**输出:**"acdb"
提示:
1 <= s.length <= 104
s
由小写英文字母组成
注意: 该题与 1081 <https://leetcode-cn.com/problems/smallest-subsequence-of-
distinct-characters> 相同
📺 视频题解
📖 文字题解
方法一:贪心 + 单调栈
思路与算法
首先考虑一个简单的问题:给定一个字符串 $s$,如何去掉其中的一个字符 ch,使得得到的字符串字典序最小呢?答案是:找出最小的满足 $s[i]>s[i+1]$ 的下标 $i$,并去除字符 $s[i]$。为了叙述方便,下文中称这样的字符为「关键字符」。
在理解这一点之后,就可以着手本题了。一个直观的思路是:我们在字符串 $s$ 中找到「关键字符」,去除它,然后不断进行这样的循环。但是这种朴素的解法会创建大量的中间字符串,我们有必要寻找一种更优的方法。
我们从前向后扫描原字符串。每扫描到一个位置,我们就尽可能地处理所有的「关键字符」。假定在扫描位置 $s[i-1]$ 之前的所有「关键字符」都已经被去除完毕,在扫描字符 $s[i]$ 时,新出现的「关键字符」只可能出现在 $s[i]$ 或者其后面的位置。
于是,我们使用单调栈来维护去除「关键字符」后得到的字符串,单调栈满足栈底到栈顶的字符递增。如果栈顶字符大于当前字符 $s[i]$,说明栈顶字符为「关键字符」,故应当被去除。去除后,新的栈顶字符就与 $s[i]$ 相邻了,我们继续比较新的栈顶字符与 $s[i]$ 的大小。重复上述操作,直到栈为空或者栈顶字符不大于 $s[i]$。
我们还遗漏了一个要求:原字符串 $s$ 中的每个字符都需要出现在新字符串中,且只能出现一次。为了让新字符串满足该要求,之前讨论的算法需要进行以下两点的更改。
代码
[sol1-C++]1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Solution { public: string removeDuplicateLetters(string s) { vector<int> vis(26), num(26); for (char ch : s) { num[ch - 'a']++; }
string stk; for (char ch : s) { 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; } };
|
[sol1-Java]1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Solution { public String removeDuplicateLetters(String s) { boolean[] vis = new boolean[26]; int[] num = new int[26]; for (int i = 0; i < s.length(); i++) { num[s.charAt(i) - 'a']++; }
StringBuffer sb = new StringBuffer(); for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (!vis[ch - 'a']) { while (sb.length() > 0 && sb.charAt(sb.length() - 1) > ch) { if (num[sb.charAt(sb.length() - 1) - 'a'] > 0) { vis[sb.charAt(sb.length() - 1) - 'a'] = false; sb.deleteCharAt(sb.length() - 1); } else { break; } } vis[ch - 'a'] = true; sb.append(ch); } num[ch - 'a'] -= 1; } return sb.toString(); } }
|
[sol1-JavaScript]1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var removeDuplicateLetters = function(s) { const vis = new Array(26).fill(0); const num = _.countBy(s); const sb = new Array(); for (let i = 0; i < s.length; i++) { const ch = s[i]; if (!vis[ch.charCodeAt() - 'a'.charCodeAt()]) { while (sb.length > 0 && sb[sb.length - 1] > ch) { if (num[sb[sb.length - 1]] > 0) { vis[sb[sb.length - 1].charCodeAt() - 'a'.charCodeAt()] = 0; sb.pop(); } else { break; } } vis[ch.charCodeAt() - 'a'.charCodeAt()] = 1; sb.push(ch); } num[ch]--; } return sb.join(''); };
|
[sol1-Golang]1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func removeDuplicateLetters(s string) string { left := [26]int{} for _, ch := range s { left[ch-'a']++ } stack := []byte{} inStack := [26]bool{} for i := range s { ch := s[i] if !inStack[ch-'a'] { for len(stack) > 0 && ch < stack[len(stack)-1] { last := stack[len(stack)-1] - 'a' if left[last] == 0 { break } stack = stack[:len(stack)-1] inStack[last] = false } stack = append(stack, ch) inStack[ch-'a'] = true } left[ch-'a']-- } return string(stack) }
|
[sol1-C]1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| char* removeDuplicateLetters(char* s) { int vis[26], num[26]; memset(vis, 0, sizeof(vis)); memset(num, 0, sizeof(num));
int n = strlen(s); for (int i = 0; i < n; i++) { num[s[i] - 'a']++; }
char* stk = malloc(sizeof(char) * 27); int stkTop = 0; for (int i = 0; i < n; i++) { if (!vis[s[i] - 'a']) { while (stkTop > 0 && stk[stkTop - 1] > s[i]) { if (num[stk[stkTop - 1] - 'a'] > 0) { vis[stk[--stkTop] - 'a'] = 0; } else { break; } } vis[s[i] - 'a'] = 1; stk[stkTop++] = s[i]; } num[s[i] - 'a'] -= 1; } stk[stkTop] = '\0'; return stk; }
|
复杂度分析