4 minute read

Problem Statement

problem-224

Note:

  • My approach is accepted by Leetcode Judge, but it’s quite slow -> need to transform the recursion into stack or something to improve the time complexity.
  • Need to review again for interview practice.

Brute Force - recursion - Accepted

Intuition

My initial thoughts are to tokenize the input string, considering the arithmetic operations and parentheses. After tokenization, I can recursively evaluate the expressions following the order of operations.

Approach

I will define a method tokenize to break the input string into meaningful tokens, such as numbers, addition, subtraction, and parentheses. Then, I’ll create a recursive method calc to perform the actual calculation. This method will handle different cases, such as encountering digits, operators, or parentheses.

The calc method will have parameters like the current index in the tokens list, the list of tokens, the current sign, and the running total. It will recursively process the tokens and update the total accordingly.

Complexity

  • Time complexity: O(n) where n is the length of the input string. The tokenization process and recursive evaluation of the expression contribute to this complexity.

  • Space complexity: O(n) where n is the length of the input string. The space required for the tokens list and the recursive call stack contributes to the space complexity.

Code

class Solution:
    def calculate(self, s: str) -> int:
        s = s.replace(" ", "")
        N = len(s)
        def tokenize(expr):
            tokens = []
            i = 0
            curr = ""
            while i < N:
                c = expr[i]
                if c in '+-()':
                    if curr:
                        tokens.append(curr)
                    tokens.append(c)
                    curr = ""
                else:
                    curr += c
                i += 1

            if curr:
                tokens.append(curr)

            return tokens


        def calc(i, tokens, sign, curr):
            if i == len(tokens) or tokens[i] == ')':
                return curr

            if tokens[i] not in "+-()": # digits
                return calc(i + 1, tokens, sign, sign * int(tokens[i]) + curr)

            if tokens[i] in "+-":
                sign = 1 if tokens[i] == "+" else -1
                return calc(i + 1, tokens, sign, curr)

            if tokens[i] == '(':
                hash_map = {'(': 1, ')': 0}
                j = i
                while j < len(tokens) and hash_map['('] > 0:
                    j += 1
                    if tokens[j] in '()':
                        hash_map['('] += (1 if tokens[j] == '(' else -1)
                return curr + (calc(i + 1, tokens, 1, 0) * sign) + calc(j + 1, tokens, 1, 0)


        tokens = tokenize(s)
        return calc(0, tokens, 1, 0)

Editorial Solution

Approach 1: Stack and String Reversal

class Solution:

    def evaluate_expr(self, stack):

        # If stack is empty or the expression starts with
        # a symbol, then append 0 to the stack.
        # i.e. [1, '-', 2, '-'] becomes [1, '-', 2, '-', 0]
        if not stack or type(stack[-1]) == str:
            stack.append(0)

        res = stack.pop()

        # Evaluate the expression till we get corresponding ')'
        while stack and stack[-1] != ')':
            sign = stack.pop()
            if sign == '+':
                res += stack.pop()
            else:
                res -= stack.pop()
        return res

    def calculate(self, s: str) -> int:

        stack = []
        n, operand = 0, 0

        for i in range(len(s) - 1, -1, -1):
            ch = s[i]

            if ch.isdigit():

                # Forming the operand - in reverse order.
                operand = (10**n * int(ch)) + operand
                n += 1

            elif ch != " ":
                if n:
                    # Save the operand on the stack
                    # As we encounter some non-digit.
                    stack.append(operand)
                    n, operand = 0, 0

                if ch == '(':
                    res = self.evaluate_expr(stack)
                    stack.pop()

                    # Append the evaluated result to the stack.
                    # This result could be of a sub-expression within the parenthesis.
                    stack.append(res)

                # For other non-digits just push onto the stack.
                else:
                    stack.append(ch)

        # Push the last operand to stack, if any.
        if n:
            stack.append(operand)

        # Evaluate any left overs in the stack.
        return self.evaluate_expr(stack)

Approach 2: Stack and No String Reversal

class Solution:
    def calculate(self, s: str) -> int:

        stack = []
        operand = 0
        res = 0 # For the on-going result
        sign = 1 # 1 means positive, -1 means negative

        for ch in s:
            if ch.isdigit():

                # Forming operand, since it could be more than one digit
                operand = (operand * 10) + int(ch)

            elif ch == '+':

                # Evaluate the expression to the left,
                # with result, sign, operand
                res += sign * operand

                # Save the recently encountered '+' sign
                sign = 1

                # Reset operand
                operand = 0

            elif ch == '-':

                res += sign * operand
                sign = -1
                operand = 0

            elif ch == '(':

                # Push the result and sign on to the stack, for later
                # We push the result first, then sign
                stack.append(res)
                stack.append(sign)

                # Reset operand and result, as if new evaluation begins for the new sub-expression
                sign = 1
                res = 0

            elif ch == ')':

                # Evaluate the expression to the left
                # with result, sign and operand
                res += sign * operand

                # ')' marks end of expression within a set of parenthesis
                # Its result is multiplied with sign on top of stack
                # as stack.pop() is the sign before the parenthesis
                res *= stack.pop() # stack pop 1, sign

                # Then add to the next operand on the top.
                # as stack.pop() is the result calculated before this parenthesis
                # (operand on stack) + (sign on stack * (result from parenthesis))
                res += stack.pop() # stack pop 2, operand

                # Reset the operand
                operand = 0

        return res + sign * operand