Start learning free

LESSON 326 OF 414 · STACK

Decode String in Rust: A Stack of Counts and Buffers

Practice O(n) Read the full explanation

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 105.


Example 1:

Input: s = "3[a]2[bc]"
Output: "aaabcbc"

Example 2:

Input: s = "3[a2[c]]"
Output: "accaccacc"

Example 3:

Input: s = "2[abc]3[cd]ef"
Output: "abcabccdcdcdef"

Constraints:

  • s has between 1 and 30 characters.
  • 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].

STEP 1 OF 16
    Ctrl + Enter to check

    RUST · STACK

    Decode String in Rust, explained

    Problem overview

    The function decode_string receives an encoded string s and returns the decoded one. The rule is k[encoded_string]: the text inside the square brackets is repeated exactly k times. Groups can be nested, and plain letters can appear anywhere between them. The input is always valid, and digits appear only as repeat counts, so inputs such as 3a or 2[4] don't occur.

    For "3[a]2[bc]" the answer is "aaabcbc". For "3[a2[c]]" the inner group 2[c] becomes cc, so the outer group repeats acc three times and gives "accaccacc". For "2[abc]3[cd]ef" the answer is "abcabccdcdcdef", with the trailing ef copied as it is.

    Approach: decode a group when its bracket closes

    A group can only be expanded once everything inside it is decoded. In a nested string that means the innermost group comes first, and the innermost group is always the one opened most recently. That is the behaviour of a stack.

    The solution keeps one stack of strings, and its entries are of two kinds:

    • a count entry, which holds only digits, such as "23";
    • a text entry, which collects decoded letters.

    Every [ pushes a new, empty text entry, and it always sits on top of the count that was read just before it. Letters are appended to the top entry. Digits either start a new count entry or extend the one on top. A ] pops the text, pops the count under it, repeats the text and appends the result to whatever is now on top, which is the text of the enclosing level.

    For "3[a2[c]]" the stack goes through these states:

    after "3["    : ["3", ""]
    after "a"     : ["3", "a"]
    after "2["    : ["3", "a", "2", ""]
    after "c"     : ["3", "a", "2", "c"]
    after "]"     : ["3", "acc"]
    after "]"     : ["accaccacc"]

    The first ] pops "c" and the count "2", and appends "cc" to the "a" below. The second pops "acc" and "3", leaving the stack with nothing in it, so the result becomes the single remaining entry.

    Step by step

    1. Create the stack stack as an empty Vec<String>.
    2. Loop over the characters of s with chars(), naming the variable c, and give each kind of character its own branch.
    3. Handle [. Push an empty String to start the buffer for the bracketed text.
    4. Handle letters in a branch that tests c.is_ascii_alphabetic(). If the stack is empty, as it is for letters before any bracket, push an empty buffer first. Then append the letter to the last entry through last_mut().
    5. Handle digits in a branch that tests c.is_ascii_digit(). Look at a copy of the top entry, or an empty string when the stack is empty. If that entry is empty or contains something other than digits, a new count begins, so push c.to_string() as a new entry. Otherwise the top is a count in progress, so append the digit to it. This is how 23 ends up in one entry instead of two.
    6. Handle ] in the final else. Pop the text into curr_str, pop the count and parse it into a usize named curr_num, and build the repeated text with curr_str.repeat(curr_num).
    7. Put the result back. If the stack is empty, the group was at the top level, so push the repeated text as a new entry. Otherwise append it to the last entry, which is the enclosing buffer.
    8. Return the one entry left on the stack. pop() gives an Option, and unwrap_or_default() turns None into an empty string.

    The solution in Rust

    impl Solution {
        fn decode_string(s: String) -> String {
            let mut stack: Vec<String> = Vec::new();
            for c in s.chars() {
                if c == '[' {
                    stack.push(String::new());
                } else if c.is_ascii_alphabetic() {
                    if stack.is_empty() {
                        stack.push(String::new());
                    }
                    stack.last_mut().unwrap().push(c);
                } else if c.is_ascii_digit() {
                    let top = stack.last().cloned().unwrap_or_default();
                    if top.is_empty() || !top.chars().all(|ch| ch.is_ascii_digit()) {
                        stack.push(c.to_string());
                    } else {
                        stack.last_mut().unwrap().push(c);
                    }
                } else {
                    let curr_str = stack.pop().unwrap();
                    let curr_num: usize = stack.pop().unwrap().parse().unwrap();
                    let repeated = curr_str.repeat(curr_num);
                    if stack.is_empty() {
                        stack.push(repeated);
                    } else {
                        *stack.last_mut().unwrap() += &repeated;
                    }
                }
            }
            stack.pop().unwrap_or_default()
        }
    }
    

    Rust details worth knowing:

    • Cloning the top entry. stack.last().cloned().unwrap_or_default() turns an Option<&String> into an owned String, or an empty one if the stack is empty. Because the copy is owned, the borrow of stack ends at once, and the next lines can call push or last_mut without a borrow conflict. The cost is a copy of the top entry for every digit.
    • all on an empty string. "".chars().all(|ch| ch.is_ascii_digit()) is true, since there is no character that fails the test. That is why the code checks top.is_empty() separately. Without it, a digit that follows a [ would be glued onto the empty text buffer instead of starting a new count.
    • *stack.last_mut().unwrap() += &repeated. last_mut() gives Option<&mut String>, and the * reaches the string itself. String += &str appends in place, and &repeated borrows the new string as a &str.
    • Parsing. let curr_num: usize = ....parse().unwrap(); uses the type annotation to pick the target type of parse. It returns a Result, and unwrap() would panic on a non-number such as "x". Valid input only puts digits in count entries. A usize is what str::repeat takes.
    • repeat. "ab".repeat(3) is "ababab". It builds the whole result in one allocation.
    • The last else. Letters, digits and [ are matched by earlier branches, so for valid input the final else only sees ].

    Complexity

    Time: O(L × d) at worst, where L is the length of the decoded output and d is the nesting depth. Each character of the input is handled once, apart from the copy of the top entry made for every digit, which is tiny. The cost is in the repeat calls and appends: text inside nested groups is copied once at each level it passes through. Nested counts multiply, so the decoded text can be far longer than the input, and the lesson limits it to 100,000 characters. With an input of at most 30 characters the depth is small.

    Space: O(L). The stack holds buffers whose total size is at most the decoded text so far.

    Edge cases and common mistakes

    • Multi-digit counts. "12[x]" gives twelve x characters. If each digit started its own entry, the code would read the count as 2.
    • Nested counts back to back. "2[3[a]b]" gives aaabaaab. The 3 right after a [ sees an empty top entry and starts a new count.
    • Letters before and after a group. "ab2[c]d" gives abccd. The count 2 is a separate entry on top of ab, and the decoded cc is appended to ab when the group closes.
    • Two groups in a row. "2[a]3[b]" gives aabbb. After the first group closes, the stack holds "aa", which contains letters, so the next digit 3 starts a new count.
    • No brackets at all. "abc" returns abc; the lazily created buffer collects the letters.
    • Treating a count as text. Without the digit test, a count that follows decoded text would be appended to that text. Keeping counts as their own all-digit entries is what keeps the two kinds apart.

    Testing

    • "3[a]2[bc]" gives aaabcbc.
    • "3[a2[c]]" gives accaccacc.
    • "2[abc]3[cd]ef" gives abcabccdcdcdef.
    • "2[a2[b]c]" gives abbcabbc.
    • "10[a]" gives ten a characters.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free