题目
标题和出处
标题:去除重复字母
出处:316. 去除重复字母
难度
6 级
题目描述
要求
给你一个字符串 s \texttt{s} s,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小。
示例
示例 1:
输入:
s
=
"bcabc"
\texttt{s = "bcabc"}
s = "bcabc"
输出:
"abc"
\texttt{"abc"}
"abc"
示例 2:
输入:
s
=
"cbacdcbc"
\texttt{s = "cbacdcbc"}
s = "cbacdcbc"
输出:
"acdb"
\texttt{"acdb"}
"acdb"
数据范围
- 1 ≤ s.length ≤ 10 4 \texttt{1} \le \texttt{s.length} \le \texttt{10}^\texttt{4} 1≤s.length≤104
- s \texttt{s} s 由小写英语字母组成
解法
思路和算法
这道题要求找到字符串 s s s 的一个子序列,该子序列包含字符串 s s s 中的每个字母恰好一次,且该子序列是在满足条件的子序列中字典序最小的。
如果不考虑子序列需要包含字符串 s s s 的每个字母,一个简单的思路是:对于两个下标 i i i 和 j j j,如果 i < j i < j i<j 且 s [ i ] > s [ j ] s[i] > s[j] s[i]>s[j],则应将下标 i i i 处的字母删除。
由此可以使用单调栈得到字典序最小的子序列,单调栈存储字母,满足从栈底到栈顶的字母单调递增。从左到右遍历字符串 s s s,对于每个字母,进行如下操作:
-
如果栈不为空且栈顶字母大于当前字母,则将栈顶字母出栈,重复该操作直到栈为空或者栈顶字母不大于当前字母;
-
将当前字母入栈。
上述操作没有考虑到另外一个要求:子序列需要包含字符串 s s s 中的每个字母恰好一次。为了满足该要求,上述操作需要进行两点更改:一是记录每个字母是否已经在栈内,如果字母已经在栈内则不能将该字母重复入栈;二是记录每个字母在字符串 s s s 中的最后一次出现的下标,如果当前字母是该字母在字符串 s s s 中的最后一次出现,则不能将其出栈,确保字符串 s s s 中的每个字母都在栈内出现一次。
进行上述更改后,即可得到这道题的解法。创建两个长度为 26 26 26 的数组,其中 lastIndices \textit{lastIndices} lastIndices 用于记录每个字母在字符串 s s s 中的最后一次出现的下标, used \textit{used} used 用于记录每个字母是否在栈内,进行如下操作:
-
从左到右遍历字符串 s s s,遍历过程中记录每个字母在字符串 s s s 中的最后一次出现的下标。
-
从左到右遍历字符串 s s s,对于每个字母,通过数组 used \textit{used} used 判断该字母是否在栈内,如果该字母在栈内则跳过当前字母,如果该字母不在栈内则进行如下操作:
-
如果栈不为空且栈顶字母大于当前字母且栈顶字母的最后一次出现的下标(通过数组 lastIndices \textit{lastIndices} lastIndices 得到)大于当前下标,则将栈顶字母的状态更新为不在栈内(更新数组 used \textit{used} used 的对应元素),并将栈顶字母出栈,重复该操作直到栈为空或者栈顶字母不满足该条件;
-
将当前字母的状态更新为在栈内(更新数组 used \textit{used} used 的对应元素),并将当前字母入栈。
-
遍历结束之后,栈内字符按照从栈底到栈顶的顺序拼接得到的字符串为 s s s 的子序列,该子序列包含字符串 s s s 中的每个字母恰好一次且字典序最小。
具体实现方面,可以用 Java 的 StringBuffer \texttt{StringBuffer} StringBuffer 类或 StringBuilder \texttt{StringBuilder} StringBuilder 类的对象 sb \textit{sb} sb 模拟栈操作,在末尾添加和删除字符等价于入栈和出栈操作,遍历结束之后,将 sb \textit{sb} sb 转成 String \texttt{String} String 类的对象即可得到答案。
代码
class Solution {
public String removeDuplicateLetters(String s) {
int[] lastIndices = new int[26];
Arrays.fill(lastIndices, -1);
boolean[] used = new boolean[26];
int length = s.length();
for (int i = 0; i < length; i++) {
lastIndices[s.charAt(i) - 'a'] = i;
}
StringBuffer sb = new StringBuffer();
int top = -1;
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
int letterIndex = c - 'a';
if (!used[letterIndex]) {
while (sb.length() > 0 && sb.charAt(top) > c && lastIndices[sb.charAt(top) - 'a'] > i) {
used[sb.charAt(top) - 'a'] = false;
sb.deleteCharAt(top);
top--;
}
used[letterIndex] = true;
sb.append(c);
top++;
}
}
return sb.toString();
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是字符串 s s s 的长度。需要遍历字符串 s s s 得到每个字母在字符串 s s s 中的最后一次出现的下标,然后遍历字符串 s s s 得到字典序最小的子序列,由于每个字母最多入栈和出栈各一次,因此时间复杂度是 O ( n ) O(n) O(n)。
-
空间复杂度: O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣),其中 Σ \Sigma Σ 是字符集,这道题的字符集是全部小写字母, ∣ Σ ∣ = 26 |\Sigma| = 26 ∣Σ∣=26。空间复杂度主要取决于两个数组和栈空间,两个数组的长度都是 ∣ Σ ∣ |\Sigma| ∣Σ∣,由于栈内每个字符最多出现一次,因此栈内元素个数不会超过 ∣ Σ ∣ |\Sigma| ∣Σ∣。