A billing config interpolator mixed (), {}, and [] in a JSON-ish template — sections, maps, and filters. The intern counted opens and closes of each type. Staging’s ()[]{} shipped clean. A generated filter ([)] had one of each pair, the counter returned valid, and the parser died: the first ) was sitting on a [.

Valid Parentheses asks whether every open is closed by the matching type, in the right order. A per-type count uses that and still accepts ([)]. Matching is order, not totals.

This is an interview writeup, not a LIFO lecture. The stack post owns push, pop, and why last-in is the ADT. Here we only care about the unmatched opens from the left so a close is a pop, not a recount.

The problem

Given a string s of brackets (), {}, and [] only, return whether it is valid: every open is closed by the same type, and a close matches the most recent unmatched open. The empty string is valid.

s = "()"      →  true
s = "()[]{}"  →  true
s = "(]"      →  false
s = "([)]"    →  false
s = "{[]}"    →  true
s = ""        →  true

Note: Enumerating every valid string of this length is a different problem — Generate Parentheses. This prompt asks you to check one string.

A backward scan is the honest brute force

A running count per type is not a brute force — ([)] balances each family and is still invalid. The honest quadratic check does not count: for each close, scan backward for the nearest unmatched character, which must be the matching open, then mark both used. Leftover unmatched opens fail.

boolean isValidScan(String s) {
    int n = s.length();
    boolean[] matched = new boolean[n];
    for (int i = 0; i < n; i++) {
        char c = s.charAt(i);
        if (c == '(' || c == '{' || c == '[') {
            continue;
        }
        int j = i - 1;
        while (j >= 0 && matched[j]) {
            j--;
        }
        if (j < 0) {
            return false;
        }
        char o = s.charAt(j);
        if ((c == ')' && o != '(')
                || (c == '}' && o != '{')
                || (c == ']' && o != '[')) {
            return false;
        }
        matched[j] = true;
        matched[i] = true;
    }
    for (boolean m : matched) {
        if (!m) {
            return false;
        }
    }
    return true;
}

At n = 20 the leftward scan is a rounding error. At n in the tens of thousands you paid a nested walk for a question whose nearest unmatched open is already the top of a stack.

Push opens, pop on a match

Walk left to right. Odd length cannot pair — return false before you allocate.

  • Open: push.
  • Close: the stack must be non-empty and peek must match that close; then pop.
  • End of string: the stack is empty. Leftover opens or a mismatch → false.

The nearest unmatched open is no longer a scan. It is the top.

s = "([{}])"

(     push            stack: (
[     push            stack: ( [
{     push            stack: ( [ {
}     peek {  match   pop     stack: ( [
]     peek [  match   pop     stack: (
)     peek (  match   pop     stack: empty
end   empty  →  true

s = "([)]"

(     push            stack: (
[     push            stack: ( [
)     peek [  ≠ (     →  false

The Java is that walk:

boolean isValid(String s) {
    int n = s.length();
    if (n % 2 != 0) {
        return false;
    }
    Deque<Character> open = new ArrayDeque<>();
    for (int i = 0; i < n; i++) {
        char c = s.charAt(i);
        if (c == '(' || c == '{' || c == '[') {
            open.push(c);
            continue;
        }
        if (open.isEmpty()) {
            return false;
        }
        char top = open.peek();
        if ((c == ')' && top != '(')
                || (c == '}' && top != '{')
                || (c == ']' && top != '[')) {
            return false;
        }
        open.pop();
    }
    return open.isEmpty();
}

Time is O(n) — one pass, one push or pop per character. Space is O(n) for unmatched opens (worst case the string is all opens). Empty string: length 0, even, the loop never runs, the stack is empty, you return true without a special case.

Note: A close on an empty stack is a pop you must not do. Return false; do not call pop. Leftover opens at the end ("((") fail the empty check. ()[] is two sequential pairs; ([)] is a crossed pair — the stack is how you tell them apart.

What interviewers usually poke next

  • Only (). Then a single counter is enough; the stack is overkill, and you should say why ([)] forced the stack in the three-type prompt.
  • Count-only for (). Nested (()()) works with +1/−1. You never needed types, and you never needed a stack.
  • Unicode / extra bracket types. Map each close to its open instead of three ifs. The walk does not change.
  • Streaming. You cannot reread s. The stack still works in one pass; you only hold unmatched opens.
  • Generate every valid string. That is Generate Parentheses, not a checker.

You are done with this problem when you can reject ([)] without a count, refuse to pop an empty stack, and require empty at the end.