Medium
题目描述
给定一个字符串 s,返回 s 的一个子序列,使得它包含 s 的所有不同字符,且只出现一次,并且按字典序最小。
示例 1:
输入:s = "bcabc"
输出:"abc"
示例 2:
输入:s = "cbacdcbc"
输出:"acdb"
提示:
1 <= s.length <= 1000s由小写英文字母组成
注意: 这个问题与第 316 题相同:https://leetcode.com/problems/remove-duplicate-letters/
解题思路
这是一道经典的单调栈问题。我们需要构造一个字典序最小的子序列,包含所有不同字符且每个字符只出现一次。
核心思路是使用贪心策略:
- 首先统计每个字符的出现次数,这样我们知道某个字符在后面是否还会出现
- 遍历字符串,对于每个字符:
- 如果已经在结果中,跳过(因为每个字符只能出现一次)
- 否则,为了保证字典序最小,需要将栈顶比当前字符大且在后面还会出现的字符移除
- 将当前字符加入结果
使用单调栈来维护结果序列:
- 栈中保存当前的结果字符
- 用一个布尔数组记录某个字符是否已经在栈中
- 当遇到新字符时,如果栈顶字符比它大且后面还会出现,就弹出栈顶
这个算法确保了:
- 每个字符只出现一次(通过
inStack数组控制) - 字典序最小(通过贪心地移除不必要的大字符)
- 包含所有不同字符(因为我们会遍历完整个字符串)
代码实现
class Solution {
public:
string smallestSubsequence(string s) {
vector<int> count(26, 0);
vector<bool> inStack(26, false);
string result;
// 统计每个字符的出现次数
for (char c : s) {
count[c - 'a']++;
}
for (char c : s) {
count[c - 'a']--;
// 如果字符已经在结果中,跳过
if (inStack[c - 'a']) continue;
// 移除栈顶比当前字符大且后面还会出现的字符
while (!result.empty() && result.back() > c && count[result.back() - 'a'] > 0) {
inStack[result.back() - 'a'] = false;
result.pop_back();
}
result.push_back(c);
inStack[c - 'a'] = true;
}
return result;
}
};
class Solution:
def smallestSubsequence(self, s: str) -> str:
count = {}
in_stack = set()
stack = []
# 统计每个字符的出现次数
for c in s:
count[c] = count.get(c, 0) + 1
for c in s:
count[c] -= 1
# 如果字符已经在栈中,跳过
if c in in_stack:
continue
# 移除栈顶比当前字符大且后面还会出现的字符
while stack and stack[-1] > c and count[stack[-1]] > 0:
removed = stack.pop()
in_stack.remove(removed)
stack.append(c)
in_stack.add(c)
return ''.join(stack)
public class Solution {
public string SmallestSubsequence(string s) {
int[] count = new int[26];
bool[] inStack = new bool[26];
Stack<char> stack = new Stack<char>();
// 统计每个字符的出现次数
foreach (char c in s) {
count[c - 'a']++;
}
foreach (char c in s) {
count[c - 'a']--;
// 如果字符已经在栈中,跳过
if (inStack[c - 'a']) continue;
// 移除栈顶比当前字符大且后面还会出现的字符
while (stack.Count > 0 && stack.Peek() > c && count[stack.Peek() - 'a'] > 0) {
char removed = stack.Pop();
inStack[removed - 'a'] = false;
}
stack.Push(c);
inStack[c - 'a'] = true;
}
char[] result = new char[stack.Count];
for (int i = result.Length - 1; i >= 0; i--) {
result[i] = stack.Pop();
}
return new string(result);
}
}
var smallestSubsequence = function(s) {
const count = new Array(26).fill(0);
const inStack = new Array(26).fill(false);
const stack = [];
// 统计每个字符的出现次数
for (const c of s) {
count[c.charCodeAt(0) - 97]++;
}
for (const c of s) {
count[c.charCodeAt(0) - 97]--;
// 如果字符已经在栈中,跳过
if (inStack[c.charCodeAt(0) - 97]) continue;
// 移除栈顶比当前字符大且后面还会出现的字符
while (stack.length > 0 && stack[stack.length - 1] > c &&
count[stack[stack.length - 1].charCodeAt(0) - 97] > 0) {
const removed = stack.pop();
inStack[removed.charCodeAt(0) - 97] = false;
}
stack.push(c);
inStack[c.charCodeAt(0) - 97] = true;
}
return stack.join('');
};
复杂度分析
| 操作 | 复杂度 |
|---|---|
| 时间复杂度 | O(n),其中 n 是字符串长度。每个字符最多被压入和弹出栈一次 |
| 空间复杂度 | O(1),由于只有 26 个小写字母,所以额外空间是常数级别 |