228 lines
10 KiB
YAML
228 lines
10 KiB
YAML
title: Decode String
|
|
slug: decode-string
|
|
difficulty: medium
|
|
leetcode_id: 394
|
|
leetcode_url: https://leetcode.com/problems/decode-string/
|
|
categories:
|
|
- strings
|
|
- stack
|
|
- recursion
|
|
patterns:
|
|
- slug: monotonic-stack
|
|
is_optimal: true
|
|
|
|
function_signature: "def decode_string(s: str) -> str:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { s: "3[a]2[bc]" }
|
|
expected: "aaabcbc"
|
|
- input: { s: "3[a2[c]]" }
|
|
expected: "accaccacc"
|
|
- input: { s: "2[abc]3[cd]ef" }
|
|
expected: "abcabccdcdcdef"
|
|
hidden:
|
|
- input: { s: "abc" }
|
|
expected: "abc"
|
|
- input: { s: "10[a]" }
|
|
expected: "aaaaaaaaaa"
|
|
- input: { s: "2[2[2[b]]]" }
|
|
expected: "bbbbbbbb"
|
|
- input: { s: "1[ab]" }
|
|
expected: "ab"
|
|
- input: { s: "2[xy3[z]]" }
|
|
expected: "xyzzzxyzzz"
|
|
- input: { s: "2[a2[b]]" }
|
|
expected: "abbabb"
|
|
- input: { s: "100[x]" }
|
|
expected: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
|
|
description: |
|
|
Given an encoded string, return its decoded string.
|
|
|
|
The encoding rule is: `k[encoded_string]`, where the `encoded_string` inside the square brackets is being repeated exactly `k` times. Note that `k` is guaranteed to be a positive integer.
|
|
|
|
You may assume that the input string is always valid; there are no extra white spaces, square brackets are well-formed, etc. Furthermore, you may assume that the original data does not contain any digits and that digits are only for those repeat numbers, `k`. For example, there will not be input like `3a` or `2[4]`.
|
|
|
|
The test cases are generated so that the length of the output will never exceed `10^5`.
|
|
|
|
constraints: |
|
|
- `1 <= s.length <= 30`
|
|
- `s` consists of lowercase English letters, digits, and square brackets `'[]'`
|
|
- `s` is guaranteed to be a valid input
|
|
- All the integers in `s` are in the range `[1, 300]`
|
|
|
|
examples:
|
|
- input: 's = "3[a]2[bc]"'
|
|
output: '"aaabcbc"'
|
|
explanation: "The pattern 3[a] expands to 'aaa' and 2[bc] expands to 'bcbc', giving 'aaabcbc'."
|
|
- input: 's = "3[a2[c]]"'
|
|
output: '"accaccacc"'
|
|
explanation: "The inner 2[c] expands to 'cc', making 3[acc], which expands to 'accaccacc'."
|
|
- input: 's = "2[abc]3[cd]ef"'
|
|
output: '"abcabccdcdcdef"'
|
|
explanation: "2[abc] becomes 'abcabc', 3[cd] becomes 'cdcdcd', plus 'ef' at the end."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Think of this problem like unpacking nested boxes. Each `k[...]` is a box containing a message that needs to be repeated `k` times. The tricky part is that boxes can be nested inside other boxes — you need to unpack the innermost boxes first before you can complete the outer ones.
|
|
|
|
This "last opened, first closed" pattern is exactly what a **stack** excels at. When you encounter an opening bracket `[`, you're entering a new nested level. When you hit a closing bracket `]`, you've completed the innermost pattern and need to expand it before continuing with the outer context.
|
|
|
|
Imagine reading `3[a2[c]]`:
|
|
- You see `3[` — push your current work aside, start fresh for what's inside
|
|
- You see `a2[` — push again, start fresh for the innermost part
|
|
- You see `c]` — you've completed `c`, multiply by `2` to get `cc`, pop back to the previous context
|
|
- Now you have `acc`, hit `]` — multiply by `3` to get `accaccacc`
|
|
|
|
The stack lets you "pause" your current string-building work, dive into a nested pattern, resolve it, and then resume where you left off.
|
|
|
|
approach: |
|
|
We solve this using a **Stack-Based Approach**:
|
|
|
|
**Step 1: Initialise data structures**
|
|
|
|
- `stack`: Holds pairs of (previous_string, repeat_count) when we enter a new `[`
|
|
- `current_string`: The string we're building at the current nesting level
|
|
- `current_num`: Accumulates multi-digit numbers like `12` or `300`
|
|
|
|
|
|
|
|
**Step 2: Iterate through each character**
|
|
|
|
- **If digit**: Accumulate into `current_num` (handle multi-digit numbers with `current_num = current_num * 10 + int(char)`)
|
|
- **If `[`**: Push `(current_string, current_num)` onto the stack, then reset both to start fresh for the nested content
|
|
- **If `]`**: Pop from stack, multiply `current_string` by the popped count, and prepend the popped string
|
|
- **If letter**: Simply append to `current_string`
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- After processing all characters, `current_string` contains the fully decoded string
|
|
|
|
|
|
|
|
The key insight is that we save our "context" (what we've built so far and how many times to repeat the upcoming section) whenever we enter a new nesting level, then restore and combine when we exit.
|
|
|
|
common_pitfalls:
|
|
- title: Forgetting Multi-Digit Numbers
|
|
description: |
|
|
The repeat count `k` can be more than one digit! For example, `12[a]` means repeat `a` twelve times, not `1` followed by `2[a]`.
|
|
|
|
When you encounter a digit, you need to accumulate: `current_num = current_num * 10 + int(char)`. This handles numbers like `123` correctly by building up `1` → `12` → `123`.
|
|
wrong_approach: "Treating each digit as a separate number"
|
|
correct_approach: "Accumulate digits: current_num = current_num * 10 + digit"
|
|
|
|
- title: Not Resetting State After Opening Bracket
|
|
description: |
|
|
When you push to the stack upon seeing `[`, you must reset `current_string` to empty and `current_num` to `0`. Otherwise, you'll mix content from different nesting levels.
|
|
|
|
For `3[a2[c]]`, after pushing at the first `[`, you need a fresh start to build `a` before encountering `2[c]`.
|
|
wrong_approach: "Continuing to append to the same string after ["
|
|
correct_approach: "Reset current_string and current_num after pushing to stack"
|
|
|
|
- title: String Concatenation in Wrong Order
|
|
description: |
|
|
When popping from the stack after `]`, the order matters:
|
|
- `prev_string + (current_string * count)` is correct
|
|
- `(current_string * count) + prev_string` is wrong
|
|
|
|
The `prev_string` is what came *before* the `k[...]` pattern, so it should stay at the front.
|
|
wrong_approach: "Appending prev_string after the repeated content"
|
|
correct_approach: "Prepend prev_string: result = prev_string + current_string * count"
|
|
|
|
- title: Using Recursion Without Tracking Position
|
|
description: |
|
|
A recursive approach is valid but requires careful handling of the current position. You need to return both the decoded string *and* the index where you stopped, so the caller knows where to resume.
|
|
|
|
The iterative stack approach avoids this complexity by maintaining state explicitly.
|
|
|
|
key_takeaways:
|
|
- "**Stack for nested structures**: Whenever you see nested patterns with matching pairs (brackets, parentheses), think stack"
|
|
- "**Save and restore context**: Push your current state when entering a new level, pop and combine when exiting"
|
|
- "**Multi-digit number handling**: Always accumulate digits with `num = num * 10 + digit` for robustness"
|
|
- "**Pattern recognition**: This stack technique applies to many parsing problems — balanced parentheses, expression evaluation, nested HTML tags"
|
|
|
|
time_complexity: "O(n * m) where `n` is the length of the encoded string and `m` is the maximum length of the decoded string. Each character is processed once, but string concatenation may involve copying characters multiple times due to repetition."
|
|
space_complexity: "O(n + m) for the stack storing intermediate strings and the output string. In the worst case of deeply nested patterns, the stack depth approaches `n`."
|
|
|
|
solutions:
|
|
- approach_name: Stack
|
|
is_optimal: true
|
|
code: |
|
|
def decodeString(s: str) -> str:
|
|
stack = [] # Stores (prev_string, repeat_count) pairs
|
|
current_string = ""
|
|
current_num = 0
|
|
|
|
for char in s:
|
|
if char.isdigit():
|
|
# Accumulate multi-digit numbers
|
|
current_num = current_num * 10 + int(char)
|
|
|
|
elif char == '[':
|
|
# Save current context and start fresh
|
|
stack.append((current_string, current_num))
|
|
current_string = ""
|
|
current_num = 0
|
|
|
|
elif char == ']':
|
|
# Pop context and apply repetition
|
|
prev_string, repeat_count = stack.pop()
|
|
current_string = prev_string + current_string * repeat_count
|
|
|
|
else:
|
|
# Regular letter - just append
|
|
current_string += char
|
|
|
|
return current_string
|
|
explanation: |
|
|
**Time Complexity:** O(n * m) — We process each character once, but string operations may copy characters multiple times based on repeat counts.
|
|
|
|
**Space Complexity:** O(n + m) — Stack space for nested levels plus the decoded output string.
|
|
|
|
The stack elegantly handles nested patterns by saving context when entering brackets and restoring when exiting. Each `]` triggers a "resolve and combine" operation that builds up the final string from the inside out.
|
|
|
|
- approach_name: Recursion
|
|
is_optimal: false
|
|
code: |
|
|
def decodeString(s: str) -> str:
|
|
def decode(index: int) -> tuple[str, int]:
|
|
result = ""
|
|
num = 0
|
|
|
|
while index < len(s):
|
|
char = s[index]
|
|
|
|
if char.isdigit():
|
|
# Accumulate the repeat count
|
|
num = num * 10 + int(char)
|
|
|
|
elif char == '[':
|
|
# Recurse to decode the nested content
|
|
decoded, index = decode(index + 1)
|
|
result += decoded * num
|
|
num = 0
|
|
|
|
elif char == ']':
|
|
# End of current level - return to caller
|
|
return result, index
|
|
|
|
else:
|
|
# Regular letter
|
|
result += char
|
|
|
|
index += 1
|
|
|
|
return result, index
|
|
|
|
decoded_string, _ = decode(0)
|
|
return decoded_string
|
|
explanation: |
|
|
**Time Complexity:** O(n * m) — Similar to the stack approach, processing each character with potential string repetition.
|
|
|
|
**Space Complexity:** O(n) — Recursion depth proportional to nesting level, plus O(m) for output.
|
|
|
|
The recursive approach mirrors the stack solution but uses the call stack implicitly. Each `[` triggers a recursive call, and each `]` returns to the caller. The key is returning both the decoded string *and* the current index so the caller knows where to resume parsing.
|