Problem Overview
LeetCode 394 — Decode String — presents an encoding scheme where a number immediately before a pair of brackets means the enclosed substring should be repeated that many times. For example, "3[a2[c]]" decodes to "accaccacc": the inner "2[c]" decodes to "cc", so the bracket contents become "acc", and "3[acc]" becomes "accaccacc". The encoding is well-formed — brackets are always matched and numbers are always positive integers.
The key challenge is nesting. Brackets can appear inside other brackets to arbitrary depth, and the outer repeat count applies to the fully decoded inner string, not the raw encoded form. This makes a purely iterative character-by-character approach with no auxiliary state insufficient — you need a way to save and restore context as you enter and exit each bracket level.
This problem is a canonical example of the stack-as-context-save pattern used across many bracket-parsing problems. Understanding it deeply equips you to handle basic calculator, evaluate reverse polish notation, and any grammar with nested recursive structure.
- Input is a valid encoded string; output is the fully decoded string
- Encoding: k[s] means repeat string s exactly k times; k is always a positive integer
- Brackets can nest: "2[a3[b]]" → "abbbabbb"
- No spaces in input; letters are always lowercase; digits always precede brackets
- Constraints: 1 <= s.length <= 30; s contains digits, letters, and brackets; always valid encoding
Stack-Based Approach
The stack-based solution tracks two running variables: currentString (the string being built for the current bracket level) and currentNum (the number accumulated from consecutive digit characters). As you scan each character, you update these variables or interact with the stack depending on what you see.
When you encounter an opening bracket "[", you push the pair (currentString, currentNum) onto the stack and reset both variables to empty string and zero. This saves your context before descending into the nested level. When you encounter a closing bracket "]", you pop (prevString, count) from the stack and set currentString = prevString + currentString * count. This restores the outer context and appends the fully decoded inner result repeated the correct number of times.
Digits accumulate into currentNum using num = num*10 + int(ch) to handle multi-digit repeat counts. Letters append directly to currentString. The stack depth equals the nesting depth of the input; at the end, currentString holds the final decoded result.
- 1Initialize currentString = "" and currentNum = 0
- 2For each character: if digit, update currentNum = currentNum*10 + int(ch)
- 3If letter, append to currentString
- 4If "[": push (currentString, currentNum) to stack; reset currentString = "" and currentNum = 0
- 5If "]": pop (prevString, count); currentString = prevString + currentString * count
- 6Return currentString after scanning all characters
The Stack Saves Your Context Before Each Bracket
Think of the stack as a save-game slot for your current position. When you hit "[", you snapshot your progress (what you had built so far, and how many times to repeat) and start fresh. When you hit "]", you restore that snapshot and append the repeated result of the nested work. Each stack frame is one level of nesting.
Processing Each Character
The algorithm processes the input string left to right one character at a time. There are exactly four cases: the character is a digit, a lowercase letter, an opening bracket, or a closing bracket. Each case has a simple, focused action — there is no look-ahead needed.
Digit handling is the subtlest case. Because repeat counts can be multi-digit (e.g., "12[a]" means twelve repetitions), you cannot treat each digit character as a standalone number. Instead, you accumulate with num = num * 10 + int(ch), building the full integer before you see the "[". A fresh scan of consecutive digit characters before each bracket produces the correct multi-digit count.
Letter handling is the simplest: append the character to currentString. This covers all non-digit, non-bracket characters in a valid input. The four cases are exhaustive for valid input, so a simple if/elif/else chain or switch handles all logic without edge case branching beyond these four.
- 1ch is digit (0-9): currentNum = currentNum * 10 + int(ch) — accumulate multi-digit count
- 2ch is letter (a-z): currentString += ch — build the current level's string
- 3ch is "[": stack.push((currentString, currentNum)); currentString = ""; currentNum = 0
- 4ch is "]": prevString, count = stack.pop(); currentString = prevString + currentString * count
Recursive Alternative
The recursive approach treats the encoded string as a context-free grammar and decodes it by consuming characters through a global index. A function decode() reads characters starting at the current index. When it encounters digits, it reads the full number. When it encounters "[", it recursively calls decode() to decode the inner string, then repeats it. When it encounters a letter, it appends to the current result. When it encounters "]" or reaches the end of input, it returns the current result and the current index.
The recursion depth equals the nesting depth of the input. Each recursive call handles exactly one bracket level, returning both the decoded string for that level and the position in the input where that level ended. The caller uses the returned index to continue scanning after the closing bracket.
The iterative stack solution and the recursive solution are functionally identical — the call stack in the recursive version plays the same role as the explicit stack in the iterative version. The iterative approach is preferred in production code to avoid stack overflow on deeply nested inputs, but the recursive version is often cleaner to reason about during interviews.
Recursion Mirrors the Grammar Structure
The recursive approach mirrors the formal grammar: Expr → (Digit+ "[" Expr "]")* Letter*. Each bracketed group is a subproblem returning its decoded string. The iterative stack simulates this call stack explicitly. Both have the same O(n * maxRepeat) time complexity — choose whichever you can implement and explain more confidently in an interview.
Walk-Through Example
Trace through "3[a2[c]]" step by step. Characters: "3", "[", "a", "2", "[", "c", "]", "]". Start: currentString="", currentNum=0, stack=[].
"3": digit → currentNum=3. "[": push ("", 3); reset currentString="", currentNum=0. "a": letter → currentString="a". "2": digit → currentNum=2. "[": push ("a", 2); reset currentString="", currentNum=0. "c": letter → currentString="c". "]": pop ("a", 2); currentString = "a" + "c"*2 = "a"+"cc" = "acc". "]": pop ("", 3); currentString = "" + "acc"*3 = "accaccacc".
Final currentString = "accaccacc" — the correct decoded output. Note how the inner "]" resolved the "2[c]" subproblem first (producing "cc"), then the outer "]" resolved "3[a...]" using the already-decoded inner result. The stack ensures inner brackets are always resolved before outer ones.
- 1"3" → currentNum=3
- 2"[" → push ("", 3); currentString="", currentNum=0
- 3"a" → currentString="a"
- 4"2" → currentNum=2
- 5"[" → push ("a", 2); currentString="", currentNum=0
- 6"c" → currentString="c"
- 7"]" → pop ("a", 2); currentString = "a" + "c"*2 = "acc"
- 8"]" → pop ("", 3); currentString = "" + "acc"*3 = "accaccacc"
Code Walkthrough — Python and Java
Python implementation: stack = []; currentString = ""; currentNum = 0; then iterate each ch in s. If ch.isdigit(): currentNum = currentNum*10 + int(ch). Elif ch.isalpha(): currentString += ch. Elif ch == "[": stack.append((currentString, currentNum)); currentString = ""; currentNum = 0. Else (ch == "]"): prevString, count = stack.pop(); currentString = prevString + currentString * count. Return currentString. Time complexity: O(n * maxRepeat) where n is output length; space: O(n) for the stack and strings.
Java implementation: Deque<Object[]> stack = new ArrayDeque<>(); StringBuilder currentString = new StringBuilder(); int currentNum = 0; then iterate each char ch. If Character.isDigit(ch): currentNum = currentNum*10 + (ch-'0'). Else if Character.isLetter(ch): currentString.append(ch). Else if ch == '[': stack.push(new Object[]{currentString.toString(), currentNum}); currentString = new StringBuilder(); currentNum = 0. Else (ch == ']'): Object[] top = stack.pop(); String prev = (String)top[0]; int count = (int)top[1]; String inner = currentString.toString(); currentString = new StringBuilder(prev); for(int i=0;i<count;i++) currentString.append(inner). Return currentString.toString().
Both implementations store (String, int) pairs on the stack. The critical Java note is to use StringBuilder for efficiency — naive String concatenation inside a loop is O(n^2) for string building. Python string multiplication (s * k) creates a new string in O(k * len(s)) time which is unavoidable but optimal for this problem structure.
Multi-Digit Numbers: Always Accumulate Before the Bracket
Numbers can be multi-digit: "12[a]" means repeat 'a' twelve times, not one then two. Always accumulate digits with currentNum = currentNum*10 + digit and only consume currentNum when you see the "[". Resetting currentNum to 0 at each "[" ensures each bracket level has exactly its own repeat count without contamination from previous levels.