Valid Parentheses/String
Overview
Valid Parentheses/String problems involve checking if brackets, parentheses, and braces are properly matched and nested. These problems test your understanding of stack operations, string parsing, and pattern matching.
Key challenges:
- Matching opening and closing brackets
- Handling nested structures
- Multiple bracket types
- Minimum removals to make valid
- Generating valid combinations
Common parentheses problems include:
- Valid parentheses validation
- Minimum removals to make valid
- Generate parentheses combinations
- Longest valid parentheses substring
- Remove invalid parentheses
- Score of parentheses
Pattern 1: Basic Valid Parentheses
Check if parentheses are properly matched.
Stack-based approach:
function isValidParentheses(s: string): boolean {
const stack: string[] = [];
const pairs: Record<string, string> = {
')': '(',
'}': '{',
']': '['
};
for (const char of s) {
if (pairs[char]) {
// Closing bracket
if (stack.length === 0 || stack.pop() !== pairs[char]) {
return false;
}
} else if (char === '(' || char === '{' || char === '[') {
// Opening bracket
stack.push(char);
}
}
return stack.length === 0;
}Time: O(n), Space: O(n)
Counter-based (single type):
function isValidSingleType(s: string): boolean {
let count = 0;
for (const char of s) {
if (char === '(') {
count++;
} else if (char === ')') {
count--;
if (count < 0) return false;
}
}
return count === 0;
}Time: O(n), Space: O(1)
Pattern 2: Minimum Removals to Make Valid
Find minimum removals to make parentheses valid.
function minRemoveToMakeValid(s: string): string {
const stack: number[] = [];
const toRemove = new Set<number>();
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') {
stack.push(i);
} else if (s[i] === ')') {
if (stack.length > 0) {
stack.pop();
} else {
toRemove.add(i);
}
}
}
// Remove unmatched opening parentheses
while (stack.length > 0) {
toRemove.add(stack.pop()!);
}
// Build result
const result: string[] = [];
for (let i = 0; i < s.length; i++) {
if (!toRemove.has(i)) {
result.push(s[i]);
}
}
return result.join('');
}Time: O(n), Space: O(n)
Two-pass approach:
function minRemoveToMakeValidTwoPass(s: string): string {
// First pass: remove invalid ')'
let result: string[] = [];
let openCount = 0;
for (const char of s) {
if (char === '(') {
openCount++;
result.push(char);
} else if (char === ')') {
if (openCount > 0) {
openCount--;
result.push(char);
}
} else {
result.push(char);
}
}
// Second pass: remove invalid '('
let finalResult: string[] = [];
let closeCount = 0;
for (let i = result.length - 1; i >= 0; i--) {
if (result[i] === ')') {
closeCount++;
finalResult.unshift(result[i]);
} else if (result[i] === '(') {
if (closeCount > 0) {
closeCount--;
finalResult.unshift(result[i]);
}
} else {
finalResult.unshift(result[i]);
}
}
return finalResult.join('');
}Pattern 3: Generate Parentheses
Generate all valid combinations of n pairs of parentheses.
function generateParentheses(n: number): string[] {
const results: string[] = [];
const backtrack = (current: string, open: number, close: number): void => {
if (current.length === 2 * n) {
results.push(current);
return;
}
if (open < n) {
backtrack(current + '(', open + 1, close);
}
if (close < open) {
backtrack(current + ')', open, close + 1);
}
};
backtrack('', 0, 0);
return results;
}Time: O(4^n / √n), Space: O(4^n / √n)
Iterative approach:
function generateParenthesesIterative(n: number): string[] {
const results: string[] = [];
const queue: Array<{str: string, open: number, close: number}> = [];
queue.push({str: '', open: 0, close: 0});
while (queue.length > 0) {
const {str, open, close} = queue.shift()!;
if (str.length === 2 * n) {
results.push(str);
continue;
}
if (open < n) {
queue.push({str: str + '(', open: open + 1, close});
}
if (close < open) {
queue.push({str: str + ')', open, close: close + 1});
}
}
return results;
}Pattern 4: Longest Valid Parentheses
Find length of longest valid parentheses substring.
Stack-based:
function longestValidParentheses(s: string): number {
const stack: number[] = [];
stack.push(-1); // Base for calculation
let maxLength = 0;
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') {
stack.push(i);
} else {
stack.pop();
if (stack.length === 0) {
stack.push(i); // New base
} else {
maxLength = Math.max(maxLength, i - stack[stack.length - 1]);
}
}
}
return maxLength;
}Time: O(n), Space: O(n)
Two-pointer approach:
function longestValidParenthesesTwoPointer(s: string): number {
let maxLength = 0;
// Left to right
let left = 0, right = 0;
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') left++;
else right++;
if (left === right) {
maxLength = Math.max(maxLength, 2 * right);
} else if (right > left) {
left = right = 0;
}
}
// Right to left
left = right = 0;
for (let i = s.length - 1; i >= 0; i--) {
if (s[i] === '(') left++;
else right++;
if (left === right) {
maxLength = Math.max(maxLength, 2 * left);
} else if (left > right) {
left = right = 0;
}
}
return maxLength;
}Time: O(n), Space: O(1)
Pattern 5: Remove Invalid Parentheses
Remove minimum number of invalid parentheses to make string valid.
function removeInvalidParentheses(s: string): string[] {
const results = new Set<string>();
let minRemovals = Infinity;
const backtrack = (
current: string,
index: number,
open: number,
close: number,
removals: number
): void => {
if (index === s.length) {
if (open === close && removals <= minRemovals) {
if (removals < minRemovals) {
results.clear();
minRemovals = removals;
}
results.add(current);
}
return;
}
const char = s[index];
if (char !== '(' && char !== ')') {
backtrack(current + char, index + 1, open, close, removals);
return;
}
// Skip current character
backtrack(current, index + 1, open, close, removals + 1);
// Keep current character
if (char === '(') {
backtrack(current + char, index + 1, open + 1, close, removals);
} else if (close < open) {
backtrack(current + char, index + 1, open, close + 1, removals);
}
};
backtrack('', 0, 0, 0, 0);
return Array.from(results);
}Time: O(2^n), Space: O(n)
Optimized with pruning:
function removeInvalidParenthesesOptimized(s: string): string[] {
const results = new Set<string>();
// Count invalid parentheses
let leftRem = 0, rightRem = 0;
for (const char of s) {
if (char === '(') leftRem++;
else if (char === ')') {
if (leftRem > 0) leftRem--;
else rightRem++;
}
}
const backtrack = (
current: string,
index: number,
leftCount: number,
rightCount: number,
leftRem: number,
rightRem: number
): void => {
if (index === s.length) {
if (leftRem === 0 && rightRem === 0) {
results.add(current);
}
return;
}
const char = s[index];
// Skip character
if ((char === '(' && leftRem > 0) || (char === ')' && rightRem > 0)) {
backtrack(
current,
index + 1,
leftCount,
rightCount,
leftRem - (char === '(' ? 1 : 0),
rightRem - (char === ')' ? 1 : 0)
);
}
// Keep character
if (char !== '(' && char !== ')') {
backtrack(current + char, index + 1, leftCount, rightCount, leftRem, rightRem);
} else if (char === '(') {
backtrack(current + char, index + 1, leftCount + 1, rightCount, leftRem, rightRem);
} else if (rightCount < leftCount) {
backtrack(current + char, index + 1, leftCount, rightCount + 1, leftRem, rightRem);
}
};
backtrack('', 0, 0, 0, leftRem, rightRem);
return Array.from(results);
}Pattern 6: Score of Parentheses
Calculate score based on parentheses nesting.
function scoreOfParentheses(s: string): number {
const stack: number[] = [];
let score = 0;
for (const char of s) {
if (char === '(') {
stack.push(score);
score = 0;
} else {
score = stack.pop()! + Math.max(2 * score, 1);
}
}
return score;
}Time: O(n), Space: O(n)
Recursive approach:
function scoreOfParenthesesRecursive(s: string): number {
return scoreHelper(s, 0, s.length - 1);
}
function scoreHelper(s: string, left: number, right: number): number {
if (right - left === 1) return 1; // "()"
let balance = 0;
for (let i = left; i < right; i++) {
if (s[i] === '(') balance++;
else balance--;
if (balance === 0) {
// Balanced split
return scoreHelper(s, left, i) + scoreHelper(s, i + 1, right);
}
}
// Nested case: (A) = 2 * A
return 2 * scoreHelper(s, left + 1, right - 1);
}Pattern 7: Valid Parentheses with Wildcards
Check validity when '*' can represent '(', ')', or empty.
function checkValidString(s: string): boolean {
let minOpen = 0, maxOpen = 0;
for (const char of s) {
if (char === '(') {
minOpen++;
maxOpen++;
} else if (char === ')') {
minOpen = Math.max(0, minOpen - 1);
maxOpen--;
if (maxOpen < 0) return false;
} else { // '*'
minOpen = Math.max(0, minOpen - 1);
maxOpen++;
}
}
return minOpen === 0;
}Time: O(n), Space: O(1)
Pattern 8: Different Bracket Types
Handle multiple types of brackets.
function isValidMultipleBrackets(s: string): boolean {
const stack: string[] = [];
const pairs: Record<string, string> = {
')': '(',
'}': '{',
']': '[',
'>': '<'
};
for (const char of s) {
if (pairs[char]) {
if (stack.length === 0 || stack.pop() !== pairs[char]) {
return false;
}
} else if (Object.values(pairs).includes(char)) {
stack.push(char);
}
}
return stack.length === 0;
}Custom bracket pairs:
function isValidCustomBrackets(
s: string,
openBrackets: string[],
closeBrackets: string[]
): boolean {
const stack: string[] = [];
const openSet = new Set(openBrackets);
const closeToOpen = new Map<string, string>();
for (let i = 0; i < openBrackets.length; i++) {
closeToOpen.set(closeBrackets[i], openBrackets[i]);
}
for (const char of s) {
if (openSet.has(char)) {
stack.push(char);
} else if (closeToOpen.has(char)) {
if (stack.length === 0 || stack.pop() !== closeToOpen.get(char)) {
return false;
}
}
}
return stack.length === 0;
}Pattern 9: Count Valid Parentheses Substrings
Count number of valid parentheses substrings.
function countValidSubstrings(s: string): number {
let count = 0;
for (let i = 0; i < s.length; i++) {
let balance = 0;
for (let j = i; j < s.length; j++) {
if (s[j] === '(') balance++;
else balance--;
if (balance < 0) break;
if (balance === 0) count++;
}
}
return count;
}Time: O(n^2), Space: O(1)
Optimized:
function countValidSubstringsOptimized(s: string): number {
let count = 0;
const dp: number[] = new Array(s.length).fill(0);
for (let i = 1; i < s.length; i++) {
if (s[i] === ')') {
if (s[i - 1] === '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s[i - dp[i - 1] - 1] === '(') {
dp[i] = dp[i - 1] + (i - dp[i - 1] >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
count += dp[i] > 0 ? 1 : 0;
}
}
return count;
}Pattern 10: Minimum Additions to Make Valid
Find minimum additions needed to make parentheses valid.
function minAddToMakeValid(s: string): number {
let openNeeded = 0;
let closeNeeded = 0;
for (const char of s) {
if (char === '(') {
closeNeeded++;
} else if (char === ')') {
if (closeNeeded > 0) {
closeNeeded--;
} else {
openNeeded++;
}
}
}
return openNeeded + closeNeeded;
}Time: O(n), Space: O(1)
Pattern 11: Check if String Can Be Valid
Check if string can be made valid by swapping characters.
function canBeValid(s: string, locked: string): boolean {
if (s.length % 2 !== 0) return false;
let open = 0, close = 0, unlocked = 0;
// Left to right
for (let i = 0; i < s.length; i++) {
if (locked[i] === '0') {
unlocked++;
} else if (s[i] === '(') {
open++;
} else {
close++;
}
if (close > open + unlocked) return false;
}
open = close = unlocked = 0;
// Right to left
for (let i = s.length - 1; i >= 0; i--) {
if (locked[i] === '0') {
unlocked++;
} else if (s[i] === ')') {
close++;
} else {
open++;
}
if (open > close + unlocked) return false;
}
return true;
}Time: O(n), Space: O(1)
Pattern 12: Maximum Nesting Depth
Find maximum nesting depth of valid parentheses.
function maxDepth(s: string): number {
let maxDepth = 0;
let currentDepth = 0;
for (const char of s) {
if (char === '(') {
currentDepth++;
maxDepth = Math.max(maxDepth, currentDepth);
} else if (char === ')') {
currentDepth--;
}
}
return maxDepth;
}Time: O(n), Space: O(1)
For nested structures:
function maxNestingDepth(s: string): number {
let maxDepth = 0;
const stack: number[] = [];
for (const char of s) {
if (char === '(' || char === '{' || char === '[') {
stack.push(1);
maxDepth = Math.max(maxDepth, stack.length);
} else if (char === ')' || char === '}' || char === ']') {
if (stack.length > 0) {
stack.pop();
}
}
}
return maxDepth;
}When to Use Parentheses Techniques
Use parentheses validation when:
- Problem mentions "valid parentheses", "brackets", or "braces"
- Need to match opening/closing pairs
- Nested structures validation
- Minimum operations to make valid
- Generate combinations of parentheses
- Calculate scores based on nesting
Template (Stack-based)
function isValidParenthesesTemplate(s: string): boolean {
const stack: string[] = [];
const pairs: Record<string, string> = {
')': '(',
'}': '{',
']': '['
};
for (const char of s) {
if (pairs[char]) {
if (stack.length === 0 || stack.pop() !== pairs[char]) {
return false;
}
} else if (Object.values(pairs).includes(char)) {
stack.push(char);
}
}
return stack.length === 0;
}Template (Counter-based)
function isValidCounterTemplate(s: string): boolean {
let count = 0;
for (const char of s) {
if (char === '(') count++;
else if (char === ')') {
count--;
if (count < 0) return false;
}
}
return count === 0;
}Time and Space Complexity Summary
- Basic validation: O(n) time, O(n) space (stack), O(1) space (counter)
- Minimum removals: O(n) time, O(n) space
- Generate parentheses: O(4^n / √n) time, O(4^n / √n) space
- Longest valid: O(n) time, O(n) space (stack), O(1) space (two-pointer)
- Remove invalid: O(2^n) time, O(n) space
- Score calculation: O(n) time, O(n) space
Practice Tips
- Use stack — For multiple bracket types
- Use counter — For single type, space-efficient
- Process right to left — For some problems
- Track balance — Count open vs close
- Handle edge cases — Empty strings, all open/close
- Prune early — In backtracking solutions
Common Mistakes
- Not checking stack empty — Before popping
- Wrong bracket matching — Verify pairs correctly
- Off-by-one errors — Careful with indices
- Not resetting counters — In nested loops
- Missing base cases — In recursive solutions
Related Concepts
- Stack Operations — For bracket matching
- String Parsing — Processing bracket strings
- Backtracking — For generating combinations
- Dynamic Programming — For optimization problems
Valid parentheses problems are fundamental in string algorithms. Master these patterns, and you'll be well-prepared for bracket-related interview questions.