Problem
Given a string of lowercase letters, remove extra copies of each character so that every letter appears exactly once in the result. Among all strings that satisfy this constraint, return the one that comes first in lexicographic (alphabetical) order.
- Input:
s = "bcabc" - Output:
"abc" - Explanation: every letter appears once, and "abc" is smaller than any other valid arrangement like "bca" or "cab".
Counter-example: s = "cbacdcbc" โ "acdb", not "abcd". The 'd' must appear before the second 'c' because after position 4 (the first 'd') there are no more 'd's left โ so 'd' cannot be pushed to the end.
Intuition
We're building a subsequence of the input that contains each distinct character exactly once, and we want it to be as small lexicographically as possible. The greedy insight: whenever we see a character that is smaller than the most recently placed character, we can evict that larger character from our result โ but only if it still appears somewhere later in the string, giving us a chance to include it again. A monotonic stack tracks this state in a single pass.
Solution โ Greedy Monotonic Stack
Count how many times each character appears in the whole string. Walk left to right: skip characters already placed in the result; otherwise, pop characters from the top of the result that are larger than the current character while they still have future occurrences available. Then place the current character.
- Build a frequency map of every character in the string.
- Maintain a
stack(the growing result) and anin_stackset tracking which characters are already placed. - For each character
c, decrement its remaining count โ this occurrence is now consumed. - If
cis already inin_stack, skip it; it has already been placed at an optimal position. - While the top of the stack is greater than
cand the top character still has future occurrences (remaining_count[top] > 0), pop it from the stack and remove it fromin_stack. - Push
conto the stack and record it inin_stack. - Join the stack into the final answer.
1def removeDuplicateLetters(s: str) -> str:
2 remaining_count = {}
3 for char in s:
4 remaining_count[char] = remaining_count.get(char, 0) + 1
5
6 stack = []
7 in_stack = set()
8
9 for char in s:
10 remaining_count[char] -= 1 # this occurrence is now behind us
11
12 if char in in_stack:
13 continue # already placed at the best earlier position
14
15 # evict larger characters that can be added again later
16 while stack and stack[-1] > char and remaining_count[stack[-1]] > 0:
17 in_stack.discard(stack.pop())
18
19 stack.append(char)
20 in_stack.add(char)
21
22 return "".join(stack)Time: O(n) โ each character is pushed and popped at most once across the entire loop.
Space: O(1) โ the stack and in-stack set hold at most 26 distinct characters regardless of input length.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Greedy Monotonic Stack | O(n) | O(1) | Always โ this is the standard linear solution |
Common Mistakes
- Popping when
remaining_count[top] == 0: if a character has no future occurrences, it must stay in the result โ evicting it means it can never be re-added, producing an incomplete string. - Skipping the
in_stackcheck: without tracking which characters are already placed, the same character can be pushed multiple times, producing duplicates in the output. - Decrementing count after the stack logic: the count must be decremented before checking whether to pop, so that
remaining_countreflects occurrences strictly after the current position. - Checking
remaining_count[char]instead ofremaining_count[stack[-1]]: the pop condition evaluates whether the top character has a future chance to reappear โ not whether the current character does. - Forgetting to update
in_stackwhen popping: if a popped character stays inin_stack, it will be silently skipped on its next occurrence and never placed in the result.
Related Problems
remove-k-digitsโ same greedy monotonic stack pattern applied to digits to minimize a numberlargest-rectangle-in-histogramโ monotonic stack tracks left and right boundaries of each barnext-greater-element-iiโ monotonic stack finds the next larger value in a circular arraydecode-stringโ stack unwinds nested encoded segments during string constructiondaily-temperaturesโ monotonic stack computes the next warmer day for every position