This is a classic stack-matching problem. The key idea is that every opening bracket creates a future expectation about which closing bracket must appear next.
Problem 1: Valid Parentheses
Problem description:
Given a string containing only the characters (, ), {, }, [ and ], return true if the brackets are properly matched and nested; otherwise return false.
What we are solving actually: We are not just checking that the counts of opening and closing brackets match. The order matters. The most recently opened bracket must be the first one to close, which is exactly LIFO behavior.
What we are doing actually:
- Scan the string from left to right.
- When we see an opening bracket, push the closing bracket we expect later.
- When we see a closing bracket, it must match the top expectation on the stack.
- At the end, the stack must be empty.
class Solution {
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(')'); // Record exactly which closer must appear for this opener.
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
if (stack.isEmpty() || stack.pop() != c) {
return false; // Wrong closer, or no opener exists for it.
}
}
}
return stack.isEmpty(); // Leftover expectations mean some opener was never closed.
}
}
Debug steps:
- print the stack after each character to see current closing expectations
- test
"()[]{}","([)]", and"{[]}"to separate valid nesting from invalid ordering - verify the invariant that the stack always contains the closers required for currently open brackets
Why a Stack Fits Perfectly
Suppose we read:
{[(
The next closer must be:
)
not ] or }.
That is why the most recent opener must be handled first. This is exactly last-in, first-out behavior, so a stack is the natural data structure.
Dry Run
Input: "{[()]}"
- read
{-> push} - read
[-> push] - read
(-> push) - read
)-> pop), match - read
]-> pop], match - read
}-> pop}, match
Stack ends empty, so the string is valid.
Invalid example: "([)]"
- read
(-> expect) - read
[-> expect]on top - read
)-> top expectation is], mismatch
So the string is invalid.
Why Push Expected Closers
Another implementation pushes opening brackets and maps them later. Pushing the expected closing bracket is often cleaner because the closing step becomes one direct comparison:
stack.pop() == currentChar
That removes extra mapping logic in the mismatch branch.
Common Mistakes
- checking only counts of brackets instead of nesting order
- popping without first checking whether the stack is empty
- forgetting to verify the stack is empty after the scan
- using legacy
Stackinstead ofArrayDeque
Boundary Cases
- empty string -> valid
- starts with closing bracket -> invalid immediately
- leftover openers at the end -> invalid
- correctly nested mixed types -> valid
Complexity
- Time:
O(n) - Space:
O(n)
Key Takeaways
- matching brackets is a stack problem because nesting is LIFO
- storing expected closers makes the validation logic compact
- a valid answer needs both correct local matches and an empty stack at the end
Pattern Extension
One good review question for valid parentheses in java is whether the same invariant still holds when the input becomes degenerate: empty arrays, repeated values, already-sorted data, or the smallest possible string. That quick pressure test usually reveals whether we truly understood the pattern or only copied the happy path.
Comments