Taro Logo

Design an Expression Tree With Evaluate Function

Medium
Asked by:
Profile picture
13 views
Topics:
TreesRecursion

An expression tree is a binary tree in which each internal node corresponds to some operator and each leaf node corresponds to some operand. We are given a string array expression of size n representing a valid expression tree.

Each element of the expression array will be in one of the following three formats:

  • number, which represents the value of this operand, e.g., "3", "4", "10".
  • "+", which represents addition operator '+'.
  • "-", which represents subtraction operator '-'.
  • "*", which represents multiplication operator '*'.
  • "/", which represents division operator '/'.

Please design a generic interface for the expression tree node, and write a function to evaluate the expression tree.

Example 1:

Input: expression = ["3","4","+","2","*","7","/"]
Output: 6
Explanation: This represents the expression ((3+4)*(2/7)).

Example 2:

Input: expression = ["4","5","-"]
Output: -1
Explanation: This represents the expression 4-5.

Constraints:

  • 1 <= expression.length < 200
  • expression[i] is either an integer or an operator ("+", "-", "*", or "/").
  • The value of each operand will be between [-100, 100].
  • expression is guaranteed to be a valid expression tree.
  • The result of division is not always an integer.
  • The result will be in the range [-105, 105].

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What types of operators will the expression tree support (e.g., +, -, *, /, exponents, unary operators)? What is the order of operations I should follow?
  2. What data type will the operands be (e.g., integers, floating-point numbers)? Are there any limitations on their range?
  3. How will the expression be provided initially? Will it be a string, a list of tokens, or some other format? Can I assume the input expression is always valid and well-formed?
  4. Are there any specific error conditions I need to handle, such as division by zero or invalid operator combinations, and how should I handle them?
  5. What is the expected return type for the `evaluate` function? Should it return an integer, a float, or another data type? Are there any precision requirements for floating-point results?

Brute Force Solution

Approach

We're building a math problem represented as a tree, where each branch is either a number or an operation like addition. The brute force approach tries out all possible arrangements of the expression to find the final result.

Here's how the algorithm would work step-by-step:

  1. Imagine you have a bunch of numbers and math symbols (like plus, minus, multiply, divide).
  2. Start building the tree by trying different combinations of these, for example, start with a single number as the root of the tree.
  3. Then, try combining two numbers with one of the math symbols to build a bigger tree.
  4. Keep trying all possible combinations to build different tree structures.
  5. For each possible tree, follow the order of operations (like doing multiplication before addition) to calculate the final answer.
  6. Keep track of all the final answers you get from these different trees.
  7. The correct answer is the one you find by correctly building and evaluating the tree based on the original mathematical expression.

Code Implementation

class ExpressionTree:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

    def evaluate(self):
        if isinstance(self.value, int):
            return self.value
        
        left_result = self.left.evaluate()
        right_result = self.right.evaluate()

        if self.value == '+':
            return left_result + right_result
        elif self.value == '-':
            return left_result - right_result
        elif self.value == '*':
            return left_result * right_result
        elif self.value == '/':
            if right_result == 0:
                return float('inf')
            return left_result / right_result

        return 0

def generate_trees(tokens):
    if not tokens:
        return []

    trees = []
    for i in range(len(tokens)):
        token = tokens[i]
        # If the token is an operator, we can build a tree with it as the root
        if token in ['+', '-', '*', '/']:
            left_subtrees = generate_trees(tokens[:i])
            right_subtrees = generate_trees(tokens[i+1:])

            # Combine all possible left and right subtrees to create new trees
            for left_subtree in left_subtrees:
                for right_subtree in right_subtrees:
                    tree = ExpressionTree(token, left_subtree, right_subtree)
                    trees.append(tree)

        # If the token is a number, create a leaf node tree
        else:
            try:
                number = int(token)
                trees.append(ExpressionTree(number))
            except ValueError:
                pass

    return trees

def evaluate_expression_brute_force(expression):
    tokens = expression.split()
    all_possible_trees = generate_trees(tokens)
    results = []

    #Evaluate all the possible trees
    for tree in all_possible_trees:
        results.append(tree.evaluate())

    # Return all results, which is necessary given the prompt
    return results

#Example usage
#expression = "3 + 2 * 5"
#results = evaluate_expression_brute_force(expression)
#print(results)

Big(O) Analysis

Time Complexity
O(4^n / n^(3/2))The algorithm explores all possible binary expression trees that can be formed from n numbers and operations. The number of such trees is related to the Catalan number, which grows roughly as 4^n / n^(3/2). For each of these trees, the evaluation takes O(n) time in the worst case, as we potentially traverse all nodes. However, the dominant factor is still the number of possible tree structures. Thus, the overall time complexity is governed by the Catalan number, making the time complexity approximately O(4^n / n^(3/2)).
Space Complexity
O(N!)The algorithm attempts to build all possible expression trees by trying different combinations of numbers and operators. This process involves generating numerous tree structures. For each tree, the algorithm recursively evaluates it. The number of possible tree structures grows factorially with the number of input elements (N numbers and operators). Therefore, the space required to store these intermediate tree structures during the construction and evaluation phase can grow up to O(N!), where N is the number of input elements.

Optimal Solution

Approach

The problem involves creating a tree-like structure to represent mathematical expressions and then evaluating them. The best way to tackle this is to build the tree in a specific order and then use a process called recursion to easily calculate the result.

Here's how the algorithm would work step-by-step:

  1. First, create building blocks called nodes. Each node will represent either a number or an operation like addition or subtraction.
  2. Organize these nodes into a tree. Numbers will be at the bottom (leaves), and operations will be higher up, connecting the numbers they operate on.
  3. To calculate the expression, start at the very top operation. Then, ask each operation to calculate its result.
  4. Each operation will, in turn, ask its connected numbers (or other operations below it) for their values.
  5. This process continues until you reach the bottom of the tree, where the numbers are. These numbers just return their own value.
  6. As the values work their way back up, each operation performs its calculation and sends the result up to the operation above it.
  7. Eventually, the top operation gets all the values it needs, calculates the final answer, and returns it. This is the result of the whole expression.

Code Implementation

class ExpressionTreeNode:
    def __init__(self, value):
        self.value = value
        self.left_child = None
        self.right_child = None

    def evaluate(self):
        raise NotImplementedError

class NumberNode(ExpressionTreeNode):
    def __init__(self, value):
        super().__init__(value)

    def evaluate(self):
        return self.value

class OperationNode(ExpressionTreeNode):
    def __init__(self, value):
        super().__init__(value)

class AdditionNode(OperationNode):
    def __init__(self):
        super().__init__("+")

    def evaluate(self):
        # Recursively evaluate children and return the sum.
        return self.left_child.evaluate() + self.right_child.evaluate()

class SubtractionNode(OperationNode):
    def __init__(self):
        super().__init__("-")

    def evaluate(self):
        # Recursively evaluate children and return the difference.
        return self.left_child.evaluate() - self.right_child.evaluate()

class MultiplicationNode(OperationNode):
    def __init__(self):
        super().__init__("*")

    def evaluate(self):
        return self.left_child.evaluate() * self.right_child.evaluate()

class DivisionNode(OperationNode):
    def __init__(self):
        super().__init__("/")

    def evaluate(self):
        return self.left_child.evaluate() / self.right_child.evaluate()

# Example Usage:
if __name__ == '__main__':
    # Create nodes.
    number_five = NumberNode(5)
    number_ten = NumberNode(10)

    addition_operation = AdditionNode()
    addition_operation.left_child = number_five
    addition_operation.right_child = number_ten

    multiplication_operation = MultiplicationNode()
    multiplication_operation.left_child = addition_operation
    number_two = NumberNode(2)
    multiplication_operation.right_child = number_two

    # Evaluate the expression tree.
    result = multiplication_operation.evaluate()
    print(f"The result of the expression is: {result}")

    subtraction_operation = SubtractionNode()
    subtraction_operation.left_child = number_ten
    subtraction_operation.right_child = number_five

    division_operation = DivisionNode()
    division_operation.left_child = number_ten
    division_operation.right_child = number_two

    # Recursively calculate result
    division_result = division_operation.evaluate()
    print(f"The result of the division is: {division_result}")

    subtraction_result = subtraction_operation.evaluate()
    print(f"The result of the subtraction is: {subtraction_result}")

Big(O) Analysis

Time Complexity
O(n)The time complexity for constructing the expression tree is O(n), where n is the number of nodes/elements in the input expression. This is because we process each node once to build the tree. The evaluation of the expression tree using recursion also takes O(n) time, as each node is visited and processed exactly once during the evaluation traversal. Therefore, the overall time complexity is dominated by the linear traversal of the tree, resulting in O(n).
Space Complexity
O(N)The space complexity is primarily determined by the depth of the expression tree and the recursive calls during the evaluation phase. In the worst-case scenario (e.g., a skewed tree where each node has only one child), the recursion depth can be equal to the number of nodes in the tree, N. Each recursive call adds a frame to the call stack, consuming memory. Therefore, the maximum space occupied by the call stack is proportional to N, resulting in a space complexity of O(N).

Edge Cases

Null or empty expression string
How to Handle:
Return null or throw an IllegalArgumentException, as an empty expression cannot be parsed or evaluated.
Expression string containing only whitespace
How to Handle:
Return null or throw an IllegalArgumentException, as whitespace-only input is not a valid expression.
Expression string with invalid characters (e.g., letters, symbols other than operators)
How to Handle:
Throw an exception indicating an invalid character was found during parsing.
Division by zero
How to Handle:
Throw an ArithmeticException to prevent program crash and signal an invalid mathematical operation.
Integer overflow during evaluation
How to Handle:
Use a larger data type (e.g., long) or BigInteger for intermediate calculations to prevent overflow.
Deeply nested expressions causing stack overflow during recursive evaluation
How to Handle:
Convert recursive evaluation to iterative evaluation using a stack to avoid stack overflow.
Unbalanced parentheses in the expression string
How to Handle:
Throw an exception indicating that the parentheses are not properly balanced during parsing.
Missing operands or operators in the expression string
How to Handle:
Throw an exception during parsing, indicating a syntax error due to missing components.