Remove Duplicate Letters

MediumMonotonic Stack · Greedy🧩 StackLeetCode #316 ↗Updated

Problem

Given a lowercase string s, delete characters so that every distinct letter appears exactly once. Among all such results (each is a subsequence of s), return the lexicographically smallest one.
Input"bcabc"Output"abc"
Input"cbacdcbc"Output"acdb"

1 ≤ s.length ≤ 104  ·  s has only lowercase English letters  ·  Same as LC 1081

Try it yourself

Java
class Solution {
    public String removeDuplicateLetters(String s) {
        
    }
}
Your code is saved in this browser.Submit on LeetCode ↗
Key insight
Build the answer left to right and keep it as small as possible: a bigger letter on top can be popped for a smaller incoming one only if it appears again later, so it can still be placed afterwards.
PatternStack

Instinct Things resolve in reverse order of arrival (brackets, adjacent pairs, nested expressions) - push now, pop when its partner shows up.

Memory hack Last in, first out - like a pile of plates, you only ever touch the one on top; every closer pops its opener.

Open the Stack guide →

Approaches

Brute ForceTry Every Valid Subsequence

Time O(2n·n)Space O(n)

Backtrack over each index: take s[i] (if its letter is unused) or skip it. Whenever the built string has all k distinct letters, compare it with the best so far. Exhaustive, so trivially correct.

Java
private String best;

public String removeDuplicateLetters(String s) {
    best = null;
    int k = (int) s.chars().distinct().count();
    build(s, 0, new StringBuilder(), new boolean[26], k);
    return best;
}

private void build(String s, int i, StringBuilder cur, boolean[] used, int k) {
    if (cur.length() == k) {
        if (best == null || cur.toString().compareTo(best) < 0) best = cur.toString();
        return;
    }
    if (i == s.length()) return;
    int c = s.charAt(i) - 'a';
    if (!used[c]) {                               // take s[i]
        used[c] = true;
        cur.append(s.charAt(i));
        build(s, i + 1, cur, used, k);
        cur.deleteCharAt(cur.length() - 1);
        used[c] = false;
    }
    build(s, i + 1, cur, used, k);                // skip s[i]
}

Correct by definition, but exponential — only usable for tiny inputs.

At a glance

LevelApproachTimeSpace
Brute ForceTry Every Valid SubsequenceO(2n·n)O(n)
BetterGreedy: Pick Smallest Safe First LetterO(k·n)O(k·n)
OptimalMonotonic Stack + Last IndexO(n)O(1)
Common pitfalls
  • Popping a larger top that never appears again — that letter is lost forever.
  • Forgetting the inStack skip — duplicates get pushed or pop valid letters.
  • Not clearing inStack on pop — the letter can never be re-added later.
Interview tips
  • State the greedy rule: drop a larger letter only if a later copy exists.
  • Argue O(n): each character is pushed and popped at most once.
  • Note the stack is bounded by 26, so extra space is O(1).
Pattern
Monotonic stack with removal budget → lexicographically smallest subsequence under constraints.
My notes saved in this browser