DSAStacksParsing Expressions

Parsing Expressions

Stacks

The commitments we take on keeps growing for ever. A stack is how your mind remembers to circle back.

How does a computer understand 3 + (2 * 5)? It can't just read left-to-right; it needs to respect the order of operations, with parentheses taking highest priority. A stack is the perfect data structure for this task!

Before reading on, try to solve this problem. How would you check if a string of parentheses is "balanced"?

Problem: Valid Parentheses (LeetCode Link): Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid. 💡 Hint

The key is to use a stack. When you see an opening bracket, you push it onto the stack. When you see a closing bracket, you check if the top of the stack is its matching opening bracket. If it is, you pop the stack. If it's not, or if the stack is empty, the string is invalid.

This "last-in, first-out" (LIFO) behavior is the core pattern for parsing. The stack naturally handles the nested structure of expressions—the last thing you opened is the first thing you must close.

Level 1: Just the Structure (Validating Parentheses)

This is the simplest form of parsing. We only care about the structural symbols ((, [, {) and ignore everything else.

  1. Define matching pairs: Use a map like { ')':'(', '}':'{', ']':'[' }, or even a switch statement.
  2. Single Pass:
    • If you see an opening bracket, push it to the stack.
    • If you see a closing bracket, check if the stack is empty or if its top doesn't match. If so, it's invalid. Otherwise, pop.
  3. Final Check: After the loop, the stack must be empty for the string to be valid.
def is_valid_parentheses(s: str) -> bool:
    stack = []
    mapping = {")": "(", "}": "{", "]": "["}

    for char in s:
        if char in mapping.values():  # Opening bracket
            stack.append(char)
        elif char in mapping:  # Closing bracket
            if not stack or mapping[char] != stack.pop():
                return False
    return not stack

Level 2: Simple Arithmetic (No Parentheses)

Now, let's evaluate an expression like 3 - 2 * 5. We need to respect that multiplication and division have higher precedence than addition and subtraction.

A clever way to handle this is with a stack and the "deferred addition, immediate multiplication" trick. We process the expression, but only add numbers to the stack. If we see a * or /, we immediately apply it to the last number we added to the stack.

  1. State Variables: Keep track of the current_number being built and the last_operator seen (default to +).
  2. Process: Iterate through the string. When you hit an operator (or the end of the string):
    • Apply the last_operator to the current_number.
    • If last_operator was + or -, just push the current_number (or -current_number) to the stack.
    • If last_operator was * or /, pop the last value from the stack, multiply/divide it by current_number, and push the result back.
  3. Final Result: The sum of all numbers in the stack is the answer.

Problem: Basic Calculator II (LeetCode Link): Given a string s which represents a basic expression, implement a calculator to evaluate it.

def calculate_simple(s: str) -> int:
    stack = []
    num = 0
    last_op = '+'
    s += '+' # This helps us to avoid a final check for the last number

    for char in s:
        if char.isdigit():
            num = num * 10 + int(char)
        elif char in "+-*/":
            if last_op == '+':
                stack.append(num)
            elif last_op == '-':
                stack.append(-num)
            elif last_op == '*':
                stack.append(stack.pop() * num)
            elif last_op == '/':
                stack.append(int(stack.pop() / num))
            last_op = char
            num = 0
            
    return sum(stack)

Level 3: Putting it Together (Handling Parentheses)

Now, let's combine the two ideas to solve expressions like 3 * (2 + 5). The trick is to treat everything inside parentheses as a sub-problem.

  1. When you see (, push the current state onto a stack. You can think of it as "pausing" the current evaluation until you find the matching ).
  2. Since we saved the current state, reset the working state:
    • Reset num = 0; and lastOp = '+';.
  3. When you see ), pop the stack to continue evaluation from where you left off.
    • When you see ), do the validations we discussed earlier for parentheses.

This solution beautifully combines both #1 and #2 we discussed above. Anything inside parentheses is evaluated as a sub-expression, and the stack allows you to handle nested structures naturally.

Problem: Basic Calculator (LeetCode Link): This problem focuses on +, -, and parentheses.

You can also use recursion to solve this problem, but the stack-based approach is more efficient and straightforward for parsing expressions.

def evaluate_expression(s):
    expression_stack = []
    context_stack = []  # To store state of calculation when we hit '('

    num = 0
    last_op = '+'

    for i, c in enumerate(s):
        if c.isdigit():
            num = num * 10 + int(c)  # Build the current number

        elif c == '(':
            # Save the current context, start fresh
            context_stack.append((expression_stack, last_op))
            expression_stack = []  # Start a new fresh context
            num = 0
            last_op = '+'

        elif c == ')':
            # Finish off the inner expression
            apply_operation(expression_stack, last_op, num)
            inner_value = sum(expression_stack)

            # Restore the previous context
            expression_stack, last_op = context_stack.pop()
            num = inner_value

        elif c != ' ':
            # If we hit an operator or end of string, process the last number
            apply_operation(expression_stack, last_op, num)

            # Update the last operator and reset num
            last_op = c
            num = 0
    
    # Final processing of the last number
    apply_operation(expression_stack, last_op, num)
    return sum(expression_stack)

def apply_operation(stack, last_op, num):
    if last_op == '+':
        stack.append(num)
    elif last_op == '-':
        stack.append(-num)
    elif last_op == '*':
        stack[-1] *= num
    elif last_op == '/':
        # Integer division
        stack[-1] = int(stack[-1] / num)

Bonus Point: The Shunting-Yard Algorithm

For a truly generic and powerful solution, look up Dijkstra's Shunting-Yard Algorithm. It's a method for parsing expressions specified in infix notation. It can handle operator precedence and associativity (e.g., (2^3)^4 vs 2^(3^4)).

The algorithm works by converting the infix expression (like 3 + 4 * 2) into a postfix expression (like 3 4 2 * +), also known as Reverse Polish Notation (RPN). Postfix expressions are trivial to evaluate with a single stack. While it's more complex than the methods above, understanding it provides a deep insight into how compilers and calculators work. It's rarely expected in an interview unless you're aiming for a compiler or language design role, but knowing it exists is a huge plus.