Valid Parentheses: LIFO in Action
[!NOTE] This module explores the core principles of the Valid Parentheses problem, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.
1. The Genesis of the Stack
Most developers memorize that “Parentheses = Stack”. But why?
Imagine you are a compiler parsing nested code blocks, or a reader following a book with nested footnotes (like this [1 [2 [3]]]?). To understand the context, you must:
- Encounter an opening bracket
[. - Maintain its “state” while reading nested content.
- Ensure the most recently opened bracket is the first one closed.
This “Last-In, First-Out” (LIFO) requirement is the fundamental logical constraint that makes the Stack the optimal data structure. An array or queue processes linearly, but a stack mirrors the nested nature of the problem perfectly.
2. Problem Definition & Edge Cases
Before diving into the stack mechanics, we must define the exact constraints of the problem. You are given a string s containing just the characters '(', ')', '{', '}', '[' and ']'. Determine if the input string is valid.
An input string is valid if:
- Open brackets must be closed by the same type of brackets.
- Open brackets must be closed in the correct order.
- Every close bracket has a corresponding open bracket of the same type.
Edge Cases to Consider
- Empty String: Usually valid by default, though constraints may enforce $1 \le s.length$.
- Odd Length String: Impossible to be valid. Early return optimization:
if (s.length() % 2 != 0) return false;. - All Opening Brackets: e.g.,
((((. The string is fully processed, but the stack is not empty at the end. - Starting with Closing Bracket: e.g.,
]()[. Immediate failure upon encountering a mismatched closing bracket.
3. Interactive Analysis
Visualize how the stack maintains state. Each opening bracket is a “promise” that must be fulfilled by its corresponding closing partner.
({[]})
LIFO Stack
Execution Step
4. Implementation
The key is to use a stack structure (like a Deque in Java or a slice in Go) to maintain state. We can employ a clever mapping strategy to pair brackets efficiently. Note the addition of the odd-length early return optimization.
Java
public boolean isValid(String s) {
// Early return: An odd-length string cannot be valid
if (s.length() % 2 != 0) return false;
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(')');
} else if (c == '{') {
stack.push('}');
} else if (c == '[') {
stack.push(']');
} else if (stack.isEmpty() || stack.pop() != c) {
return false;
}
}
return stack.isEmpty();
}
Go
func isValid(s string) bool {
// Early return: An odd-length string cannot be valid
if len(s) % 2 != 0 {
return false
}
stack := []rune{}
pairs := map[rune]rune{')': '(', ']': '[', '}': '{'}
for _, char := range s {
if open, ok := pairs[char]; ok {
if len(stack) == 0 || stack[len(stack)-1] != open {
return false
}
stack = stack[:len(stack)-1]
} else {
stack = append(stack, char)
}
}
return len(stack) == 0
}
5. Hardware Reality: Why Stack?
You could solve this with a dynamically sizing array and a pointer, but a Stack is conceptually closer to how the CPU handles Memory Indirection and Recursion.
- Memory Locality: A stack typically grows in a contiguous memory block, ensuring high cache hit rates during sequential push/pop operations.
- Temporal Locality: The most recently added item is the most likely to be accessed next, aligning perfectly with CPU cache hierarchies (L1/L2 caches).
| Metric | Stack Approach |
|---|---|
| Time Complexity | O(N) (One pass through the string) |
| Space Complexity | O(N) (Worst case, all open brackets) |
| Data Locality | High (Sequential Access aligns with hardware caches) |