𝔩𝔢𝔩𝕠𝔭𝔢𝔷
Connect With Me on LinkedIn

Python Study

Evaluate Expression Tree

Efficient Evaluation of Binary Expression Trees in Python

Introduction

Binary expression trees are powerful data structures for representing and evaluating mathematical expressions. This article explores implementing a method to evaluate such trees in Python, demonstrating recursive and iterative traversal techniques with operation-based node value processing.

Problem Statement

Given:

  • A binary expression tree where leaf nodes represent positive integers and internal nodes represent operators

Objective:

  • Implement a method to evaluate the tree and return the result as a single integer

Example:

  • Input: A tree with root (-1), left child (5), right child (-4), right child's left child (2), right child's right child (3)
  • Output: 11 (5 + (2 * 3) = 11)

Assumption:

  • The tree will always be a valid expression tree
  • Operators are represented by negative integers: -1 (addition), -2 (subtraction), -3 (division), -4 (multiplication)

Strategy and Hypothesis

To evaluate the expression tree effectively:

  1. Use recursion to traverse the tree
  2. Evaluate leaf nodes (positive integers) as is
  3. For operator nodes, recursively evaluate left and right subtrees, then apply the operator
  4. Handle division by rounding towards zero

This approach ensures correct expression evaluation while maintaining the order of operations.

Implementation

Initial Attempt

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

def evaluateExpressionTree(tree):
    if tree.value >= 0:
        return tree.value

    left = evaluateExpressionTree(tree.left)
    right = evaluateExpressionTree(tree.right)

    if tree.value == -1:
        return left + right
    elif tree.value == -2:
        return left - right
    elif tree.value == -3:
        return int(left / right)  # Integer division
    elif tree.value == -4:
        return left * right

This approach correctly evaluates the tree but uses multiple if statements for different operators.

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

op_map = {
    -1: int.__add__,
    -2: int.__sub__,
    -3: lambda a, b: int(a / b),
    -4: int.__mul__,
}

def evaluateExpressionTree(tree):
    if tree.value >= 0:
        return tree.value

    left = evaluateExpressionTree(tree.left)
    right = evaluateExpressionTree(tree.right)
    return op_map[tree.value](left, right)

This version uses an operation map to simplify the logic and improve readability.

Optimization Analysis

Optimizing for Space

The recursive implementation uses O(h) space on the call stack, where h is the tree's height. An iterative approach using an explicit stack optimizes space usage, particularly for deep trees:

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

def evaluateExpressionTree(tree):
    op_map = {
        -1: int.__add__,
        -2: int.__sub__,
        -3: lambda a, b: int(a / b),
        -4: int.__mul__,
    }

    stack = []
    current = tree

    while stack or current:
        if current:
            stack.append(current)
            current = current.left
        else:
            node = stack.pop()

            if node.value >= 0 or (isinstance(node.left, int) and isinstance(node.right, int)):
                if node.value >= 0:
                    value = node.value
                else:
                    value = op_map[node.value](node.left, node.right)

                if stack:
                    parent = stack[-1]
                    if parent.left == node:
                        parent.left = value
                    else:
                        parent.right = value
                else:
                    return value
            else:
                node.left = node.left.value if isinstance(node.left, BinaryTree) else node.left
                stack.append(node)
                current = node.right

    return 0  # In case of an empty tree

This iterative approach uses O(h) space, where h is the tree's height. It's more space-efficient than recursion for deep trees and avoids potential stack overflow issues.

Optimizing for Time

The recursive and iterative implementations achieve optimal time complexity of O(n), where n is the number of nodes in the tree. Each node is visited exactly once, and operations at each node take constant time. No further significant time optimization is possible without changing the problem's nature.

Optimal Space & Time

The iterative approach with operation mapping provides an optimal balance of space and time efficiency for most cases:

  • It maintains O(n) time complexity, visiting each node once
  • It uses O(h) space for the stack, which is more efficient than recursion for deep trees
  • The O(1) extra space for the operation map is negligible

This approach is particularly beneficial for large or potentially unbalanced trees where recursion depth could be a concern.

Key Takeaways

  1. Both recursive and iterative approaches can be effective for tree traversal problems
  2. Using a mapping for operations can simplify code and improve maintainability
  3. Iterative solutions can be more space-efficient, especially for deep trees
  4. Consider the trade-off between code simplicity and performance optimization
  5. Handling special cases (like division rounding) is crucial for correct results
  6. The choice of implementation (recursive vs. iterative) can significantly impact performance for certain tree structures

Conclusion

Binary expression tree evaluation showcases the strengths of both recursive and iterative algorithms. We've explored recursive methods—basic and optimized with operation mapping—and an iterative approach balancing space and time efficiency. The iterative approach with operation mapping offers a good balance of efficiency and flexibility, especially for deep or unbalanced trees. Remember, the best solution depends on the specific requirements and data structure.

Previous
Depth First Search