AlgoMaster Logo

Remove Duplicate Letters

mediumFrequency6 min readUpdated June 23, 2026

Understanding the Problem

The phrase "remove duplicates" makes this sound easy, but the lexicographical ordering requirement is what makes it hard. We cannot keep the first occurrence of each letter, because that might not give the smallest result. In "cbabc", first occurrences give "cba", but the smallest valid answer is "abc".

The constraints define what counts as a valid answer. We select exactly one occurrence of each distinct character, and the selected characters keep their relative order from the original string, so the result is a subsequence. Among all subsequences that contain every distinct letter exactly once, we want the lexicographically smallest.

This combines a greedy choice with a monotonic stack. We build the result left to right and remove a larger character we already placed whenever a smaller one comes along, provided the larger character appears again later so we can recover it.

Key Constraints:

  • 1 <= s.length <= 10^4: An O(n^2) approach reaches 10^8 operations, which is borderline. An O(n) solution is comfortable.
  • s consists of lowercase English letters: there are only 26 possible characters, so a frequency array, a last-index array, and an in-stack array are all constant size.

Approach 1: Brute Force (Generate All Subsequences)

Intuition

Generate every subsequence that contains each distinct character exactly once, then pick the lexicographically smallest one.

If the string has k distinct characters, we choose one position for each character. A character may appear at several positions, so there are multiple choices per character. We try every combination, read each one in index order to form a candidate string, and keep the smallest.

This is simple to reason about but inefficient. If each character appears up to m times and there are k distinct characters, the number of combinations is up to m^k.

Algorithm

  1. Find all distinct characters in the string and the list of positions where each appears.
  2. Generate every way to pick one position for each distinct character.
  3. For each combination, sort the chosen positions in increasing order and read the characters in that order to form a candidate string.
  4. Return the lexicographically smallest candidate.

Example Walkthrough

1Input: s = "bcabc", distinct chars: {a, b, c}
0
b
1
c
2
a
3
b
4
c
1/4

Code

The brute force explores an exponential number of combinations and discards almost all of them. The next approach builds the answer in a single pass, making a greedy choice at each character that is provably optimal.

Approach 2: Greedy with Monotonic Stack

Intuition

We build the result one character at a time, keeping it as a subsequence, and want the earliest positions of the result to hold the smallest characters possible.

While scanning left to right, we hold the result so far on a stack. When the current character is smaller than the character on top of the stack, removing the top would make the result lexicographically smaller. That removal is safe only when the top character appears again later in the string, since we can add it back when we reach that later occurrence. If the top character does not appear again, removing it would drop it from the answer entirely, so we leave it in place.

A monotonic stack captures this rule directly: pop the top while it is larger than the current character and appears later. To keep each character exactly once, we skip the current character if it is already on the stack.

Algorithm

  1. Count the frequency of each character in the string (or record the last index of each character).
  2. Initialize an empty stack and a boolean array tracking which characters are currently in the stack.
  3. Iterate through each character in the string:
    • If the character is already in the stack, skip it.
    • Otherwise, while the stack is not empty, and the top of the stack is greater than the current character, and the top character appears later in the string (last index > current index), pop the top and mark it as not in the stack.
    • Push the current character onto the stack and mark it as in the stack.
  4. The stack contains the answer.

Example Walkthrough

1Initialize: scan string left to right, stack is empty
0
c
i
1
b
2
a
3
c
4
d
5
c
6
b
7
c
1/9

Code