· 4 min read

"Valid Parentheses" is a parsing problem

In light of some events that happened recently, I realised that valid parentheses (and some other problems) are actually parsing problems.

The valid parentheses problem gives you a string of (), [], or {}. You have to return whether every opening brace is closed by a brace of the same type, in the right order. There are three bracket types, but they all work the same way, so for the rest of this post I’ll assume there’s only ().

NOTE

If there were only a single type of bracket, a counter would be sufficient.

The “standard” solution

If you do even a little bit of leetcode, you should know how to solve it. The first option would be to use a stack. Push every opening parenthesis onto the stack, and when you see a closing parenthesis, you pop from the stack (this should be an opening parenthesis). Ensure that the popped opening parenthesis matches the closing parenthesis. At the end of the loop, the stack must be empty.

The stack solution is effectively a shift-reduce parser. Pushing an opening brace is a shift, and popping it when its closing brace comes is a reduce. It parses with the following grammar.

e::=e  (  e  )∣ϵe ::= e \; \texttt{(} \; e \; \texttt{)} \mid \epsilon

where ϵ\epsilon is the empty string.

What’s that mean?!?!

It is a formal notation to describe syntax of languages known as Backus-Naur Form

The other direction

Recursive descent is the dual of shift-reduce parsing. It builds the parse top-down instead of bottom-up. Recursive descent can’t handle left recursion, so we flip the grammar to put the recursion on the right. Once you have the grammar, the code writes itself.

e::=(  e  )  e∣ϵe ::= \texttt{(} \; e \; \texttt{)} \; e \mid \epsilon
const char *parse_expr(const char *s) {
    if (!is_opening(*s)) {
        return s;
    }

    const char *rest = parse_expr(s + 1);
    if (!is_matching_paren(*s, *rest)) return s;
    return parse_expr(rest + 1);
}

bool is_valid(const char *s) { return *parse_expr(s) == '\0'; }

is_opening returns true for an opening bracket, and is_matching_paren returns true if the two characters form a pair.

A note on functional parsing

The standard function header for a parser is String -> Maybe (Tree, String). In this case, we don’t really care about the Tree, so we drop it. We also don’t need the Maybe, because parsing ee never fails. If (  e  )  e\texttt{(} \; e \; \texttt{)} \; e fails, we fall back to ϵ\epsilon and return the entire input. So the parser is just String -> String, and the input is valid when nothing is left over.

An alternative

The trailing ee is a tail call, so it becomes a loop. The inner ee can’t, because the ) is still pending after it returns. Every tail call is a loop, but not every recursion is a tail call.

const char *parse_expr(const char *s) {
    while (is_opening(*s)) {
        const char *rest = parse_expr(s + 1);
        if (!is_matching_paren(*s, *rest)) break;
        s = rest + 1;
    }
    return s;
}

In grammar form, that’s

e::={  (  e  )  }e ::= \{ \; \texttt{(} \; e \; \texttt{)} \; \}

Ackshually

{ x } means zero or more x. Plain BNF has no shorthand for this, you’d have to write the recursion out yourself. The braces are an Extended BNF (EBNF) thing.

Which solution is the best?

On its own, I don’t think any have real advantages over each other. The important part is how you can view the problem as a parsing problem, and use all the tools that parsing theory has given us.

That said, if I had to pick, I’d choose the more general approach. The approach using the stack is LR parsing, and it’s strictly more expressive than LL parsing (the recursive approaches).

LR? LL?

If you have no idea what LL and LR are, you might want to get started here.

    Back to Blog

    Related Posts

    View All Posts »