Practice Lab

Valid Parentheses

Valid Parentheses is a stack problem. Every closing bracket must match the most recent unmatched opening bracket.

The useful mental model: brackets close in reverse order. That is exactly what a stack gives us.

Problem

Given a string s containing bracket characters, return true if the string is valid.

A string is valid when:

Examples:

"()" -> true
"()[]{}" -> true
"(]" -> false
"([)]" -> false
"{[]}" -> true

First Attempt

public boolean isValid(String s) {
    if (s == null && s.length() % 2 != 0) return false;

    Map m = Map.of(
            '}', '{',
            ')', '(',
            ']', '['
    );

    Deque<Character> st = new ArrayDeque<>();

    for (char c : s.toCharArray()) {
        if (m.containsKey(c)) {
            if (st.isEmpty() || st.pop() != m.get(c)) return false;
        } else {
            st.push(c);
        }
    }

    return st.isEmpty();
}

The stack idea is right. The map from closing bracket to opening bracket is also a clean move.

The important bug is in the guard:

if (s == null && s.length() % 2 != 0) return false;

This should be ||, not &&.

If s is null, then s.length() throws NullPointerException. We want to stop before calling length().

What Changed

Tune the solution in three places:

  1. Use || in the null/odd-length guard.
  2. Use generics: Map<Character, Character>.
  3. Push only opening brackets, and reject unexpected characters if the input is not guaranteed to contain only brackets.

For the standard LeetCode version, the input contains only bracket characters, so the final else can push opening brackets directly.

Final Version

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;

class Solution {
    public boolean isValid(String s) {
        if (s == null || s.length() % 2 != 0) {
            return false;
        }

        Map<Character, Character> pairs = Map.of(
            ')', '(',
            ']', '[',
            '}', '{'
        );

        Deque<Character> stack = new ArrayDeque<>();

        for (char c : s.toCharArray()) {
            if (pairs.containsKey(c)) {
                if (stack.isEmpty() || stack.pop() != pairs.get(c)) {
                    return false;
                }
            } else {
                stack.push(c);
            }
        }

        return stack.isEmpty();
    }
}

Walkthrough

Input:

s = "{[]}"

Steps:

charactionstack
{push opening bracket{
[push opening bracket[ {
]pop and match [{
}pop and match {empty

At the end, the stack is empty, so the string is valid.

Why The Stack Works

Consider:

([)]

When we reach ), the most recent opening bracket is [, not (.

That means the string is invalid immediately. The stack lets us check the latest unmatched opening bracket in O(1).

Stricter Version

If the input may contain non-bracket characters, reject them:

if (pairs.containsKey(c)) {
    if (stack.isEmpty() || stack.pop() != pairs.get(c)) {
        return false;
    }
} else if (c == '(' || c == '[' || c == '{') {
    stack.push(c);
} else {
    return false;
}

Complexity

ApproachTimeSpace
Stack scanO(n)O(n)

The worst-case space happens when every character is an opening bracket.

Takeaways