Valid Parentheses
Stack matching
The stack’s ‘hello world’. Match most-recent open bracket.
Problem
Given a string s containing only the characters '(', ')', '{', '}', '[', ']', determine whether the input is balanced.
A string is balanced if every opening bracket is closed by the same type of bracket, and in the correct order.
Intuition
Brackets obey a last-in, first-out rule: the most recently opened bracket must be the first one closed. That's the exact semantics of a stack.
Push every opener. When you see a closer, pop the top and check it matches. If the stack is empty on a closer — or nonempty at the end — the string is unbalanced.
This generalizes to any nesting problem: function call frames, JSON parsing, HTML tag matching. Stack is the canonical "what's the most recent unresolved thing?" data structure.
Approach
Stack Matching
O(n)O(n)One pass. Push openers, pop + match on closers, verify empty at the end.
impl Solution {
pub fn is_valid(s: String) -> bool {
let mut stack: Vec<u8> = Vec::with_capacity(s.len());
for &c in s.as_bytes() {
match c {
b'(' | b'[' | b'{' => stack.push(c),
b')' => if stack.pop() != Some(b'(') { return false; },
b']' => if stack.pop() != Some(b'[') { return false; },
b'}' => if stack.pop() != Some(b'{') { return false; },
_ => return false,
}
}
stack.is_empty()
}
}
function isValid(s: string): boolean {
const match: Record<string, string> = { ")": "(", "]": "[", "}": "{" };
const stack: string[] = [];
for (const c of s) {
if (c === "(" || c === "[" || c === "{") {
stack.push(c);
} else if (match[c] !== undefined) {
if (stack.pop() !== match[c]) return false;
} else {
return false;
}
}
return stack.length === 0;
}
Language notes:
- Rust — Operating on bytes (
s.as_bytes()) is safe because the alphabet is ASCII by definition.stack.pop()returnsOption<u8>; comparing againstSome(b'(')handles the "closer with empty stack" case uniformly. - TypeScript — A lookup
Record<string, string>keeps the match map explicit.stack.pop()returnsundefinedon an empty stack, which never equals an opener — same uniform-comparison trick.
Trace
Input: "([])"
| i | c | action | stack after |
|---|---|---|---|
| 0 | ( | push | ['('] |
| 1 | [ | push | ['(', '['] |
| 2 | ] | pop → '[' ✓ | ['('] |
| 3 | ) | pop → '(' ✓ | [] |
Final: stack empty → true.
Edge cases
- Empty string — trivially balanced; returns
true. - Odd length — guaranteed unbalanced; micro-opt is a short-circuit
s.length % 2 !== 0. - Closer with empty stack — handled by the "pop returns None/undefined ≠ opener" check.
- Leftover openers — caught by the final
is_empty()/length === 0check.
The monotonic-stack family (#739 Daily Temperatures, #84 Largest Rectangle) builds on the same "stack of unresolved things" pattern. Internalize this one first.
Takeaway
When order-of-operations matters and the relationship is last-opened-first-closed, reach for a stack. Scales from this one-screen problem all the way up to compilers and runtime systems.
More in Stack & Queue Fundamentals
Auxiliary structure trick: maintain a parallel stack of minimums.
Stack as an accumulator. This pattern scales to expression parsing.
Your first monotonic stack! ‘Next greater element’ pattern.
Stack with a physical intuition — cars merge when a faster one catches a slower one.