A complete visual walkthrough of the stack-based approach — from brute force to the elegant one-pass solution.
The naive approach: repeatedly scan the string and remove any adjacent matching pairs (), {}, or []. Keep going until nothing changes. If the string becomes empty, it was valid.
This works, but each pass scans the entire string and we may need up to n/2 passes. That gives us O(n²) time in the worst case — far too slow for large inputs.
The problem with brute force: We're re-scanning the same characters over and over. What if we could process each character exactly once and remember what we've seen? That's where a stack comes in.
A stack is the perfect data structure for matching nested pairs. The rule is simple:
Here is the visual idea. Watch how the stack grows when we encounter opening brackets and shrinks when we find matching closing brackets:
Clever trick: Instead of pushing the opening bracket and then mapping to its pair later, we push the expected closing bracket. That way, when we see a closing bracket, we just check stack.pop() == c — no mapping table needed!
Let's trace through the input {[]} step by step, watching the stack at each stage.
Now let's see what happens with an invalid input: ([)]
Before coding, let's identify all the tricky inputs and how the stack handles them:
Quick optimization: If the string length is odd, return false immediately — you can never pair up an odd number of brackets. This avoids unnecessary work.
class Solution { public boolean isValid(String s) { // Use ArrayDeque as a stack (faster than Stack class) Deque<Character> stack = new ArrayDeque<>(); for (char c : s.toCharArray()) { // Opening bracket? Push the EXPECTED closing bracket if (c == '(') stack.push(')'); else if (c == '{') stack.push('}'); else if (c == '[') stack.push(']'); // Closing bracket? Pop and check for match // If stack is empty or top doesn't match → invalid else if (stack.isEmpty() || stack.pop() != c) return false; } // Valid only if all opening brackets were matched return stack.isEmpty(); } }
Why push the closing bracket? By pushing the expected closing bracket instead of the opening one, the matching logic becomes a simple equality check: stack.pop() != c. No need for a separate Map or switch statement to look up pairs.
Enter any bracket string and step through the algorithm one character at a time. Watch the stack grow and shrink in real time.
We process each character exactly once in a single pass through the string, giving O(n) time. In the worst case (all opening brackets like "((((("), the stack holds all n characters, so space is O(n).
Can we do better on space? Not in general. We need to remember the nesting order of opening brackets. The stack is the minimal data structure for this. However, the early-exit on odd length and on first mismatch means we often use far less than O(n) in practice.