力扣(Leetcode)相关题目链接:
给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。
需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
关键点:
- 每个字母只能出现一次。
- 字典序最小:就是要确保最终结果尽量按照字母顺序排列(如 'a' < 'b' < 'c')。
解题思路:
我们使用单调栈和贪心策略来解决问题。通过不断检查当前字符和栈顶字符,决定是否弹出栈顶字符来获得更小的字典序。
算法步骤:
-
记录每个字母最后出现的位置:
我们需要知道某个字母是否还会在后面出现,以便决定是否可以移除当前栈顶字符并在后面重新添加它。 -
使用栈维护一个字典序最小的序列:
使用栈来存储处理后的字符,栈中的字符始终是去重的,并且字典序最小。 -
贪心策略:
如果当前字符比栈顶字符字典序小,并且栈顶字符在后面还会出现,那么我们可以弹出栈顶字符以保证字典序更小。
逐步推导示例:
我将以字符串 s = "cbacdcbc"
为例,详细推导过程。
-
初始化:
- 记录每个字符的最后出现位置:
last_occurrence = {'c': 7, 'b': 6, 'a': 2, 'd': 4}
- 定义一个栈
stack
和一个集合seen
,用来存储栈中的字符是否已经存在。 - 集合seen是必要的吗?仅仅使用栈本身来判断字符是否已经在栈中也可以,但这需要遍历整个栈来检查(时间复杂度为 O(n))。使用
seen
集合 可以将字符是否已经存在的判断操作从 O(n) 降低到 O(1)(集合的查询时间复杂度为 O(1))。
- 记录每个字符的最后出现位置:
-
逐字符处理:
Step 1: 处理字符 'c'
- 栈是空的,直接将
'c'
入栈。 stack = ['c']
seen = {'c'}
Step 2: 处理字符 'b'
'b'
小于栈顶字符'c'
,并且'c'
之后还会出现(last_occurrence['c'] = 7
)。- 因此,我们弹出栈顶的
'c'
,将'b'
入栈。 stack = ['b']
seen = {'b'}
Step 3: 处理字符 'a'
'a'
小于栈顶字符'b'
,并且'b'
之后还会出现(last_occurrence['b'] = 6
)。- 因此,我们弹出栈顶的
'b'
,将'a'
入栈。 stack = ['a']
seen = {'a'}
Step 4: 处理字符 'c'
'c'
大于栈顶字符'a'
,可以直接入栈。stack = ['a', 'c']
seen = {'a', 'c'}
Step 5: 处理字符 'd'
'd'
大于栈顶字符'c'
,可以直接入栈。stack = ['a', 'c', 'd']
seen = {'a', 'c', 'd'}
Step 6: 处理字符 'c'
'c'
已经在栈中(seen
集合里),直接跳过。
Step 7: 处理字符 'b'
'b'
小于栈顶字符'd'
,但'd'
之后不会再出现了(last_occurrence['d'] = 4
)。- 所以不能弹出栈顶元素
'd'
直接将'b'
入栈。 stack = ['a', 'c', 'd', 'b']
seen = {'a', 'c', 'd', 'b'}
Step 8: 处理字符 'c'
'c'
已经在栈中,直接跳过。
最终栈中的字符:
stack = ['a', 'c', 'd', 'b']
- 最终的结果是
'acdb'
。
完整代码
栈+集合(哈希表)版本
class Solution:
def removeDuplicateLetters(self, s: str) -> str:
# 记录每个字符最后出现的位置
last_occurrence = {char: i for i, char in enumerate(s)}
stack = [] # 单调栈
seen = set() # 用于判断字符是否已经在栈中
for i, char in enumerate(s):
# 如果字符已经在栈中,跳过
if char in seen:
continue
# 栈顶字符字典序大于当前字符,并且栈顶字符在后面还会出现,移除栈顶字符
# last_occurrence[stack[-1]] 表示栈顶元素最后出现的位置
while stack and char < stack[-1] and i < last_occurrence[stack[-1]]:
seen.remove(stack.pop())
# 将当前字符压入栈,并加入到 seen 集合中
stack.append(char)
seen.add(char)
# 最终栈中的字符即为答案,转为字符串返回
return ''.join(stack)
只要栈的版本
class Solution:
def removeDuplicateLetters(self, s: str) -> str:
# 记录每个字符最后出现的位置
last_occurrence = {char: i for i, char in enumerate(s)}
stack = [] # 单调栈
for i, char in enumerate(s):
# 如果字符已经在栈中,跳过(通过栈检查)
if char in stack:
continue
# 栈顶字符字典序大于当前字符,并且栈顶字符在后面还会出现,移除栈顶字符
while stack and char < stack[-1] and i < last_occurrence[stack[-1]]:
stack.pop()
# 将当前字符压入栈
stack.append(char)
# 最终栈中的字符即为答案,转为字符串返回
return ''.join(stack)