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.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Watch it happen, frame by frame
- The code, line by line
- Variant: Basic Calculator II (LC #227)
- Variant: Backspace String Compare / Text Editor Simulation
- The skeleton for all nested-decode problems
- Where to spot this pattern
- Common traps
- Check yourself
- Practice problems
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:
- The string built so far (before this opening bracket) — you need to append to it after the inner part is expanded.
- 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["processes1first (current_num=1), then2(current_num = 1×10 + 2 = 12). Essential — don't just docurrent_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:
- Decode String (LC #394): the canonical example — push/pop
(string, count). - Basic Calculator (LC #224): parentheses +
+/-— stack of running sums per depth level. - Basic Calculator II (LC #227): no parentheses, all operators — stack-based precedence handling.
- Mini Parser (LC #385): parse a nested list — stack of
NestedIntegerobjects. - 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 ofcurrent_num = current_num * 10 + int(ch)silently breaks on any number ≥ 10. Always accumulate. - Wrong order when restoring:
current_str * repeat + saved_strinstead ofsaved_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_numto 0 after pushing to stack. If you forgetcurrent_num = 0afterstack.append(...), the number bleeds into the next bracket level. - Using string concatenation in a loop for very long outputs. In Python,
current_str += chinside a loop creates a new string every iteration — O(n²) total for long strings. Uselist+"".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:
cur_num * 10 + int(ch)— notint(ch)alone.saved + cur_str * repeat— outer string comes first.- Reset both
cur_strandcur_numto 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
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Decode String | Medium | Push (string, count) on [; restore on ] | LC #394 |
| Basic Calculator | Hard | Stack of partial sums per parenthesis depth | LC #224 |
| Basic Calculator II | Medium | +/- push; *// pop-compute-push | LC #227 |
| Design Browser History | Medium | Two 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).