Menu
DSA interview questionsQuestion 179 of 249

DSA interview question · Question 179 of 249

Remove Duplicate Letters: Smallest Order with a Greedy Monotonic Stack

  • Medium
  • coding
  • ~20 min
  • Medium relevance
  • 4 min read
  • Updated Oct 2026

Short answer

Record the last index of each letter. Scan left to right with a stack holding the answer so far and a set of letters in it. Skip a letter already on the stack. Otherwise, while the top of the stack is larger than the current letter and that top letter appears again later, pop it, since you can take it later in a better position. Then push the current letter. Each letter is pushed and popped at most once, so it is O(n) time and O(k) space for k distinct letters. The common bug is popping a letter that will not appear again.

On this page
  1. Problem
  2. Examples
  3. Approach 1: brute force
  4. Approach 2: optimal (greedy monotonic stack)
  5. Tests
  6. Edge cases and pitfalls
  7. Where this shows up in data engineering

Problem

Given a string of lowercase letters, delete characters so that every distinct letter appears exactly once, keeping the remaining characters in their original order. Among all results, return the one that comes first in dictionary order. This is LeetCode 316, Remove Duplicate Letters (LeetCode 1081 asks the same question with different wording).

Examples

"cbacdcbc"  ->  "acdb"
"bcabc"     ->  "abc"
"abacb"     ->  "abc"
"zyx"       ->  "zyx"     (no duplicates: nothing to choose)
"aaaa"      ->  "a"

Approach 1: brute force

Every valid result is a subsequence that uses each distinct letter once. Try every subset of positions, keep the valid ones, and take the smallest. Only usable for very short strings, but it is an honest reference.

from itertools import combinations

def remove_dups_brute(s):
    k = len(set(s))
    best = None
    for idx in combinations(range(len(s)), k):
        t = "".join(s[i] for i in idx)
        if len(set(t)) == k and (best is None or t < best):
            best = t
    return best if best is not None else ""

Complexity: O(C(n, k) · k), exponential in general.

Approach 2: optimal (greedy monotonic stack)

Build the answer from left to right and keep it as small as possible at every step. When the next letter is smaller than the last letter you kept, you would rather have the smaller one earlier, and you can afford to drop the larger one if another copy of it appears later. Keep popping while that holds. A letter already in the answer is skipped: its earlier position is at least as good, since everything after it was chosen to be as small as possible.

def remove_dups(s):
    last = {ch: i for i, ch in enumerate(s)}
    stack, in_stack = [], set()
    for i, ch in enumerate(s):
        if ch in in_stack:
            continue
        while stack and stack[-1] > ch and last[stack[-1]] > i:
            in_stack.remove(stack.pop())
        stack.append(ch)
        in_stack.add(ch)
    return "".join(stack)

Walkthrough on "cbacdcbc":

i char action stack
0 c push c
1 b c > b and c appears later: pop c; push b b
2 a b > a and b appears later: pop b; push a a
3 c push a c
4 d push a c d
5 c already in stack: skip a c d
6 b d > b but d never appears again: stop; push b a c d b
7 c already in stack: skip a c d b

Why it is correct: the stack always holds the smallest possible prefix that can still be completed with every remaining letter. A pop only happens when the removed letter can be picked up later, so feasibility is kept, and placing the smaller letter earlier makes the result strictly smaller in dictionary order.

Complexity: O(n) time (each position is pushed and popped at most once), O(k) space for the stack, set and last-index map.

Tests

import random

for f in (remove_dups, remove_dups_brute):
    assert f("cbacdcbc") == "acdb"
    assert f("bcabc") == "abc"
    assert f("abacb") == "abc"
    assert f("zyx") == "zyx"
    assert f("aaaa") == "a"
    assert f("") == ""
    assert f("bbcaac") == "bac"

random.seed(14)
for _ in range(400):
    s = "".join(random.choice("abcd") for _ in range(random.randint(0, 9)))
    assert remove_dups(s) == remove_dups_brute(s), s
print("ok")

Edge cases and pitfalls

  • Check last[top] > i before popping. Popping a letter whose last copy has already passed loses it from the answer entirely.
  • Skip letters already on the stack; without the set, a later copy can pop letters and create duplicates or a worse order.
  • Sorting the distinct letters ("abcd") is wrong: the result must be a subsequence, so order constraints from the input still apply, as "cbacdcbc" -> "acdb" shows.
  • Remove from the set when you pop, so the letter can be pushed again later.

Where this shows up in data engineering

Deduplicating while keeping order is routine, for example keeping one row per key in arrival order. This problem adds a choice of which copy to keep under a global ordering goal, the kind of reasoning used when you pick which duplicate event to retain so a downstream sorted output or a canonical key is as small or as early as possible. The “pop while it can be recovered later” test is the same safety check you make before discarding a record that might be the only one left.

By DataDank Editorial · Last reviewed Oct 2026 · Python 3 solutions verified with assert-based tests

Progress is saved in this browser only. No account needed.

Search
Filter by type