DSA sheet · Stack · Expression evaluation pattern

Decode String: Stack & Recursion

This is the last problem of the expression evaluation pattern. A string like "3[a]2[bc]" is a compressed message: the number in front of a bracket says how many times to repeat what is inside. We have to expand it back to "aaabcbc". The teacher admits the brackets scared her the first time she saw it in college. The thing that unlocked it: a closing bracket ] is the only moment we can actually do something, and everything else is just "remember it for later". That "remember now, use later" is exactly what a stack is for. She solves it with one stack of characters, explains why the time depends on the decoded length (not the input length), and says people who like recursion can solve it that way too. This page covers her stack solution, then the classic two-stack version (one stack for counts, one for strings), and then the recursive version.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Stacks, brackets and strings from scratch

What is a stack?

A stack is a pile: you add on the top (push) and remove from the top (pop). Peek means looking at the top without removing it. The last thing pushed is the first thing popped: LIFO (Last In, First Out).

stack operationPython listcost
push xst.append(x)O(1)
pop the topst.pop()O(1)
peekst[-1]O(1)
empty?not stO(1)

We draw a stack left = bottom, right = top. Popping or peeking an empty list raises IndexError, so always check st is non-empty before st[-1] when it could be empty. That check is exactly where the teacher's version needs a fix (Part A, Doubt 4).

Why brackets and stacks go together

Brackets nest: in 3[a2[c]] the inner 2[c] opens after the outer bracket but closes before it. The bracket that opened last is the first to close. That's LIFO again. So a stack naturally remembers "which bracket am I inside right now, and what was going on outside it?"

This problem is listed under expression evaluation because it works like a calculator: a number (the repeat count) and an operation (repeat) get applied to an operand (the string inside the brackets), and the result is used by the level outside.

Strings are immutable: build them in a list

In Java (and Python) a string can't be changed. s = s + "a" builds a brand-new string and copies everything each time. Doing that in a loop costs O(length) per step. The teacher uses Java's StringBuilder to avoid it. The Python equivalent: collect pieces in a list, then join once with "".join(parts). Also, "ab" * 3 gives "ababab" in one go.

Recursion in one paragraph

Recursion is a function calling itself on a smaller piece of the problem. Each call waits on Python's call stack until the inner call returns. So a recursive solution is secretly also a stack solution: the call stack plays the role our list played. Each nested bracket = one deeper call.


Part A · The teacher's way: one stack of characters

LeetCode 394

1The question in simple words

You get an encoded string. The rule is k[text]: the text inside the square brackets is repeated exactly k times. Brackets can be nested. Return the fully expanded string.

inputoutputwhy
3[a]2[bc]aaabcbc"a" three times, then "bc" two times, joined together
3[a2[c]]accaccaccinside first: 2[c] = "cc", so the bracket holds "acc"; repeat 3 times
2[abc]3[cd]efabcabccdcdcdefletters outside any bracket are copied as they are

The input is always valid: brackets are balanced, digits only appear as repeat counts (never as letters to copy), and there's no 3a or 2[4].

2What the constraints tell us

Doubt: how can 30 characters produce a huge answer?
→ The teacher's example: 20[50[20[a]]] is only 13 characters, but it means 20 × 50 × 20 = 20,000 a's. Wrap it in 100[...] (5 more characters, 18 total) and it's 2 × 10⁶. Wrap once more with 1000[...] and you reach about 2 × 10⁹ characters with fewer than 30 input characters. Every extra bracket multiplies the size. So the time complexity is measured by the decoded length, and that's why we must avoid wasteful string copying. (LeetCode's tests are made so the output stays ≤ 10⁵; any solution without needless nested loops passes.)

3Intuition: nothing can be decided until "]"

Walk through 3[a]2[bc] one character at a time and ask "can I do anything yet?"

So the plan: push every character onto a stack until a ] shows up. At a ], walk backwards by popping: letters until [, then the [, then the digits. Expand, and put the result back.

She also notices that each closed bracket gives a finished part that won't be multiplied by anything to its right: 3[a] → part 1 = "aaa", 2[bc] → part 2 = "bcbc". The answer is just the parts joined in order.

4Building the logic

At "]", step 1: pop the letters

Pop until the top is [. Everything popped is the text inside. It comes out backwards (for "bc" we pop c, then b).

Doubt 1: how do we fix the backwards order?
→ The teacher inserts each popped character at position 0 of her StringBuilder, so it ends up in the natural order without a separate reverse. In Python, list.insert(0, x) shifts the whole list each time, so it's cheaper to append while popping and reverse once at the end. Same result.

Step 2: throw away the "["

The [ is just a separator that marks where the text starts. We don't need it: pop it once.

Step 3: pop the number

Now the top of the stack holds the digits of the count. It could be 3, 13 or 113. They also come out backwards: for 113 we pop 3, then 1, then 1.

Doubt 2: can I build the number with num = num * 10 + digit while popping?
→ No. That builds digits in the order they come out, so 113 would become 311. The teacher's fix is the same as for the letters: collect the digit characters in their original order (insert at the front, or reverse once), then convert the string with int(...).

Step 4: expand and push it back

Repeat the text k times. Then the teacher pushes every character of the result back onto the stack. At the very end, the stack holds the whole answer, bottom to top, and we join it.

Doubt 3: she also says you could keep the finished parts in a separate output string instead. Why push back onto the stack?
→ The separate string only works when brackets are not nested. In 3[a2[c]], the expanded "cc" is not a finished part: it is still inside the outer bracket and must be repeated 3 times together with the "a". If we pushed "cc" back onto the stack, the outer ] will pop it along with "a" and handle it correctly. Pushing back works for every input, so that's what her final code does.
Doubt 4 (a fix to the video): when popping the digits, she says to keep popping "until the stack is empty". Is that right?
→ No, that's a bug once anything sits below the number. In 3[a]2[bc], when we reach the second ] the stack is a a a 2 [ b c. Popping "until empty" would take the 2 and the "aaa" underneath it, and int("aaa2") crashes. Nested input breaks the same way: for 3[a2[c]] it would pop "3[a2". The correct rule is keep popping while the stack is not empty and the top is a digit: while st and st[-1].isdigit(). The st and part is the empty-check from Part 0, because the number may be at the very bottom.

5Approach steps

  1. For each character of s: if it's not ], push it.
  2. If it is ]: pop letters until the top is [, and reverse them → text.
  3. Pop the [.
  4. While the stack is non-empty and the top is a digit, pop it. Reverse → k = int(digits).
  5. Push every character of text * k back onto the stack.
  6. At the end, join the stack from bottom to top. That's the answer.

6Code (Python)

Decode String with one stack of characters (the teacher's way, with the digit fix)
class Solution:
    def decodeString(self, s: str) -> str:
        st = []
        for ch in s:
            if ch != ']':
                st.append(ch)                     # can't decide anything yet: remember it
                continue
            # ch == ']' : one bracket is complete
            letters = []
            while st[-1] != '[':                  # 1. the text inside, popped backwards
                letters.append(st.pop())
            letters.reverse()
            text = "".join(letters)

            st.pop()                              # 2. throw away the '['

            digits = []
            while st and st[-1].isdigit():        # 3. the count (FIX: stop at a non-digit)
                digits.append(st.pop())
            digits.reverse()
            k = int("".join(digits))

            st.extend(text * k)                   # 4. push the expansion back, char by char
        return "".join(st)

7Code line by line

linewhat it means
if ch != ']': st.append(ch)Digits, [ and letters are all just remembered.
while st[-1] != '[': letters.append(st.pop())Walk backwards through the bracket's contents. A valid input always has a [ below, so the stack can't run empty here.
letters.reverse()Popping reversed them; this puts them back in reading order.
st.pop()Remove the [ separator.
while st and st[-1].isdigit():Take only the digits of this bracket's count. Stop at a letter, a [, or the bottom.
k = int("".join(digits))Digits in order ("1","1","3") → the number 113.
st.extend(text * k)Push each character of the repeated text back. If this bracket is inside another one, the outer ] will collect them.
return "".join(st)All brackets are gone; the stack bottom → top is the decoded string.

8Dry run

Example 1: s = "3[a]2[bc]"

i012345678
s3[a]2[bc]yellow = the only moments we do work
icurrent charwhat we pop (and why)what we pushstack AFTER (bottom → top)answer so far
03nothing (not a "]")33–
1[nothing[3 [–
2anothinga3 [ a–
3]a (text, until "["); "[" (separator); 3 (count)a a aa a apart 1 = aaa
42nothing2a a a 2
5[nothing[a a a 2 [
6bnothingba a a 2 [ b
7cnothingca a a 2 [ b c
8]c, b → "bc"; "["; 2 → stop, top is "a" (not a digit!)b c b ca a a b c b cpart 2 = bcbc
endjoin the stackaaabcbc ✓
just before i = 8 is handled
aaa2[bc
after popping "bc", "[" and "2"
aaa
after pushing "bcbc"
aaabcbc

The middle picture is why "pop digits until empty" fails: right after the 2 the next item is "a", and we must stop there.

Example 2 (nested): s = "3[a2[c]]"

icurrent charwhat we pop (and why)what we pushstack AFTER (bottom → top)answer so far
0–43 [ a 2 [nothingeach char3 [ a 2 [–
5cnothingc3 [ a 2 [ c–
6]c; "["; 2 → stop at "a"c c3 [ a c cinner = cc (not final yet!)
7]c, c, a → "acc"; "["; 3 → stack empty, stopacc × 3a c c a c c a c c
endjoinaccaccacc ✓
before i = 6
3[a2[c
after i = 6: "cc" pushed back inside the outer bracket
3[acc
after i = 7
a c ca c ca c c

9Complexity & remember

RememberPush everything until ]. At ]: pop letters to [ (reverse), pop [, pop digits while the top is a digit (reverse, int), push text * k back. Join the stack at the end.

Part B · Two stacks: counts and strings

Part A pops characters one by one and rebuilds strings and numbers backwards. A tidier stack version (the one you'll often see in interviews) keeps whole numbers and whole strings on two separate stacks, so nothing ever needs reversing. The teacher doesn't code this one in the video. It's the same idea as hers ("remember the outside, expand at ]"), organised differently.

1The question in simple words

Same question: expand k[text], with nesting.

2What the constraints tell us

3Intuition: save the outside world at every "["

Keep a variable cur: the string we're building at the current bracket level. When a [ opens, we're about to start a new, deeper level. Before going in, we save two things about the level we're leaving:

Then reset cur = "" and num = 0 and build the inside. At ], the inside is complete: pop the saved count k and the saved outside text prev, and set cur = prev + cur * k. We're back at the outer level, with the expansion attached.

4Building the logic

characterwhat we dowhy
digitnum = num * 10 + int(ch)the count may have several digits
[push num to counts, push cur to strs; cur = "", num = 0save the outer level, start a fresh inner level
]k = counts.pop(); prev = strs.pop(); cur = prev + cur × kthe inner level is done: expand it and glue it after the outer text
lettercur += chpart of the current level's text
Doubt: why push cur at "[" instead of keeping it?
→ Because cur is about to be reused for the inner text. In 3[a2[c]], at the second [ we have cur = "a". If we didn't save it, building "c" would overwrite it. The string stack keeps "a" safe until the inner bracket closes, and then "a" + "cc" gives "acc".
Doubt: do the two stacks always have the same height?
→ Yes. We push to both at every [ and pop from both at every ]. Their height = how many brackets are currently open.

5Approach steps

  1. counts = [], strs = [], cur = "", num = 0.
  2. Scan s and apply the table above to each character.
  3. Return cur (after the last character, all brackets are closed and we're at the outermost level).

6Code (Python)

Decode String with a count stack and a string stack
class Solution:
    def decodeString(self, s: str) -> str:
        counts = []                 # repeat count of each open bracket
        strs = []                   # text built BEFORE each open bracket
        cur = ""                    # text of the level we're in now
        num = 0
        for ch in s:
            if ch.isdigit():
                num = num * 10 + int(ch)
            elif ch == '[':
                counts.append(num)  # save the count for this bracket
                strs.append(cur)    # save the outside text
                cur = ""            # start the inside fresh
                num = 0
            elif ch == ']':
                k = counts.pop()
                prev = strs.pop()
                cur = prev + cur * k   # outside text + repeated inside
            else:
                cur += ch           # a letter
        return cur

cur += ch copies the string, but the strings here are short (each level's text) and LeetCode's limits are small. For very long texts you could keep cur as a list and join at the end.

7Code line by line

linewhat it means
num = num * 10 + int(ch)Reading forward, so "113" builds 1 → 11 → 113 correctly.
counts.append(num) strs.append(cur)Remember the outer level before going one bracket deeper.
cur = ""; num = 0The inside starts empty, and the next number belongs to a different bracket.
cur = prev + cur * kClose the bracket: repeat the inside, attach it after what was built outside.
return curOnly the outermost level remains.

8Dry run: s = "3[a2[c]]", both stacks after every step

icurrent charwhat we pop (and why)what we pushcounts AFTERstrs AFTERcur, num
03––[][]"", 3
1[–3 to counts, "" to strs[3][""]"", 0
2a––[3][""]"a", 0
32––[3][""]"a", 2
4[–2 to counts, "a" to strs[3, 2]["", "a"]"", 0
5c––[3, 2]["", "a"]"c", 0
6]k = 2, prev = "a" (inner bracket closes)–[3][""]"a" + "cc" = "acc"
7]k = 3, prev = "" (outer bracket closes)–[][]"" + "acc"×3 = "accaccacc"
counts after i = 4
32
strs after i = 4
"""a"
counts after i = 6
3
strs after i = 6
""

Read each pair of pictures side by side: the top of counts (red) and the top of strs belong to the same open bracket.

"3[a]2[bc]" in short: at the first [ push 3, ""; cur = "a"; at ] cur = "" + "aaa". At the next [ push 2, "aaa"; cur = "bc"; at ] cur = "aaa" + "bcbc" = "aaabcbc". Here the "aaa" waited safely in the string stack, which is exactly the teacher's "part 1" idea.

9Complexity & remember

Remember[ → push (num, cur), reset both. ] → cur = strs.pop() + cur * counts.pop(). Digit → num×10+d. Letter → cur += ch.

Part C · Recursion: let the call stack do it

The teacher mentions that many people who know recursion solve this problem that way. It's correct, but in her experience recursive code is usually slower (each call has extra overhead), so she prefers the explicit stack. Here's how it works, so you can choose.

1The question in simple words

Same question. Now "inside a bracket" becomes "inside a recursive call".

2What the constraints tell us

3Intuition

The text inside a bracket is itself a smaller encoded string. So: "decode from position i until you hit the matching ]" is the same job as the whole problem, just smaller. When we see k[, we call ourselves to decode the inside, get back the inside's text, and repeat it k times. The call stack remembers the outside text and the count, exactly like the two stacks in Part B did.

4Building the logic

Doubt: why must the helper return the index too?
→ The caller needs to know where the inner bracket ended so it can continue from the next character. Otherwise it would read the inside again.

5Approach steps

  1. dfs(i): parts = [], num = 0.
  2. While i < len(s): digit → num; letter → parts.append; [ → (inner, i) = dfs(i + 1), parts.append(inner * num), num = 0; ] → return (join(parts), i). Then i += 1.
  3. At the end of s, return (join(parts), i).
  4. Answer = dfs(0)[0].

6Code (Python)

Decode String with recursion
class Solution:
    def decodeString(self, s: str) -> str:
        n = len(s)

        def dfs(i):
            parts = []
            num = 0
            while i < n:
                ch = s[i]
                if ch.isdigit():
                    num = num * 10 + int(ch)
                elif ch == '[':
                    inner, i = dfs(i + 1)       # decode the inside; i is now at its ']'
                    parts.append(inner * num)
                    num = 0
                elif ch == ']':
                    return "".join(parts), i    # this bracket level is done
                else:
                    parts.append(ch)
                i += 1
            return "".join(parts), i            # reached the end of s (outermost level)

        return dfs(0)[0]

7Code line by line

linewhat it means
inner, i = dfs(i + 1)Go one bracket deeper. The call returns the inside's text and the index of its ].
parts.append(inner * num)Repeat the inside and attach it to this level.
return "".join(parts), iAt ]: hand this level's text back to the caller (the one that saw the [).
i += 1After handling a bracket, this steps past its ].

8Dry run: s = "3[a2[c]]"

  1. dfs(0): reads 3 → num = 3. At i = 1 sees [ → calls dfs(2) and waits.
  2. dfs(2): reads "a" → parts = [a]. Reads 2 → num = 2. At i = 4 sees [ → calls dfs(5) and waits.
  3. dfs(5): reads "c" → parts = [c]. At i = 6 sees ] → returns ("c", 6).
  4. Back in dfs(2): appends "c" × 2 → parts = [a, cc]. i = 6 → i += 1 → 7, sees ] → returns ("acc", 7).
  5. Back in dfs(0): appends "acc" × 3. i = 7 → 8 = n, loop ends → returns ("accaccacc", 8). Answer accaccacc ✓
step 3 (deepest)
dfs(0) num=3, parts=[]dfs(2) num=2, parts=[a]dfs(5) → "c"
step 4
dfs(0) num=3, parts=[]dfs(2) → "acc"
step 5
dfs(0) → "accaccacc"

Compare with Part B: each waiting frame holds a (num, parts) pair, exactly what the counts and strs stacks held.

9Complexity & remember

RememberA bracket = a recursive call. [ → recurse, multiply what comes back. ] → return (text, index). The call stack replaces the two explicit stacks.

Part D · Revision page

A: one char stack (video)B: two stacksC: recursion
what's rememberedevery charactercount + outside text per open bracketcount + parts in each call frame
when work happensat ]: pop text, [, digitsat [ (save) and ] (expand)at [ (recurse) and ] (return)
reversing needed?yes (text and digits come out backwards)nono
multi-digit countspop digits, reverse, int()num×10+d going forwardnum×10+d going forward
timelinear in the decoded length (× depth at worst), not in len(s)
spaceO(decoded length)
If you remember only 5 lines 1. Nothing can be decided until ]; before that, just remember things on a stack.
2. At ]: get the inside text, drop the [, get the count, push the expansion back.
3. Pop digits only while the top is a digit, and reverse popped text and digits.
4. Two-stack version: [ saves (num, cur); ] does cur = prev + cur * k.
5. Time depends on the decoded length: 13 input chars can mean 20,000 output chars.
Mistakes to avoid ✗ popping digits "until the stack is empty" (it eats earlier letters)
✗ building the count from popped digits as num×10+d (113 becomes 311)
✗ forgetting to reverse the popped letters
✗ reading only one digit of the count (10[a] has count 10)
✗ saving finished parts outside the stack when brackets can nest
✗ judging the speed by len(s) ≤ 30
test it yourself (paste under any of the three solutions)
s = Solution()
print(s.decodeString("3[a]2[bc]"))       # aaabcbc
print(s.decodeString("3[a2[c]]"))        # accaccacc
print(s.decodeString("2[abc]3[cd]ef"))   # abcabccdcdcdef
print(s.decodeString("abc"))             # abc
print(s.decodeString("10[a]"))           # aaaaaaaaaa
print(s.decodeString("2[2[2[x]]]"))      # xxxxxxxx

Based on this video: Decode String