LeetCode #316 Medium

Remove Duplicate Letters

Remove Duplicate Letters: delete characters so every letter appears exactly once, and among all such results return the lexicographically smallest.

Constraints
  • 1 <= s.length <= 10⁴
  • s consists of lowercase English letters
stringstackgreedymonotonic stack
Open on LeetCode ↗
Remove Duplicate Letters diagramA labelled diagram of the structure this problem turns on.a placed letter is given up only if BOTH conditions holdincoming < top of stackthe swap lowers the resulttop still occurs laterso nothing is lostANDpop itdrop the second condition and "bac" loses its b permanently —the result becomes invalid, not merely larger
02

Intuition

Reading left to right, a letter already placed should be given up whenever it is larger than the incoming letter and it still occurs later — swapping it out lowers the result and loses nothing. That is a monotonic stack with one extra condition: a letter may only be popped if a future copy exists to replace it. Remaining-count information is therefore as essential as the stack itself.

How to spot this pattern

A monotonic stack applies whenever a result is built left to right and an earlier choice can be revoked once a better one arrives. The extra ingredient here is a feasibility check on the pop. Remove K Digits is the same structure with a pop budget instead of remaining counts.

03

Approach

Try it first

Before reading on: work out the exact condition under which a letter already placed may be given up. Then find the input where popping a larger letter would make the answer invalid rather than smaller.

1

The greedy exchange that makes it lexicographic

Comparing strings of equal length is decided by the earliest differing position, so making an early character smaller always wins, regardless of what follows. If the stack ends with c and the incoming letter is b, then dropping c produces a strictly smaller prefix — provided c appears again later so it can still be included. This exchange argument is what licenses the greedy pop; without a later copy the swap would lose a required letter and the result would be invalid rather than merely larger.

2

Two pieces of state: remaining counts and an in-stack set

Precompute how many times each letter still occurs at or after the current position — a countdown decremented as the scan advances. Also maintain a set of letters currently on the stack. When a letter arrives that is already in the stack, skip it entirely: it is placed, and every valid answer contains exactly one copy. Otherwise pop while the top is larger than the incoming letter and its remaining count is positive, then push. The set prevents duplicates; the counts prevent popping a letter that will never return.

3

Why the result is both valid and minimal

Validity holds because a letter is only popped when a later occurrence is guaranteed, and the set ensures exactly one copy of each distinct letter ends up placed. Minimality follows from the exchange argument applied at every step: the stack is kept as small as possible at each position without sacrificing a needed letter, and any lexicographically smaller string would require making some earlier position smaller, which the greedy already attempted. Each character is pushed and popped at most once, giving O(n) time and O(26) space.

04

Solution & live demo

1from collections import Counter
2 
3 
4class Solution:
5 def removeDuplicateLetters(self, s):
6 remaining = Counter(s)
7 in_stack = set()
8 stack = []
9 for ch in s:
10 remaining[ch] -= 1
11 if ch in in_stack:
12 continue
13 while stack and ch < stack[-1] and remaining[stack[-1]] > 0:
14 in_stack.remove(stack.pop())
15 stack.append(ch)
16 in_stack.add(ch)
17 return "".join(stack)
05

Common pitfalls

Popping without checking the remaining count

✗ Wrong
while stack and ch < stack[-1]:
    in_stack.remove(stack.pop())
✓ Right
while stack and ch < stack[-1] and remaining[stack[-1]] > 0:
    in_stack.remove(stack.pop())

If the letter on top has no later occurrence, popping it removes it from the answer permanently. On "bac" the b would be dropped and never restored, producing an invalid result missing a required letter.

Decrementing the count after the skip

✗ Wrong
if ch in in_stack:
    continue
remaining[ch] -= 1
✓ Right
remaining[ch] -= 1
if ch in in_stack:
    continue

The count must reflect occurrences strictly ahead of the current position for every character, including skipped ones. Decrementing late leaves a stale count that can permit an unsafe pop.

Re-pushing a letter already in the stack

✗ Wrong
stack.append(ch)
✓ Right
if ch in in_stack:
    continue
stack.append(ch)

Each distinct letter must appear exactly once. Without the membership check the stack accumulates repeats and the output violates the problem's core requirement.

06

Edge cases

Already sorted and distinct, e.g. "abc"

No pop ever fires and the string is returned unchanged.

All identical characters, e.g. "aaaa"

The first is pushed and the rest are skipped as already present.

Last occurrence of a larger letter

Its remaining count is 0, so it is not popped even though it exceeds the incoming letter.

Strictly decreasing, e.g. "cba"

Each new letter pops the previous ones when later copies exist.

Single character

Pushed once and returned.

07

Complexity

Time
O(n)
Space
O(1)
Each character is pushed and popped at most once. The stack and counts hold at most 26 letters.