[Easy] 20. Valid Parentheses
[Easy] 20. Valid Parentheses
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.
Examples
Example 1:
Input: s = "()"
Output: true
Example 2:
Input: s = "()[]{}"
Output: true
Example 3:
Input: s = "(]"
Output: false
Example 4:
Input: s = "([)]"
Output: false
Example 5:
Input: s = "{[]}"
Output: true
Constraints
1 <= s.length <= 10^4sconsists of parentheses only'()[]{}'.
Clarification Questions
Before diving into the solution, here are 5 important clarifications and assumptions to discuss during an interview:
-
Valid parentheses definition: What makes parentheses valid? (Assumption: Every opening bracket has matching closing bracket, properly nested - no cross-nesting)
-
Bracket types: What bracket types are there? (Assumption: Three types - ‘()’, ‘[]’, ‘{}’ - each must match its own type)
-
Nesting rules: Can brackets be nested? (Assumption: Yes - but must be properly nested, e.g., “([])” is valid, “([)]” is not)
-
Return value: What should we return? (Assumption: Boolean - true if valid, false otherwise)
-
Empty string: What if string is empty? (Assumption: Return true - empty string is valid)
Interview Deduction Process (10 minutes)
Step 1: Brute-Force Approach (2 minutes)
Initial Thought: “I need to check if parentheses are valid. Let me think about counting opening and closing brackets.”
Naive Solution: Count opening and closing brackets of each type. Check if counts match and if they’re properly nested by scanning multiple times.
Complexity: O(n) time, O(1) space (but incorrect logic)
Issues:
- Simple counting doesn’t handle nesting order correctly
- Can’t distinguish between “([)]” (invalid) and “([])” (valid) with just counts
- Doesn’t track the order of bracket types
- Fails for nested structures
Step 2: Semi-Optimized Approach (3 minutes)
Insight: “I need to track the order of opening brackets. A stack can help maintain the order.”
Improved Solution: Use a stack to track opening brackets. When encountering a closing bracket, check if it matches the most recent opening bracket. If stack is empty at the end, all brackets are matched.
Complexity: O(n) time, O(n) space
Improvements:
- Correctly handles nesting order
- Tracks bracket type matching
- Single pass through string
- Handles all edge cases properly
Step 3: Optimized Solution (5 minutes)
Final Optimization: “The stack approach is already optimal. Let me verify edge cases and consider if we can optimize space.”
Best Solution: Stack-based approach is optimal. Space can’t be reduced below O(n) worst case (e.g., all opening brackets). Consider using a counter for single bracket type, but stack is needed for multiple types.
Key Realizations:
- Stack is the natural data structure for matching problems
- O(n) space is necessary for worst case
- Single pass O(n) time is optimal
- Stack approach handles all bracket types elegantly
Solution: Stack-Based Matching
Time Complexity: O(n)
Space Complexity: O(n)
Use a stack to track opening brackets. When encountering a closing bracket, check if it matches the most recent opening bracket. If stack is empty at the end, all brackets are matched.
class Solution {
public:
bool isValid(string s) {
stack<char> st;
unordered_map<char, char> map = {
{'}', '{'},
{']', '['},
{')', '('}
};
for(char c: s) {
if(c == '{' || c == '[' || c == '(') {
st.push(c);
} else {
if(st.empty() || st.top() != map[c]) return false;
st.pop();
}
}
return st.empty();
}
};
How the Algorithm Works
Key Insight: LIFO (Last In, First Out)
Parentheses must be closed in the reverse order they were opened. This matches the stack’s LIFO property perfectly.
Step-by-Step Example: s = "([{}])"
| Step | Char | Action | Stack | Valid? |
|---|---|---|---|---|
| 0 | - | Initialize | [] | - |
| 1 | ( |
Push | ['('] |
- |
| 2 | [ |
Push | ['(', '['] |
- |
| 3 | { |
Push | ['(', '[', '{'] |
- |
| 4 | } |
Pop: { matches |
['(', '['] |
Yes |
| 5 | ] |
Pop: [ matches |
['('] |
Yes |
| 6 | ) |
Pop: ( matches |
[] |
Yes |
Final Check: Stack is empty → true
Step-by-Step Example: s = "([)]"
| Step | Char | Action | Stack | Valid? |
|---|---|---|---|---|
| 1 | ( |
Push | ['('] |
- |
| 2 | [ |
Push | ['(', '['] |
- |
| 3 | ) |
Check: ( matches |
['['] |
Yes |
| 4 | ] |
Check: [ matches |
[] |
Yes |
Wait, that’s incorrect. Let me recalculate:
| Step | Char | Action | Stack | Valid? |
|---|---|---|---|---|
| 1 | ( |
Push | ['('] |
- |
| 2 | [ |
Push | ['(', '['] |
- |
| 3 | ) |
Check: top is [, not ( |
['(', '['] |
No → Return false |
Final Answer: false
Visual Representation
Valid: "([{}])"
( [ { } ] )
↑ ↑ ↑ ↑ ↑ ↑
1 2 3 4 5 6
Stack progression:
1: ['(']
2: ['(', '[']
3: ['(', '[', '{']
4: ['(', '['] (matched {})
5: ['('] (matched [])
6: [] (matched ())
Result: true ✓
Invalid: "([)]"
( [ ) ]
↑ ↑ ↑ ↑
1 2 3 4
Stack progression:
1: ['(']
2: ['(', '[']
3: ['(', '['] (trying to match ) with [, fails!)
Result: false ✗
Key Insights
- Stack for LIFO: Opening brackets must close in reverse order
- Map for Matching: Use hash map to map closing to opening brackets
- Empty Stack Check: All brackets matched if stack is empty at end
- Early Return: Return false immediately on mismatch or empty stack with closing bracket
Algorithm Breakdown
1. Initialize Stack and Map
stack<char> st;
unordered_map<char, char> map = {
{'}', '{'},
{']', '['},
{')', '('}
};
- Stack: Stores opening brackets
- Map: Maps closing brackets to their corresponding opening brackets
2. Process Opening Brackets
if(c == '{' || c == '[' || c == '(') {
st.push(c);
}
- Push opening brackets onto stack
- They will be matched later when closing brackets appear
3. Process Closing Brackets
else {
if(st.empty() || st.top() != map[c]) return false;
st.pop();
}
- Check if stack is empty: No opening bracket to match
- Check if top matches: Most recent opening bracket must match current closing bracket
- Pop if matched: Remove the matched opening bracket
4. Final Validation
return st.empty();
- If stack is empty, all brackets were matched
- If stack has remaining elements, some opening brackets were never closed
Complexity Analysis
| Aspect | Complexity |
|---|---|
| Time | O(n) - Single pass through string, each operation is O(1) |
| Space | O(n) - Stack can hold at most n/2 opening brackets in worst case |
Alternative Approaches
Approach 1: Without Hash Map (Explicit Checks)
bool isValid(string s) {
stack<char> st;
for(char c: s) {
if(c == '(' || c == '[' || c == '{') {
st.push(c);
} else {
if(st.empty()) return false;
char top = st.top();
st.pop();
if((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return st.empty();
}
Time Complexity: O(n)
Space Complexity: O(n)
Approach 2: Using String as Stack
bool isValid(string s) {
string stack = "";
for(char c: s) {
if(c == '(' || c == '[' || c == '{') {
stack += c;
} else {
if(stack.empty()) return false;
char last = stack.back();
stack.pop_back();
if((c == ')' && last != '(') ||
(c == ']' && last != '[') ||
(c == '}' && last != '{')) {
return false;
}
}
}
return stack.empty();
}
Time Complexity: O(n)
Space Complexity: O(n)
Note: This is less efficient due to string operations, but uses less code.
Approach 3: Counter-Based (Only for Single Type)
// Only works for single bracket type like "()"
bool isValid(string s) {
int count = 0;
for(char c: s) {
if(c == '(') count++;
else count--;
if(count < 0) return false;
}
return count == 0;
}
Time Complexity: O(n)
Space Complexity: O(1)
Limitation: Doesn’t work for multiple bracket types like "([)]".
Comparison of Approaches
| Approach | Time | Space | Pros | Cons |
|---|---|---|---|---|
| Stack + Map | O(n) | O(n) | Clean, extensible | Slight overhead |
| Stack + Explicit | O(n) | O(n) | No map needed | More verbose |
| String Stack | O(n) | O(n) | Simple code | Less efficient |
| Counter | O(n) | O(1) | Space optimal | Only single type |
Edge Cases
- Empty string:
""→true(valid by definition) - Single bracket:
"("or")"→false - Only opening:
"((("→false - Only closing:
")))"→false - Nested valid:
"([{}])"→true - Interleaved invalid:
"([)]"→false - Mixed valid:
"()[]{}"→true
Common Mistakes
- Not checking stack empty: Forgetting to check
st.empty()beforest.top() - Wrong map direction: Mapping opening → closing instead of closing → opening
- Not returning false immediately: Continuing after finding a mismatch
- Forgetting final check: Not checking if stack is empty at the end
- Using wrong comparison: Comparing
st.top() == cinstead ofst.top() == map[c]
Detailed Example Walkthrough
Example 1: s = "([{}])"
Step 0: Initialize
st = []
map = {')': '(', ']': '[', '}': '{'}
Step 1: c = '('
Opening bracket → push
st = ['(']
Step 2: c = '['
Opening bracket → push
st = ['(', '[']
Step 3: c = '{'
Opening bracket → push
st = ['(', '[', '{']
Step 4: c = '}'
Closing bracket → check
st.empty()? No
st.top() = '{'
map['}'] = '{'
'{' == '{'? Yes → pop
st = ['(', '[']
Step 5: c = ']'
Closing bracket → check
st.empty()? No
st.top() = '['
map[']'] = '['
'[' == '['? Yes → pop
st = ['(']
Step 6: c = ')'
Closing bracket → check
st.empty()? No
st.top() = '('
map[')'] = '('
'(' == '('? Yes → pop
st = []
Final: st.empty()? Yes → return true ✓
Example 2: s = "([)]"
Step 0: Initialize
st = []
Step 1: c = '('
Opening bracket → push
st = ['(']
Step 2: c = '['
Opening bracket → push
st = ['(', '[']
Step 3: c = ')'
Closing bracket → check
st.empty()? No
st.top() = '['
map[')'] = '('
'[' == '('? No → return false ✗
Why Stack Works
LIFO Property
Parentheses matching requires Last In, First Out:
- Most recent opening bracket must match the next closing bracket
- Stack naturally provides LIFO behavior
Example: "([{}])"
Opening order: ( [ {
Closing order: } ] )
↑
Must close in reverse order
Counter Example: "([)]"
Opening order: ( [
Closing order: ) ]
↑
Wrong order! Can't close ')' before ']'
Optimization Tips
Early Exit
Already optimized - we return false immediately on mismatch.
Memory Optimization
For very large strings, consider using a string as stack (if your use case allows) to potentially reduce allocations.
Branch Prediction
The hash map lookup is very fast, but explicit checks might be slightly faster due to branch prediction:
if(c == ')') {
if(st.empty() || st.top() != '(') return false;
st.pop();
}
Related Problems
- 22. Generate Parentheses - Generate all valid parentheses
- 32. Longest Valid Parentheses - Find longest valid substring
- 301. Remove Invalid Parentheses - Remove minimum to make valid
- 1249. Minimum Remove to Make Valid Parentheses - Remove invalid characters
- 1541. Minimum Insertions to Balance a Parentheses String - Add minimum to balance
Pattern Recognition
This problem demonstrates the Stack for Matching pattern:
- Use stack when you need to match elements in reverse order
- Perfect for nested structures (parentheses, brackets, tags)
- LIFO property naturally handles nested matching
Key Insight:
- Opening brackets → push
- Closing brackets → pop and verify match
- Stack empty at end → all matched
Applications:
- HTML/XML tag validation
- Expression evaluation
- Function call tracking
- Nested structure parsing
Code Quality Notes
- Readability: Hash map makes code clean and extensible
- Efficiency: Optimal O(n) time and space
- Correctness: Handles all edge cases properly
- Maintainability: Easy to add new bracket types
Extending to More Bracket Types
The solution easily extends to other bracket types:
unordered_map<char, char> map = {
{'}', '{'},
{']', '['},
{')', '('},
{'>', '<'} // Add new type
};
// Check also includes '<'
if(c == '{' || c == '[' || c == '(' || c == '<') {
st.push(c);
}
This is a fundamental stack problem that demonstrates the LIFO property perfectly. It’s an excellent introduction to stack-based algorithms and pattern matching.