Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°02

Valid Parentheses

Easy StackStrings

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.

SOLVE IT YOURSELF

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.

YOUR TASK

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.

CPython · WebAssembly
Examples and constraints 6 examples
EXAMPLE 1
Input s = "()"
Output true
EXAMPLE 2
Input s = "()[]{}"
Output true
EXAMPLE 3
Input s = "(]"
Output false
wrong type
EXAMPLE 4
Input s = "([)]"
Output false
wrong order
EXAMPLE 5
Input s = "{[]}"
Output true
properly nested
EXAMPLE 6
Input s = ")("
Output false
edge case: the closer arrives while the stack is still empty
CONSTRAINTS
  • 1 ≤ s.length ≤ 10⁴
  • s contains only the six bracket characters.
  • A stack gives O(n) time and O(n) space.
Hints 4 ideas
HINTS — 4 IDEAS
  1. The most recently opened bracket must be the first one closed — that is exactly a stack (last in, first out).
  2. Push every opening bracket. On a closing bracket, pop and check it matches.
  3. A map from closing → opening makes the match check one lookup.
  4. 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.

stepchkindactionstack after
1{openerpush['{']
2[openerpush['{', '[']
3]closerpop '[', equals pairs[']']['{']
4}closerpop '{', equals pairs['}'][]
end——return not stackTrue

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 final return not stack catches it.
  • A closer on an empty stack, ")(": false at the first character. In Python an unguarded stack.pop() raises IndexError; in TypeScript pop() returns undefined, 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. "(()": always false, which makes len(s) % 2 == 1 a 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 if and the elif, 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("([)]"))     # -> False
Reference 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("([)]"));         // -> false

Questions 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.

▶  Watch it explained
Finished this one? 0 / 76 Challenges done
NextMin Stack Next on the “Interview classics” ladder · rung 4 of 10

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.