Start learning free

LESSON 322 OF 414 · STACK

Valid Parentheses in Rust: O(n) with a Stack and a HashMap

Practice O(n) Read the full explanation

Given a string input containing just the characters '(', ')', '{', '}', '[' and ']', determine if it is valid.

An input string is valid if:

  1. Open brackets must be closed by the same type of brackets.
  2. Open brackets must be closed in the correct order.

Example 1:

Input: input = "()"
Output: true

Example 2:

Input: input = "()[]{}"
Output: true

Example 3:

Input: input = "(]"
Output: false

Example 4:

Input: input = "([)]"
Output: false

Example 5:

Input: input = "{[]}"
Output: true

Constraints:

  • The length of input is between 1 and 104.
  • input consists of parentheses only '()[]{}'.

Note:

Your algorithm should run in O(n) time and use O(n) extra space.


STEP 1 OF 11
    Ctrl + Enter to check

    RUST · STACK

    Valid Parentheses in Rust, explained

    Problem overview

    The function is_valid receives a string, input, made only of the characters (, ), {, }, [ and ]. It returns true when the brackets are balanced and false otherwise. Two rules define balanced:

    1. every opening bracket is closed by a bracket of the same type;
    2. brackets close in the right order: the most recently opened one is closed first.

    "()", "()[]{}" and "{[]}" are valid. "(]" is not, because the type is wrong, and "([)]" is not, because the [ is still open when the ) tries to close the ( underneath it.

    The statement asks for O(n) time and O(n) extra space.

    Approach: a stack of open brackets

    Counting brackets is not enough. In "([)]" every bracket has a partner of the same type, yet the string is invalid, because the order is wrong. What matters is that a closing bracket must match the most recent opening bracket that is still open. "Most recent first" describes a stack.

    Scan the string from left to right:

    • An opening bracket is pushed onto the stack. It waits there until its closing bracket arrives.
    • A closing bracket must match the top of the stack. Pop the top, and compare. If the stack is empty, or the popped bracket is of another type, the string is invalid.

    At the end, the stack must be empty. Anything left on it is an opening bracket that was never closed, as in "((".

    A map from each opening bracket to its closing partner keeps the matching rule in one place, so the loop does not need a separate branch for each of the three types.

    Take "([)]". The ( and the [ are pushed, so the stack is ( then [. The ) pops the [. The partner of [ is ], not ), so the function returns false on the third character.

    Step by step

    1. Build the map closing from each opening bracket to its closing one, as a HashMap<char, char>. It is built from an array of pairs with .iter(), .cloned() and .collect(). iter() yields references to the pairs, cloned() copies each pair, and collect builds the map named by the type annotation.
    2. Create the stack as an empty, mutable Vec<char>. Its last element is the top.
    3. Loop over the characters of input with input.chars(), calling each one bracket.
    4. Test for an opening bracket. closing.contains_key(&bracket) is true exactly when bracket is a key of the map, which means it is an opening bracket. The method takes a reference.
    5. Push it. An opening bracket goes onto the stack.
    6. Handle everything else in an else branch. What isn't a key must be a closing bracket.
    7. Pop and match. Inside the else, match on stack.pop(), which is an Option<char>. The None arm, None => return false, covers a closing bracket with nothing open.
    8. Bind the popped bracket. The Some(open) arm names the bracket taken from the stack: the only one this closing bracket may close.
    9. Compare with its partner. If bracket is not closing[&open], the type is wrong, and the function returns false.
    10. Check that nothing is left open. After the loop, stack.is_empty() is the final expression. It is true only when every opening bracket was closed.

    The solution in Rust

    use std::collections::HashMap;
    
    impl Solution {
        fn is_valid(input: String) -> bool {
            let closing: HashMap<char, char> = [('(', ')'), ('{', '}'), ('[', ']')]
                .iter()
                .cloned()
                .collect();
            let mut stack: Vec<char> = Vec::new();
            for bracket in input.chars() {
                if closing.contains_key(&bracket) {
                    stack.push(bracket);
                } else {
                    match stack.pop() {
                        Some(open) => {
                            if bracket != closing[&open] {
                                return false;
                            }
                        }
                        None => return false,
                    }
                }
            }
            stack.is_empty()
        }
    }
    

    The Rust details worth a closer look:

    • Characters, not bytes. input.chars() yields char values. A String can't be indexed with input[i], because it is UTF-8 and indexing by position isn't defined. Iterating over chars() is the normal way to read it. The brackets here are single-byte ASCII, but chars() is the habit that works for any text.
    • Collecting into a map. An iterator of (char, char) pairs can be collected into a HashMap, using the first element as the key and the second as the value. The type annotation on closing tells collect what to build. Without .cloned(), the iterator would yield &(char, char), which can't be collected into this map.
    • Indexing a map. closing[&open] panics if the key is missing. It is safe here, because only keys of the map are ever pushed onto the stack. closing.get(&open) would return an Option instead.
    • Exhaustive match. A match must cover every case. Option has two, Some and None, and the code has an arm for each. The Some arm has a block as its body, and the None arm is a single expression, return false. The block's value is (), and return false has a type that fits any arm, so the two arms agree.
    • Early return. return false leaves the function at once from inside the loop and the match. The last line has no semicolon, so it is the value returned when the loop completes.

    Complexity

    Time: O(n). The loop looks at each character once. A map lookup, a push and a pop each take constant time (the map has three entries, and a push is O(1) amortized).

    Space: O(n). In the worst case, such as a string of only opening brackets, the stack holds every character. The map has three entries, which is a constant.

    Edge cases and common mistakes

    • Closing bracket first. ")" and "][" hit the empty stack and return false through the None arm.
    • Unclosed openers. "((" and "(()" pass through the loop, and the final stack.is_empty() reports false. Returning true at the end without that test would be wrong.
    • Extra closers. "())" returns false at the third character, because the stack is empty again by then.
    • Right count, wrong order. "([)]" is rejected because the top of the stack is the wrong type.
    • Nesting. "{[()]}" is valid: each closer meets its own opener at the top.
    • Only counting. Checking that the number of ( equals the number of ), and the same for the others, accepts "([)]".
    • Empty string. The function returns true, since the loop doesn't run and the empty stack counts as balanced. The constraints say the string has at least one character.

    Testing

    • "()" gives true.
    • "()[]{}" gives true.
    • "{[()]}" gives true.
    • "(]" gives false.
    • "([)]" gives false.
    • "(()" gives false.
    • "())" gives false.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free