Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Valid Parentheses
The canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript.
The problem
Given a string s of just the characters ()[]{}, decide whether it is valid: every open bracket is closed by a bracket of the same type, and brackets close in the right order (the most recently opened closes first). Return a boolean.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement is_valid(s): return True only if the brackets are balanced and correctly nested. Push opens onto a stack; on a close, the top of the stack must be its matching open.
Examples and constraints 6 examples
s = "()"trues = "()[]{}"trues = "(]"falses = "([)]"falses = "{[]}"trues = ")("false- 1 ≤ s.length ≤ 10⁴
- s contains only the six bracket characters.
- A stack gives O(n) time and O(n) space.
Hints 4 ideas
- The most recently opened bracket must be the first one closed — that is exactly a stack (last in, first out).
- Push every opening bracket. On a closing bracket, pop and check it matches.
- A map from closing → opening makes the match check one lookup.
- It is invalid if a close finds an empty stack (nothing to match) — and at the end the stack must be empty.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious approach deletes matched pairs until nothing changes: repeat s = s.replace("()", "").replace("[]", "").replace("{}", "") and answer whether s ends up empty. It works because a valid string always contains an adjacent matched pair (its innermost one), and deleting that pair keeps a valid string valid and an invalid one invalid. Each pass is O(n) time and builds a new string, O(n) space. It is too slow on deep nesting: 5,000 ( followed by 5,000 ) exposes only the innermost pair per pass, so it takes 5,001 passes over a 10,000-character string, O(n²) time.
The approach
The bracket that may close next is always the most recently opened one that is still open, so the open brackets come back in last-in, first-out order: a stack. The invariant: after any prefix, stack holds exactly that prefix’s unmatched openers in the order they were opened, so its top is the only opener a closing bracket may match.
The reference maps each closer to its opener in pairs = {')': '(', ']': '[', '}': '{'}. For each ch, an opener (ch in pairs.values()) is pushed. A closer must find a non-empty stack whose popped top equals pairs[ch]: the single check if not stack or stack.pop() != pairs[ch] returns False for a stray closer, a wrong type and a wrong order alike, and short-circuiting means pop() never runs on an empty list. At the end, return not stack rejects anything still open, such as '(('.
Walk through an example
The fifth example, s = "{[]}", where nesting takes the stack two deep.
| step | ch | kind | action | stack after |
|---|---|---|---|---|
| 1 | { | opener | push | ['{'] |
| 2 | [ | opener | push | ['{', '['] |
| 3 | ] | closer | pop '[', equals pairs[']'] | ['{'] |
| 4 | } | closer | pop '{', equals pairs['}'] | [] |
| end | — | — | return not stack | True |
Complexity
Stack: O(n) time, one pass with constant work per character (the dict and pairs.values() checks cover three entries), and O(n) space in the worst case, when the string is all openers. Repeated pair removal: O(n²) time, up to about n/2 passes of O(n) each on nested input, and O(n) space for each rebuilt string.
Edge cases to test
- Only openers,
"(((":false. Every character is pushed and nothing fails inside the loop; only the finalreturn not stackcatches it. - A closer on an empty stack,
")(":falseat the first character. In Python an unguardedstack.pop()raisesIndexError; in TypeScriptpop()returnsundefined, which never equals an opener, so the reference returns false safely. - Right counts, wrong order,
"([)]":false. Each type has one opener and one closer, so a check with one counter per type accepts it; the stack sees)arrive while[is on top. - Deep nesting, 5,000
(then 5,000):true, with the stack 5,000 deep at its peak. A recursive matcher that recurses once per level passes Python’s default recursion limit of 1,000. - Odd length, e.g.
"(()": alwaysfalse, which makeslen(s) % 2 == 1a valid early exit. - The empty string: the reference returns
True, since nothing is left open, although the constraints promise at least one character.
Common mistakes
- Not checking the stack is empty at the end, so
"((("is wrongly accepted. - Popping an empty stack on a stray closing bracket — check emptiness first.
- Only counting brackets rather than matching types and order, so
"([)]"slips through — it must fail.
Follow-ups an interviewer asks next
- Return the position of the first error instead of a boolean.
- Push indices instead of characters. A closer that meets an empty stack or a mismatched top is the error, so return its index. If the loop finishes with indices still on the stack, every one of them is an unclosed opener: report the innermost,
stack[-1], or the outermost,stack[0], depending on what an editor should highlight. - The string also contains letters, digits, or real code.
- Skip any character that is neither an opener nor a closer; the reference already does, because such a character fails both the
ifand theelif, so"a(b)c"is valid. Real code adds string literals and comments, where brackets do not count, so a real checker tracks lexer state, such as being inside a string, before it consults the stack. - Only one bracket type. Can you use O(1) space?
- Yes. With a single type the stack only ever holds copies of
(, so its height is all the information it carries: a counter that rises on(and falls on), failing if it ever goes below zero and requiring zero at the end. With two or more types the order of the types matters, and no fixed set of counters records it. - Generate every valid string of n pairs.
- Backtrack with two counters: add
(while fewer than n have been opened, and add)while closers trail openers. Every string completed at length 2n is valid by construction, so no check is needed. There are Catalan(n) of them, 42 for n = 5, so the size of the output, not the checking, dominates the cost.
Where this shows up
This is the canonical stack problem, and the “match nested pairs with a stack” pattern is exactly how parsers, expression evaluators, JSON/XML validators, and your editor’s bracket-matching work. Recognizing that “most recent unmatched open” means “stack” is the transferable insight.
Reference solution
Reference solution — Python
def is_valid(s):
pairs = {')': '(', ']': '[', '}': '{'} # closing -> matching opening
stack = []
for ch in s:
if ch in pairs.values(): # an opening bracket
stack.append(ch)
elif ch in pairs: # a closing bracket
# must match the most recent unmatched open
if not stack or stack.pop() != pairs[ch]:
return False
return not stack # valid only if nothing is left open
print(is_valid("()[]{}")) # -> True
print(is_valid("([)]")) # -> FalseReference solution — TypeScript
function isValid(s: string): boolean {
const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
const opens = new Set(Object.values(pairs));
const stack: string[] = [];
for (const ch of s) {
if (opens.has(ch)) {
stack.push(ch); // opening: remember it
} else if (ch in pairs) {
// closing: top of stack must be the matching open
if (stack.pop() !== pairs[ch]) return false;
}
}
return stack.length === 0; // valid only if nothing is left open
}
console.log(isValid("()[]{}")); // -> true
console.log(isValid("([)]")); // -> falseQuestions about Valid Parentheses
Why is a stack the right data structure for valid parentheses?
Because brackets must close in the reverse of the order they opened. The bracket that may close next is always the most recently opened one that is still open, and a stack is exactly the structure that hands back the most recent item first. Pushing every opener and popping on every closer keeps the unmatched openers in order, so checking a closer is a single comparison with the top. A queue would hand back the oldest opener instead, which is the wrong one for any nested string such as {[]}.
Can valid parentheses be solved with a counter instead of a stack?
Only when there is a single bracket type. With just round brackets the stack holds nothing but copies of one character, so its height is the only information, and a counter that rises on an opener and falls on a closer, failing if it goes negative and requiring zero at the end, is enough in constant space. With three types, counters lose the order: ([)] has one opener and one closer of each type, so every count balances, yet it is invalid because the round bracket closes while the square one is still open.
What is the time and space complexity of the stack solution?
Time is linear in the length of the string: every character is visited once and does a constant amount of work, either a push or a pop and one comparison. Space is linear in the worst case, when the string is all openers and every one of them sits on the stack, and constant when brackets close as soon as they open. The approach that repeatedly deletes adjacent matched pairs is also correct but quadratic, because deeply nested input exposes only one pair per pass.
Is an empty string valid parentheses?
Yes, under the usual definition: it contains no bracket left open and none closed by the wrong type, so there is nothing to violate. The reference returns True for it, because the loop never runs and the stack is empty at the end. This problem’s constraints promise at least one character, so the tests never check it, but returning True keeps the definition consistent: joining two valid strings gives a valid string, and the empty string is the identity for that.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- SoftmaxThe function at the end of every classifier and language model: turn raw scores (logits) into a probability distribution. Implement the numerically stable version so big logits do not overflow. Solve it in Python or TypeScript.Read →
- Cosine SimilarityThe measure behind every embedding search and RAG system: how aligned are two vectors, ignoring their length? Dot product over the product of magnitudes — 1 identical, 0 orthogonal, -1 opposite. Solve it in Python or TypeScript.Read →
- Top-K RetrievalThe core of the "R" in RAG: given a query embedding and a set of document embeddings, return the indices of the k most similar docs by cosine similarity, with a stable tie-break. Solve it in Python or TypeScript.Read →
- Token-Level F1The metric behind QA evaluation (SQuAD and friends): how well does a predicted answer overlap a reference as a bag of words? Compute token precision and recall, then their harmonic-mean F1. Solve it in Python or TypeScript.Read →