Back

Expression Tree in Data Structure: Properties, Construction with Examples

05 Dec 2024
6 min read

In Data structures, trees are often used to represent hierarchical ways. One such tree is the expression tree, a specialized tree structure used to represent expressions in mathematical or logical form. This is particularly useful for evaluating mathematical expressions, parsing, and compiler design. Understanding how expression trees function and their construction is essential for efficient expression evaluation.

Definition of an Expression Tree

The expression tree is a binary tree with each internal node representing an operand, and each leaf node representing an operator.

Properties of an expression tree

  • Leaf nodes represent operands (numbers or variables).
  • Internal nodes represent operators (such as +, -, *, /). Each operator in the tree corresponds to a sub-expression of the original expression.
  • The structure of the tree preserves operator precedence, ensuring that the order of operations is maintained.
  • An expression tree can be used to evaluate an arithmetic expression. By recursively evaluating the left and right subtrees and applying the operator at the internal node, the result can be computed.

For example, the expression (3 + 5) * (2 - 1) can be represented as an expression tree where:

  • The root is the multiplication operator ‘*’.
  • The left child is the addition operator ‘+’, with operands ‘3’ and ‘5’ as its children.
  • The right child is the subtraction operator’ -’, with operands ‘2’ and ‘1’ as its children.
          *
         / \
        +   -
       / \ / \
      3  5 2  1

Types of Expressions Represented by Expression Trees

Expression trees are a fundamental data structure for representing arithmetic and logical expressions in various notations (infix, postfix, and prefix). These trees can represent different types of expressions, depending on the operators, operands, and their organization in the tree structure. Below are the types of expressions that can be represented by expression trees:

1. Arithmetic Expressions

Arithmetic expressions involve mathematical operations like addition, subtraction, multiplication, and division, typically with numerical values (constants or variables).

Expression Tree for 3 + 4 * 2

  • Infix: 3 + 4 * 2
  • Postfix: 3 4 2 * +
  • Prefix: + 3 * 4 2
       +
      / \
     3   *
        / \
       4   2

In this example:

  • The root is the + operator.
  • The left child is the operand 3.
  • The right child is a multiplication (*) operation, which itself has operands 4 and 2.

2. Boolean Expressions

Boolean expressions are used in logical operations, such as AND, OR, NOT, and combinations thereof. These expressions often involve Boolean variables or constants (true or false).

Expression Tree for A AND B OR C

  • Infix: A AND B OR C
  • Postfix: A B AND C OR
  • Prefix: OR AND A B C
        OR
       /  \
    AND    C
   /  \
  A    B

In this case:

  • The root is the OR operator.
  • The left child is an AND operation, which has A and B as its operands.
  • The right child is the operand C.

3. Relational Expressions

Relational expressions compare two values and are commonly used in conditional statements. These expressions use relational operators such as =, >, <, >=, <=, and !=.

Expression Tree for A > B AND C < D:

  • Infix: A > B AND C < D
  • Postfix: A B > C D < AND
  • Prefix: AND > A B < C D
        AND
       /    \
      >      <
     / \    / \
    A   B   C  D

Here:

  • The root is the AND operator.
  • The left child is a comparison (>) with operands A and B.
  • The right child is a comparison (<) with operands C and D.

4. Conditional Expressions

Conditional expressions are expressions used in decision-making, such as if-else statements or ternary operators. A common example is the ternary conditional operator (? :)

Expression Tree for x > 10 ? y : z:

  • Infix: x > 10 ? y : z
  • Postfix: x 10 > y z ? :
  • Prefix: ? > x 10 y z
        ?
       /  \
      >    :
    / \   / \
   x  10  y  z

In this case:

  • The root is the ternary conditional operator (?).
  • The left child is the relational expression x > 10.
  • The right child is the conditional operation (:), with operands y and z.

Construction of an Expression Tree

An expression tree can be constructed using infix, prefix, or postfix notations. Let's break down how these are constructed and represented with example:

       +
      / \
     A   *
        / \
       B   C

Infix Notation

In infix notation, operators are placed between operands. This is the most common way of writing arithmetic expressions.

Construction Process (Infix to Expression Tree)

Step 1: Start with the lowest precedence operator (if multiple operators are present).

Step 2: Make this operator the root of the tree.

Step 3: The left subtree is created from the part of the expression to the left of the operator, and the right subtree is created from the part of the expression to the right of the operator.

Step 4: Recursively apply this process to the left and right subexpressions.

Example: A + B * C

  • The  + has a lower precedence than *, so + becomes the root.
  • Operand A becomes the left child of +.
  • The expression B*C becomes the right subtree. Since * has the highest precedence, it is the root of this subtree, and B and C are its children.

Postfix Notation

In postfix notation, operators are placed after their operands.

Construction Process (Postfix to Expression Tree)

Step 1: Start from the left of the postfix expression and push operands onto a stack.

Step 2: When an operator is encountered, pop the required number of operands (2 for binary operators) from the stack, create a new node for the operator, and make the popped operands its children.

Step 3: Push the new operator node back onto the stack.

Step 4: At the end of the expression, the stack will contain a single node, which is the root of the expression tree.

Example: A B C * +

  • Push A, B, and C onto the stack.
  • Pop B and C, create a node with *, and push it back onto the stack.
  • Pop A and the * node, create a node with + and push it back onto the stack.
  • The final stack contains the root of the tree.

Prefix Notation

In prefix notation, operators appear before their operands.

Construction Process (Prefix to Expression Tree)

Step 1: Start from the right of the prefix expression and push operands onto a stack.

Step 2: When an operator is encountered, pop the required number of operands (2 for binary operators) from the stack, create a new node for the operator, and make the popped operands its children.

Step 3: Push the new operator node back onto the stack.

Step 4: At the end of the expression, the stack will contain a single node, which is the root of the expression tree.

Example: + A * B C

  • Push ‘A’, ‘B’, and ‘C’ onto the stack.
  • Pop ‘B’ and C, create a node with *, and push it back onto the stack.
  • Pop ‘A’ and the * node, create a node with + and push it back onto the stack.
  • The final stack contains the root of the tree.

Comparison of Expression Trees with Example

Example Notation Example Expression Tree
custom img Infix A + B * C + (root), with left child A and right child * (subtree with B and C as leaves)
Postfix A B C * + Same as the infix tree, but the construction order follows postfix operations
Prefix + A * B C Same as the infix tree, but the construction order follows prefix operations

Advantages and Limitations of Expression Tree

Here are the advantages and disadvantages of the expression tree:

Advantages

  • Expression trees allow for efficient evaluation of expressions by systematically applying operators to operands in a structured manner.
  • They enable the simplification of complex expressions by transforming the tree structure.
  • They can also be used for syntax validation in compilers by checking if the expression follows correct grammar rules.
  • In many compilers, expression trees are optimized for better performance through various algorithms, such as constant folding or common subexpression elimination.

Disadvantages

  • Expression trees can take up a significant amount of memory, especially for complex expressions.
  • Constructing an expression tree requires parsing the expression, which can be time-consuming.
  • For simple expressions, using a tree may be overkill, and direct evaluation might be more efficient.
  • Handling different types of operators like unary and ternary can make expression trees more complex to construct.

Common Algorithms to Evaluate Expression Tree

Some of the algorithms are commonly used to manipulate and evaluate expression trees:

  • Post-order Traversal: This is a depth-first traversal method used to evaluate expression trees. It processes the left and right subtrees before evaluating the operator at the node.
  • Pre-order Traversal: Useful in some cases where we want to print or convert the expression tree to a prefix notation.
  • In-order Traversal: In this method, operands are printed in between operators, leading to an infix expression representation.

Implementation of Expression Tree

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

// Define a structure for the tree node
struct Node {
    char data;
    struct Node *left, *right;
};

// Stack structure to hold nodes during tree construction
struct Stack {
    int top;
    unsigned capacity;
    struct Node **array;
};
// Function to create a new tree node
struct Node* newNode(char data) {
    struct Node* node = (struct Node*)malloc(sizeof(struct Node));
    node->data = data;
    node->left = node->right = NULL;
    return node;
}

// Function to create a stack for nodes
struct Stack* createStack(unsigned capacity) {
    struct Stack* stack = (struct Stack*)malloc(sizeof(struct Stack));
    stack->capacity = capacity;
    stack->top = -1;
    stack->array = (struct Node**)malloc(stack->capacity * sizeof(struct Node*));
    return stack;
}

// Stack operations
int isFull(struct Stack* stack) { return stack->top == stack->capacity - 1; }
int isEmpty(struct Stack* stack) { return stack->top == -1; }
void push(struct Stack* stack, struct Node* node) {
    if (isFull(stack)) return;
    stack->array[++stack->top] = node;
}
struct Node* pop(struct Stack* stack) {
    if (isEmpty(stack)) return NULL;
    return stack->array[stack->top--];
}

// Function to check if a character is an operator
int isOperator(char c) {
    return (c == '+' || c == '-' || c == '*' || c == '/');
}

// Function to build the expression tree from a postfix expression
struct Node* constructExpressionTree(char* postfix) {
    struct Stack* stack = createStack(strlen(postfix));
    struct Node *node, *left, *right;
    
    for (int i = 0; postfix[i]; i++) {
        // If the character is an operand, create a node and push to stack
        if (!isOperator(postfix[i])) {
            node = newNode(postfix[i]);
            push(stack, node);
        } else {
            // If the character is an operator, pop two nodes and make them children
            right = pop(stack);
            left = pop(stack);
            node = newNode(postfix[i]);
            node->left = left;
            node->right = right;
            push(stack, node);
        }
    }
    
    return pop(stack); // The final node will be the root of the tree
}

// Function to perform inorder traversal (infix notation)
void inorder(struct Node* root) {
    if (root) {
        inorder(root->left);
        printf("%c ", root->data);
        inorder(root->right);
    }
}

// Function to perform preorder traversal (prefix notation)
void preorder(struct Node* root) {
    if (root) {
        printf("%c ", root->data);
        preorder(root->left);
        preorder(root->right);
    }
}

// Function to perform postorder traversal (postfix notation)
void postorder(struct Node* root) {
    if (root) {
        postorder(root->left);
        postorder(root->right);
        printf("%c ", root->data);
    }
}

// Main function
int main() {
    char postfix[] = "ab+c*d-";  // Example postfix expression: (a + b) * c - d
    struct Node* root = constructExpressionTree(postfix);
    
    printf("Infix Expression: ");
    inorder(root);
    printf("\n");
    
    printf("Prefix Expression: ");
    preorder(root);
    printf("\n");
    
    printf("Postfix Expression: ");
    postorder(root);
    printf("\n");

    return 0;
}

Output

Infix Expression: a + b * c - d 
Prefix Expression: - * + a b c d 
Postfix Expression: a b + c * d -

Conclusion

In conclusion, an expression tree is a binary tree used to represent mathematical expressions, where leaf nodes are operands and internal nodes are operators. It efficiently supports expression evaluation, respects operator precedence, and simplifies transformations. Expression trees are widely used in compilers, calculators, and computer algebra systems for parsing, evaluating, and optimizing expressions. By using traversals like post-order, expressions can be evaluated or simplified easily.

Frequently Asked Questions

1. Can an expression tree be used for symbolic computation?

Yes, expression trees are a key component in symbolic computation systems, allowing for symbolic manipulation and simplification of algebraic expressions.

2. What is the time complexity of evaluating an expression tree?

The time complexity of evaluating an expression tree is O(n), where n is the number of nodes in the tree since each node is visited once during a post-order traversal.

Read More Articles

Chat with us
Chat with us
Talk to career expert