文章目录
摘要
这题看起来像是处理字符串的常规操作:“把重复的字母删掉就好了嘛。”——但实际上,一旦你加上“字典序最小”这个条件,事情就不简单了。
LeetCode 316《去除重复字母》是一道结合了贪心算法和单调栈思想的经典题,不仅考察你对字符串的操作熟练度,还考验你对“相对顺序 + 字典序”的理解。
本文将用 Swift 带你一行行拆解这题,并配合完整 Demo 和现实场景分析,带你掌握这类“按规则选字符”的高频技巧。
描述
题目原文如下:
给你一个字符串
s
,去除其中重复的字母,使得每个字母只出现一次,并且返回结果的字典序最小。你必须保证字符的相对位置不能打乱。
举几个例子:
-
输入:
"bcabc"
输出:"abc"
-
输入:
"cbacdcbc"
输出:"acdb"
注意点:
- 要去重。
- 相对顺序不能乱。
- 最终结果字典序要最小(也就是“越靠前越小”的字母尽量放前面)。
题解答案
如果不考虑顺序和字典序,我们可以直接用 Set<Character>
来去重。但这题麻烦在于:
- 要“只出现一次”
- 要保证结果的字典序最小(比如 “cbacdcbc” 的最优解是 “acdb”)
- 字母相对顺序不能乱
这时候,我们就得引入一种经典策略:单调栈 + 贪心算法。
基本思路是:
- 从左到右扫描字符串;
- 每次处理一个字符,决定是否放进结果栈中;
- 如果当前字符比栈顶字符字典序小,并且栈顶字符后面还会出现,那就把栈顶弹出;
- 最终栈中的字符就是去重后、字典序最小、相对顺序合理的字符串。
题解代码分析(Swift 实现)
func removeDuplicateLetters(_ s: String) -> String {
var stack: [Character] = []
var seen: Set<Character> = []
var lastIndex: [Character: Int] = [:]
let chars = Array(s)
// 记录每个字符最后出现的位置
for (i, ch) in chars.enumerated() {
lastIndex[ch] = i
}
for (i, ch) in chars.enumerated() {
// 如果当前字符已经在结果中,就跳过
if seen.contains(ch) {
continue
}
// 如果当前字符比栈顶更小,并且栈顶后面还会再出现,就弹出栈顶
while let last = stack.last,
ch < last,
lastIndex[last]! > i {
stack.removeLast()
seen.remove(last)
}
stack.append(ch)
seen.insert(ch)
}
return String(stack)
}
题解代码分析
让我们一步步来拆解上面这段代码在干什么:
记录字符的最后出现位置
for (i, ch) in chars.enumerated() {
lastIndex[ch] = i
}
这一段的目的是为了让我们在后续操作中判断:当前字符后面还会不会再出现,如果会,我们可以“大胆删”。
主循环逻辑(贪心 + 栈)
if seen.contains(ch) {
continue // 已处理过,不重复加入
}
这是保证每个字符只出现一次的关键。
接下来是“贪心删栈顶”的逻辑:
while let last = stack.last,
ch < last,
lastIndex[last]! > i {
stack.removeLast()
seen.remove(last)
}
意思是:
- 如果当前字符
ch
字典序更小, - 栈顶的字符还会在后面出现,
- 那么我们可以把栈顶删掉,为字典序让路。
比如 “cbacdcbc”,我们走到 a
时,栈顶是 c
,但 c
还会再出现,所以 a
可以把 c
替掉。
加入栈和记录
stack.append(ch)
seen.insert(ch)
这就是把当前合法的字符加入结果栈,同时标记它已经被用了。
示例测试及结果
我们写几个测试例子,来看下效果:
示例 1
let result1 = removeDuplicateLetters("bcabc")
print(result1) // 输出:"abc"
解释:
b
→ 入栈c
→ 入栈a
→ 比c
小,而且c
后面还会出现,所以把c
弹出,再把a
入栈- 后面遇到重复的就跳过
最终是 "abc"
示例 2
let result2 = removeDuplicateLetters("cbacdcbc")
print(result2) // 输出:"acdb"
解释过程如下:
c
入栈b
入栈a
比b
小,b
后面还有,弹出;c
后面还有,也弹出;然后a
入栈- 后面依次加入
c
、d
、b
,但跳过重复字符
最终得到 "acdb"
时间复杂度
- 遍历一次字符串:
O(n)
- 每个字符最多只会被压栈和出栈一次:
O(n)
- 所以总体时间复杂度是
O(n)
空间复杂度
lastIndex
和seen
使用了常数空间(最多 26 个字母):O(1)
- 栈最多也放 26 个字母:
O(1)
所以虽然用了多个数据结构,但总体空间复杂度也是 O(1)
(和输入字符集大小有关,输入字符集为小写英文字母)。
总结
这道题是典型的“处理字符串顺序 + 优化字典序”的组合题,在面试中非常常见。
你不仅需要理解“字典序”的含义,还需要:
- 学会用贪心策略找“更优顺序”;
- 能熟练使用 栈 和 Set 组合去控制结果;
- 处理“重复元素”时保证只选一次。