Learn/DSA Patterns
DSA PatternsStringsmedium10 min read

String Building / Simulation

Some string problems ask you to follow a set of rules step-by-step and produce a result — decode a compressed string, evaluate a nested expression, simulate a text editor. A stack that holds (current_string, pending_count) state is the clean O(n) solution for all nested-structure decoding.

#stack#simulation#decode#strings#nested#recursion#beginner#interview
Table of contents

Before we start

You have used stacks to track matching brackets. This chapter shows a second stack superpower: saving state when entering a nested context and restoring it when leaving. This is how recursion works — and a stack lets you simulate recursion without the function-call overhead. By the end you will be able to:

  • Decode nested repetition strings like "3[a2[bc]]" in O(n) time.
  • Explain exactly what the stack stores and why it needs two lanes (string + count).
  • Adapt the same skeleton to expression evaluation and text-editor simulation.

Picture this first (no code yet)

A real-life story

You are reading a recipe written in a compressed shorthand: "make 3 portions of (2 portions of (bread crumb topping) + sauce)". You start reading, hit the outer "3 portions of (", and immediately face a problem: you can't finish the outer repetition until you know what's inside the brackets.

A chef handles this with a notepad. She writes "3 × [currently building...]" on the notepad and starts fresh on the inner content. When she hits the inner [, she pushes another entry: "2 × [currently building...]". When the inner ] arrives, she looks at her notepad: "ah, I was building 2 copies of 'bread crumb topping'." She writes that repeated string, then pops back to the outer context: "now I continue building the content for the 3× repetition."

That notepad is the stack. Each entry is (string_built_so_far, pending_repeat_count).


The actual problem

Decode String (LC #394):

Given s = "3[a2[bc]]", decode it to produce the repeated string.

"3[a2[bc]]""abcbcabcbcabcbc"

Why? 2[bc]"bcbc". a2[bc]"abcbc". 3[abcbc]"abcbcabcbcabcbc".

The brackets can nest arbitrarily deep. That "arbitrarily deep" is the signal that a stack (or recursion) is the right tool.


First, the slow way (so you feel the pain)

You could try to resolve the innermost brackets first by scanning for the deepest [, expanding it, and repeating. For a string like "2[2[2[a]]]" you would need 3 passes — O(n × depth). For depth 100 and n = 10 000, that is 1 million operations that should be 10 000. And the string grows with each pass, making later passes slower. The stack approach does it in one pass.


The turning point

Pause & think

Before reading the algorithm: what exactly do you need to "remember" when you encounter a [? And what do you need to "restore" when you encounter the matching ]? Write out two things.

Answer

When you hit [, you need to remember:

  1. The string built so far (before this opening bracket) — you need to append to it after the inner part is expanded.
  2. The repeat count (the number before this [) — you need it to repeat the inner result.

When you hit ], you expand: inner = current_string * repeat_count, then restore the saved outer string and append to it: outer + inner.


The one idea to remember

The entire pattern in one sentence

On [: push (current_string, current_count) onto the stack and start fresh (current_string = "", current_count = 0). On ]: pop (saved_string, repeat), set current_string = saved_string + current_string × repeat.


Watch it happen, frame by frame

Input: "3[a2[bc]]"

current_str = ""    current_num = 0    stack = []

ch='3': digit → current_num = 3
ch='[': push ("", 3) → stack=[("", 3)]. Reset: current_str="", current_num=0
ch='a': letter → current_str = "a"
ch='2': digit → current_num = 2
ch='[': push ("a", 2) → stack=[("",3),("a",2)]. Reset: current_str="", current_num=0
ch='b': current_str = "b"
ch='c': current_str = "bc"
ch=']': pop ("a", 2). current_str = "a" + "bc" × 2 = "a" + "bcbc" = "abcbc"
ch=']': pop ("", 3). current_str = "" + "abcbc" × 3 = "abcbcabcbcabcbc"

Answer: "abcbcabcbcabcbc"  ✅

Every character is touched exactly once. O(n) time.


The code, line by line

def decodeString(s: str) -> str:
    stack       = []     # each entry: (string_built_before_this_bracket, repeat_count)
    current_str = ""     # string being built at current depth
    current_num = 0      # number being accumulated (may be multi-digit: "12[a]")

    for ch in s:
        if ch.isdigit():
            current_num = current_num * 10 + int(ch)   # handle multi-digit numbers
        elif ch == '[':
            stack.append((current_str, current_num))   # save state
            current_str = ""                           # start fresh inside bracket
            current_num = 0
        elif ch == ']':
            saved_str, repeat = stack.pop()            # restore state
            current_str = saved_str + current_str * repeat
        else:
            current_str += ch                          # regular character

    return current_str
  • current_num * 10 + int(ch) — multi-digit support. "12[" processes 1 first (current_num=1), then 2 (current_num = 1×10 + 2 = 12). Essential — don't just do current_num = int(ch).
  • stack.append((current_str, current_num)) — saves the outer context before diving into the inner bracket.
  • current_str = saved_str + current_str * repeat — "the outer string so far" + "the inner string repeated count times." The order matters: outer comes first.

Pause & think

What happens if the input is "10[a]"? Trace through the digit handling. What would be wrong if you used current_num = int(ch) instead of current_num = current_num * 10 + int(ch)?

Answer

"10[a]" should produce "aaaaaaaaaa" (10 a's). With the correct formula:

  • ch='1': current_num = 0 * 10 + 1 = 1
  • ch='0': current_num = 1 * 10 + 0 = 10

With the naive current_num = int(ch):

  • ch='1': current_num = 1
  • ch='0': current_num = 0 ← overwrites! Then "[a]" would repeat 0 times → "". Completely wrong.

Always accumulate digits with the * 10 + digit formula.


Variant: Basic Calculator II (LC #227)

Evaluate a string like "3+2*2" or " 3/2 " following standard operator precedence (no parentheses).

def calculate(s: str) -> int:
    stack  = []
    num    = 0
    sign   = '+'     # the pending operator before the current number

    for i, ch in enumerate(s):
        if ch.isdigit():
            num = num * 10 + int(ch)

        if (not ch.isdigit() and ch != ' ') or i == len(s) - 1:
            # time to apply the pending sign
            if sign == '+':   stack.append(num)
            elif sign == '-': stack.append(-num)
            elif sign == '*': stack.append(stack.pop() * num)
            elif sign == '/': stack.append(int(stack.pop() / num))  # truncate toward 0
            sign = ch
            num  = 0

    return sum(stack)

The insight: + and - push numbers onto the stack (deferred addition). * and / apply immediately by popping and pushing (they bind tighter). At the end, sum everything on the stack.

Frame-by-frame: "3+2*2"

ch='3': num=3
ch='+': sign was '+' → push 3. stack=[3]. sign='+', num=0
ch='2': num=2
ch='*': sign was '+' → push 2. stack=[3,2]. sign='*', num=0
ch='2': num=2 (last char)
  end:  sign was '*' → stack.pop()=2, push 2*2=4. stack=[3,4]

sum([3,4]) = 7  ✅

Variant: Backspace String Compare / Text Editor Simulation

A stack naturally simulates a text editor with backspace (#):

def processTyped(typed: str) -> str:
    stack = []
    for ch in typed:
        if ch == '#':
            if stack: stack.pop()    # backspace — delete last character
        else:
            stack.append(ch)
    return "".join(stack)

This is O(n) time and O(n) space. Every character is pushed once and popped at most once.


The skeleton for all nested-decode problems

stack = []
current_context = initial_state

for ch in s:
    if ch is "open" signal:
        stack.append(current_context)    # save outer state
        current_context = fresh_state    # start inner context
    elif ch is "close" signal:
        outer = stack.pop()              # restore outer state
        current_context = combine(outer, current_context)
    else:
        update current_context with ch

The specific types of context, fresh_state, and combine depend on the problem. For Decode String: context = (string, count), combine = outer_string + inner_string * count.


Where to spot this pattern

Trigger words:

  • "decode" with repeated/nested structure like "3[a2[bc]]"
  • "evaluate expression" with parentheses or operator precedence
  • "simulate" a process step by step (text editor, robot path)
  • "nested" — any nesting signals a stack-based approach

5 disguises:

  1. Decode String (LC #394): the canonical example — push/pop (string, count).
  2. Basic Calculator (LC #224): parentheses + +/- — stack of running sums per depth level.
  3. Basic Calculator II (LC #227): no parentheses, all operators — stack-based precedence handling.
  4. Mini Parser (LC #385): parse a nested list — stack of NestedInteger objects.
  5. Robot Return to Origin (LC #657): simulate moves, track position. Simple counter — no stack needed. Contrast to know when stack is necessary.

Common traps

Watch out for these

  • Single-digit assumption. Using current_num = int(ch) instead of current_num = current_num * 10 + int(ch) silently breaks on any number ≥ 10. Always accumulate.
  • Wrong order when restoring: current_str * repeat + saved_str instead of saved_str + current_str * repeat. The outer string comes before the inner expanded string — the brackets appear after whatever was written before them.
  • Not resetting current_num to 0 after pushing to stack. If you forget current_num = 0 after stack.append(...), the number bleeds into the next bracket level.
  • Using string concatenation in a loop for very long outputs. In Python, current_str += ch inside a loop creates a new string every iteration — O(n²) total for long strings. Use list + "".join() for production code; for interviews, += is acceptable unless explicitly asked about performance.

Remember this forever

Decode String — stack skeleton

stack = [];  cur_str = "";  cur_num = 0

for ch in s:
    if ch.isdigit():
        cur_num = cur_num * 10 + int(ch)     # multi-digit!
    elif ch == '[':
        stack.append((cur_str, cur_num))
        cur_str, cur_num = "", 0
    elif ch == ']':
        saved, repeat = stack.pop()
        cur_str = saved + cur_str * repeat   # outer THEN inner×repeat
    else:
        cur_str += ch

Key traps:

  1. cur_num * 10 + int(ch) — not int(ch) alone.
  2. saved + cur_str * repeat — outer string comes first.
  3. Reset both cur_str and cur_num to clean state on [.

Check yourself

Trace `"2[abc]3[cd]ef"` step by step. What is the final output?
cur="", num=0, stack=[]

'2': num=2
'[': push("",2). cur="", num=0
'a': cur="a"
'b': cur="ab"
'c': cur="abc"
']': pop("",2). cur = "" + "abc"*2 = "abcabc"
'3': num=3
'[': push("abcabc",3). cur="", num=0
'c': cur="c"
'd': cur="cd"
']': pop("abcabc",3). cur = "abcabc" + "cd"*3 = "abcabccdcdcd"
'e': cur="abcabccdcdcde"
'f': cur="abcabccdcdcdef"

Output: "abcabccdcdcdef"
In Basic Calculator II, why are `*` and `/` applied immediately (by popping and pushing) while `+` and `-` are deferred (just pushing the number)?

Operator precedence: * and / bind more tightly than + and -. When we see *, we know the number to its left (on the stack) and the number to its right (current num) must multiply — nothing between them can separate them. So we apply the operation immediately. + and - have lower precedence — a later * or / might "steal" the right-hand number away. By pushing them deferred onto the stack, we let higher-precedence operators resolve first; at the end, summing the stack gives the correct result because all that remains are additive contributions.


Practice problems

ProblemDifficultyWhat to noticeLink
Decode StringMediumPush (string, count) on [; restore on ]LC #394
Basic CalculatorHardStack of partial sums per parenthesis depthLC #224
Basic Calculator IIMedium+/- push; *// pop-compute-pushLC #227
Design Browser HistoryMediumTwo stacks (back / forward)LC #1472

Next up: Anagram Pattern — why two strings are anagrams, three ways to check it, and how sliding-window anagram detection works in O(n).