← Back to DevBytes

Valid Parentheses: Multiple Solutions and Complexity Analysis

Valid Parentheses: Multiple Solutions and Complexity Analysis

The "Valid Parentheses" problem is one of the most classic algorithmic challenges encountered in coding interviews and competitive programming. At its core, the problem asks whether a given string containing only bracket characters—(, ), {, }, [, ]—is valid. A string is considered valid when every opening bracket has a corresponding closing bracket of the same type, and the brackets close in the correct order.

This tutorial walks through the problem definition, multiple solution strategies, their time and space complexity analysis, and best practices you should follow when implementing or extending this algorithm in real-world scenarios.

Problem Statement

Given a string s consisting only of the characters '(', ')', '{', '}', '[', and ']', determine if the input string is valid. A string is valid if:

For example, "()[]{}" is valid, "([)]" is not, and "{[]}" is valid.

Why It Matters

Beyond being a frequent interview question, the Valid Parentheses problem models a fundamental concept in computer science: the proper nesting of structures. Compilers use similar checks to validate syntax in source code. Text editors and IDEs rely on bracket matching to highlight errors. Configuration parsers, expression evaluators, and even HTML/XML parsers apply the same underlying principle.

Understanding how to solve this problem efficiently teaches you about stack-based algorithms, LIFO (Last In, First Out) data structures, and how to reason about both time and space complexity. These skills transfer directly to more advanced problems such as evaluating arithmetic expressions, parsing nested data formats, and validating tree-like structures.

Solution 1: Stack-Based Approach

The most intuitive and widely accepted solution uses a stack. The idea is simple: iterate through the string, push every opening bracket onto the stack, and when you encounter a closing bracket, check whether the top of the stack contains the matching opening bracket. If it does, pop it; otherwise, the string is invalid. At the end, the stack must be empty.

Implementation

function isValid(s) {
    const stack = [];
    const map = {
        ')': '(',
        '}': '{',
        ']': '['
    };

    for (let char of s) {
        if (char === '(' || char === '{' || char === '[') {
            stack.push(char);
        } else {
            if (stack.length === 0 || stack[stack.length - 1] !== map[char]) {
                return false;
            }
            stack.pop();
        }
    }

    return stack.length === 0;
}

Complexity Analysis

This approach is optimal in terms of asymptotic complexity because you must examine every character at least once, giving a lower bound of O(n) for time. The space usage is also unavoidable in the general case, since you need to remember the nesting structure.

Solution 2: Stack With Early Termination

A refinement of the basic stack approach adds early termination checks to fail fast. If the string length is odd, it can never be valid, so we return false immediately. Additionally, we can combine the opening bracket check and the map lookup into a single pass using a cleaner structure.

Implementation

function isValidOptimized(s) {
    if (s.length % 2 !== 0) return false;

    const stack = [];
    const pairs = new Map([
        ['(', ')'],
        ['{', '}'],
        ['[', ']']
    ]);

    for (let char of s) {
        if (pairs.has(char)) {
            stack.push(char);
        } else {
            const top = stack.pop();
            if (top === undefined || pairs.get(top) !== char) {
                return false;
            }
        }
    }

    return stack.length === 0;
}

Complexity Analysis

Using a Map instead of a plain object can improve readability and provides consistent key handling, especially if you later extend the solution to support additional bracket types.

Solution 3: Counter-Based Approach (Single Bracket Type)

If the problem is simplified to only one type of bracket—say, ( and )—you do not need a stack at all. A simple counter suffices: increment for each opening bracket and decrement for each closing bracket. If the counter ever goes negative, the string is invalid. At the end, the counter must be zero.

Implementation

function isValidSingleType(s) {
    let balance = 0;
    for (let char of s) {
        if (char === '(') {
            balance++;
        } else if (char === ')') {
            balance--;
            if (balance < 0) return false;
        }
    }
    return balance === 0;
}

Complexity Analysis

This approach is useful in scenarios like validating balanced parentheses in mathematical expressions where only one bracket type is present. It demonstrates how problem constraints affect the choice of data structure.

Solution 4: Recursive Approach

Although less practical due to recursion depth limits, a recursive solution can validate parentheses by processing the string and tracking an implicit stack through the call chain. This approach is mainly educational, illustrating the relationship between recursion and stacks.

Implementation

function isValidRecursive(s) {
    const pairs = { ')': '(', '}': '{', ']': '[' };
    let index = 0;

    function helper() {
        let stack = [];
        while (index < s.length) {
            let char = s[index];
            if ('({['.includes(char)) {
                stack.push(char);
                index++;
            } else {
                if (stack.length === 0 || stack.pop() !== pairs[char]) {
                    return false;
                }
                index++;
            }
        }
        return stack.length === 0;
    }

    return helper();
}

Complexity Analysis

In production code, prefer the iterative stack approach to avoid stack overflow errors on deeply nested inputs.

Solution 5: Without a Stack (Limited Bracket Types)

For the standard three bracket types, some developers attempt a string-replacement approach: repeatedly remove "()", "{}", and "[]" substrings until the string is empty or no more replacements can be made. While concise, this approach is significantly less efficient.

Implementation

function isValidReplace(s) {
    let prev;
    while (s.length > 0) {
        prev = s;
        s = s.replace('()', '').replace('{}', '').replace('[]', '');
        if (s === prev) break;
    }
    return s.length === 0;
}

Complexity Analysis

This solution is not recommended for large inputs but can be acceptable for short strings or quick prototyping. It highlights the trade-off between code simplicity and algorithmic efficiency.

Comparing the Solutions

The table below summarizes the key differences between the approaches discussed:

Best Practices

When implementing a Valid Parentheses solution in a real codebase, keep the following best practices in mind:

Extending the Problem

The Valid Parentheses problem has many variations that build on the same foundation. Common extensions include:

Mastering the basic stack solution gives you the foundation to tackle all of these variations with confidence.

Conclusion

The Valid Parentheses problem is a cornerstone of algorithmic problem solving that elegantly demonstrates the power of stack-based reasoning. While the stack approach remains the gold standard for its O(n) time complexity and clarity, exploring alternative solutions—such as counters, recursion, and string replacement—deepens your understanding of trade-offs between time, space, and code simplicity. By following best practices like early termination, map-based extensibility, and careful edge-case handling, you can write robust solutions that scale from interview whiteboards to production parsers. Whether you are validating user input, building a compiler, or preparing for your next technical interview, the principles behind this problem will serve you across a wide range of computational challenges.

— Ad —

Google AdSense will appear here after approval

← Back to all articles