Problem Walkthrough

Removing Stars from String LeetCode 2390

LeetCode 2390 Removing Stars from a String simulates a backspace key using a stack: iterate each character and push non-star characters onto the stack; when you encounter a star, pop the top of the stack to remove the nearest non-star character to the left; join the stack at the end to produce the result string after all deletions.

7 min read|

Removing Stars

LeetCode 2390

Problem Overview

LeetCode 2390 — Removing Stars from a String — gives you a string containing lowercase letters and star characters. Each star removes the closest non-star character immediately to its left. After processing all stars, you return the remaining string with no stars in it. The problem guarantees that every star always has at least one non-star character to its left, so you never need to handle an invalid deletion.

The key insight is that stars act as a backspace key on whatever has been built so far. A star does not remove the character at a fixed offset — it removes the most recently added character that has not already been deleted by a previous star. This "most recently added" property is the hallmark of a stack (Last-In, First-Out).

The problem is solvable in a single left-to-right pass with no look-ahead. The result is always deterministic because the order of deletions is fully determined by the left-to-right traversal order.

  • Input: string s with lowercase letters and stars; Output: s after removing all stars and their targets
  • Each star removes the nearest non-star character to its left
  • Guaranteed valid input — every star always has a non-star character to remove
  • Stars are not included in the output; only surviving non-star characters remain
  • Constraints: 1 <= s.length <= 10^5; s contains only lowercase letters and stars; always valid

Stack Simulation

The stack simulation iterates the string left to right one character at a time. For each character, apply a simple rule: if the character is not a star, push it onto the stack; if the character is a star, pop the top element from the stack. After processing all characters, join the stack into a string and return it.

Pushing a non-star character records that character as part of the current "typed" result. Popping on a star undoes the most recent addition — exactly the semantics of the nearest-left-non-star deletion rule. Because the stack always holds the characters that have survived all deletions so far, joining it at the end directly produces the answer.

This approach handles every edge case automatically. Multiple consecutive stars each pop once, removing two characters from the stack. Stars at the beginning of the string are impossible (guaranteed valid input). A string with no stars at all simply fills the stack with every character and joins to the original string.

💡

Stars Are Backspace Keys — Non-Stars Are Keystrokes

This problem is identical to backspace string simulation: treat every non-star character as pressing a key on a keyboard and every star as pressing the Backspace key. The stack naturally tracks what is currently visible on screen after every keystroke and deletion. This mental model makes the algorithm immediately intuitive and easy to recall under interview pressure.

Why a Stack Is Perfect for This Problem

A stack is the ideal data structure here because the deletion rule is LIFO: a star always removes the most recently added surviving character, not any arbitrary character. This is the defining property of a stack. Stack pop gives you the most recently pushed item in O(1) time without searching.

If you tried to solve this with an index into the original string, you would need to track which characters have been deleted and skip over them when looking left for the next surviving character — adding complexity. The stack eliminates that bookkeeping entirely: whatever is on top is always the nearest surviving character to the left.

The stack approach also makes the algorithm's correctness trivially obvious. After each push or pop, the stack invariant is: "the stack contains exactly the surviving characters from the portion of the string processed so far, in original left-to-right order." No additional reasoning is needed.

  1. 1Initialize an empty list/stack
  2. 2For each character ch in s:
  3. 3 If ch != "*": stack.append(ch) — push the character
  4. 4 If ch == "*": stack.pop() — remove nearest left non-star
  5. 5Return "".join(stack) — surviving characters in order

Connection to Backspace String Compare (LeetCode 844)

LeetCode 844 — Backspace String Compare — uses the exact same core mechanic: a hash "#" character acts as a backspace, deleting the previous character. The difference is that LeetCode 844 asks you to compare two processed strings for equality, while LeetCode 2390 asks you to return the processed string. The stack template that solves 2390 directly solves 844 as well — just run it on both input strings and compare the results.

Recognizing this family of problems — "character + delete signal" processed left to right — is a valuable pattern to internalize. Any time a problem describes a character that removes or cancels the character before it, reach for a stack. The delete signal (star, hash, or another marker) always maps to a pop, and every other character maps to a push.

Both problems are O(n) time and O(n) space. LeetCode 844 has a follow-up asking for O(1) space using two pointers scanning right to left, but that optimization applies to the comparison version and not to 2390 which must reconstruct the full output string.

ℹ️

One Stack Template, Two LeetCode Problems

Once you recognize the "character + delete" pattern, you can solve both LeetCode 2390 and LeetCode 844 with the same stack template in under 5 minutes. Push non-delete characters, pop on delete signals, join or compare at the end. Knowing this family saves you from re-deriving the approach from scratch on each problem — just identify the delete signal and apply the template.

Walk-Through Example

Trace through "leet**cod*e" step by step. The string has 11 characters: l, e, e, t, *, *, c, o, d, *, e. Start with an empty stack [].

Process l → push → [l]. Process e → push → [l, e]. Process e → push → [l, e, e]. Process t → push → [l, e, e, t]. Process * → pop t → [l, e, e]. Process * → pop e → [l, e]. Process c → push → [l, e, c]. Process o → push → [l, e, c, o]. Process d → push → [l, e, c, o, d]. Process * → pop d → [l, e, c, o]. Process e → push → [l, e, c, o, e].

Join the stack: "lecoe". This is the correct answer. Notice that the two consecutive stars at positions 4-5 removed t then e (the two most recently pushed characters), and the star at position 9 removed d immediately after it was pushed. The stack cleanly handles all three deletions without any index arithmetic.

  1. 1"l" → push → stack: [l]
  2. 2"e" → push → stack: [l, e]
  3. 3"e" → push → stack: [l, e, e]
  4. 4"t" → push → stack: [l, e, e, t]
  5. 5"*" → pop t → stack: [l, e, e]
  6. 6"*" → pop e → stack: [l, e]
  7. 7"c" → push → stack: [l, e, c]
  8. 8"o" → push → stack: [l, e, c, o]
  9. 9"d" → push → stack: [l, e, c, o, d]
  10. 10"*" → pop d → stack: [l, e, c, o]
  11. 11"e" → push → stack: [l, e, c, o, e]
  12. 12join → "lecoe"

Code Walkthrough — Python and Java

Python implementation: use a list as the stack. stack = []; for ch in s: if ch != "*": stack.append(ch); else: stack.pop(); return "".join(stack). Time complexity: O(n) — each character is pushed at most once and popped at most once. Space complexity: O(n) for the stack. A Python one-liner using functools.reduce is also possible — reduce(lambda acc, ch: acc[:-1] if ch == "*" else acc + ch, s, "") — but this builds intermediate strings and is O(n^2) in the worst case due to string concatenation; avoid it for large inputs.

Java implementation: use a StringBuilder as a stack surrogate. StringBuilder sb = new StringBuilder(); for (char ch : s.toCharArray()) { if (ch != '*') { sb.append(ch); } else { sb.deleteCharAt(sb.length() - 1); } } return sb.toString(). StringBuilder.deleteCharAt operates on an internal char array, making pop O(1) amortized. Return sb.toString() for the final result. Alternatively, use a Deque<Character> as an explicit stack and collect the result in order.

Both implementations are single-pass O(n) solutions. The Python list and the Java StringBuilder both support O(1) amortized append and O(1) remove-from-end, making them equivalent in performance. The key is to avoid any solution that builds the result using string concatenation in a loop, which degrades to O(n^2).

⚠️

Use a List or StringBuilder — Not String Concatenation in a Loop

Building the result by concatenating to a plain string inside a loop is O(n^2) in many languages because each concatenation copies the entire accumulated string. Use a list with append and join in Python, or a StringBuilder in Java. Both give O(n) overall. This is a common performance trap on string-building problems and is worth explicitly calling out in an interview.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now