Page Content

Tutorials

What Are The Types of Trees In Data Structure With Example

Types of Trees

Types of Trees
Types of Trees

Trees are essential non-linear data structures that allow for hierarchical data organisation in computers and facilitate effective algorithms for a range of tasks. They are frequently found in file systems, databases, and graphical user interfaces and offer benefits over linear data structures like array-based lists or linked lists.

General Tree: An abstract data type known as a “general tree” is one in which all of the elements aside from the root, which is at the top have a parent and zero or more children. Computer applications frequently use nodes to represent entities like files or departments.In an object-oriented programming language, nodes are objects that represent these concepts.

There are no parents for the root, the top node. The parents of every other node are identical. There are two offspring per node in a binary tree, which is different from a generic tree. An example of a linked structure is a generic tree. Additionally, a general tree can be transformed into a binary tree.

Forest: A forest consists of at least one disjointed tree. A graph without any cycles is called a forest. Since a tree is essentially a connected forest that is, a connected graph devoid of cycles this idea fits within the broad category of trees. Trees are seen as a broader class of graphs.

Binary Tree: A binary tree is a specific type of tree in which there can be up to two offspring per node. Because of where they are when they are drawn, these two kids are explicitly referred to as the left and right children. In a binary tree, a node can have no children at all (i.e., it’s a leaf), only left children, or right children. The heights and number of nodes of binary trees are related; level d can have no more than 2^d nodes. It is possible to implement binary trees using an array-based representation or a linked structure.

Strictly Binary Tree: A strictly binary tree is one in which each non-leaf node has precisely two offspring. A strictly binary tree, often called a “proper binary tree” or “full binary tree”. Has nodes with zero or two children. All internal nodes in such a tree will have two children, while leaf nodes will have none. Defines a “full binary tree” as a binary tree with two children for every node except the leaves.

Complete Binary Tree: This type of binary tree has all nodes at the final level as far to the left as feasible and all levels possibly with the exception of the last are fully filled. By numbering the nodes and presuming that all are present to complete the binary tree, an array can be used to represent an incomplete binary tree.

Full Binary Tree: The number of children per node defines a full binary tree. Every node in a full binary tree must have two or zero offspring. This implies that entire binary trees contain two children for every internal nodes. Some call these “proper binary trees”. Half of a balanced full binary tree’s nodes will be on the bottom level, which has one more node than all subsequent levels.

Extended Binary Tree: Listed as a particular kind of binary tree is the Extended Binary Tree. The table of contents lists “Extended Binary Tree” as one of the tree types. In the portions that are provided,Do, not, however, define or explain what a “Extended Binary Tree” is in the sections that are specified.

Binary Search Tree (BST): Node keys are ordered in this binary tree. Right child of a node must have key larger than or equal to parent’s key, whereas left child must have key less. This distinguishes it. This order allows efficient node location, addition, and removal in O(log N) time when the tree is balanced. These operations may become O(N) if an imbalance occurs.

Expression Tree: An algebraic expression can be represented by a binary tree called an expression tree. An expression tree’s inorder traversal, for instance, visits locations in a sequence that corresponds to the expression’s standard form.

Python Building and Operational Procedures

Python may be used to construct and manipulate several of these tree types. The implementation of Binary Search Trees.

Constructing a Binary Search Tree (BST)

Nodes are arranged by their keys in a Binary Search Tree class, which usually starts empty. The constructor sets root node to None. An inner Node class with pointers to the node’s parent, left child, right child, and element is a typical approach to describe tree nodes.

Example:

class BinarySearchTree(object):
    """
    A binary search tree class.
    The tree organizes nodes by their keys. Initially, it is empty.
    """
    def __init__(self):
        self.__root = None
    # Inner class to represent individual nodes in the tree
    class __Node(object):
        """
        Non-public class for storing a node.
        Each node holds a key and associated data.
        """
        def __init__(self, key, data):
            self.key = key
            self.data = data
            self.leftChild = None  
            self.rightChild = None
    def insert(self, key, data):
        """
        Inserts a new node with the given key and data into the tree.
        If the key already exists, the new node is placed in the right subtree
        (allowing for duplicate keys).
        """
        newNode = self.__Node(key, data) 
        if self.__root is None: 
            self.__root = newNode
            return
        current = self.__root
        while True:
            if key < current.key: 
                if current.leftChild is None:
                    current.leftChild = newNode
                    break
                current = current.leftChild
            else: # If new key is greater than or equal, go right
                if current.rightChild is None:
                    current.rightChild = newNode
                    break
                current = current.rightChild
    def search(self, key):
        """
        Searches for a node with the given key.
        Returns the data associated with the first instance of the key found, None otherwise.
        """
        current = self.__root
        while current is not None:
            if key == current.key:
                return current.data
            elif key < current.key:
                current = current.leftChild
            else: # key > current.key or key == current.key (due to insertion logic)
                current = current.rightChild
        return None  
    def min_node(self, node=None):
        """
        Finds the node with the minimum key in the subtree rooted at 'node'.
        If 'node' is None, it starts from the root of the tree.
        Returns the node itself.
        """
        if node is None:
            node = self.__root
        if node is None:
            return None 
        current = node
        while current.leftChild is not None:
            current = current.leftChild
        return current
    def max_node(self, node=None):
        """
        Finds the node with the maximum key in the subtree rooted at 'node'.
        If 'node' is None, it starts from the root of the tree.
        Returns the node itself.
        """
        if node is None:
            node = self.__root
        if node is None:
            return None 
        current = node
        while current.rightChild is not None:
            current = current.rightChild
        return current
    def delete(self, key):
        """
        Deletes the node with the given key from the tree.
        Note: This implementation will delete the first instance of a key
        found during traversal. If duplicate keys exist, only one will be removed.
        Returns True if the node was deleted, False otherwise.
        """
        # Helper function to find the node and its parent
        def _find_node_and_parent(key):
            current = self.__root
            parent = None
            while current is not None:
                if key == current.key:
                    return current, parent
                parent = current
                if key < current.key:
                    current = current.leftChild
                else: # key > current.key or key == current.key (due to insertion logic for duplicates)
                    current = current.rightChild
            return None, None 
        node_to_delete, parent = _find_node_and_parent(key)
        if node_to_delete is None:
            return False  
        # Case 1: Node to delete is a leaf node
        if node_to_delete.leftChild is None and node_to_delete.rightChild is None:
            if parent is None:  
                self.__root = None
            elif parent.leftChild == node_to_delete:
                parent.leftChild = None
            else:
                parent.rightChild = None
            return True
        # Case 2: Node to delete has only one child
        if node_to_delete.leftChild is None:
            child = node_to_delete.rightChild
            if parent is None: # Root node
                self.__root = child
            elif parent.leftChild == node_to_delete:
                parent.leftChild = child
            else:
                parent.rightChild = child
            return True
        elif node_to_delete.rightChild is None:
            child = node_to_delete.leftChild
            if parent is None: # Root node
                self.__root = child
            elif parent.leftChild == node_to_delete:
                parent.leftChild = child
            else:
                parent.rightChild = child
            return True
        # Case 3: Node to delete has two children
        # Find the in-order successor (smallest in the right subtree)
        successor_parent = node_to_delete
        successor = node_to_delete.rightChild
        while successor.leftChild is not None:
            successor_parent = successor
            successor = successor.leftChild
        # Copy successor's data to the node to be deleted
        node_to_delete.key = successor.key
        node_to_delete.data = successor.data
        # Delete the successor (which now has at most one child)
        if successor_parent.leftChild == successor:
            successor_parent.leftChild = successor.rightChild
        else:
            successor_parent.rightChild = successor.rightChild
        return True
    def inorder_traversal(self):
        """
        Performs an in-order traversal of the tree and returns a list
        of (key, data) tuples in ascending order of keys.
        If duplicate keys exist, they will appear in the order they are visited.
        """
        result = []
        def _inorder(node):
            if node:
                _inorder(node.leftChild)
                result.append((node.key, node.data))
                _inorder(node.rightChild)
        _inorder(self.__root)
        return result
    def is_empty(self):
        """
        Checks if the tree is empty.
        """
        return self.__root is None
    def get_root(self):
        """
        Returns the root node of the tree. (For internal/testing purposes)
        """
        return self.__root
#  Program Usage and Output 
if __name__ == "__main__":
    bst = BinarySearchTree()
    print("--- Inserting elements ---")
    bst.insert(50, "Apple")
    bst.insert(30, "Banana")
    bst.insert(70, "Cherry")
    bst.insert(20, "Date")
    bst.insert(40, "Elderberry")
    bst.insert(60, "Fig")
    bst.insert(80, "Grape")
    bst.insert(35, "Honeydew")
    bst.insert(45, "Ice Cream")
    bst.insert(75, "Jalapeno")
    bst.insert(85, "Kiwi")
    bst.insert(50, "Another Apple") 
    print("In-order traversal after insertions:")
    print(bst.inorder_traversal())
    print("-" * 30)
    print("\n Searching for elements")
    print(f"Search for 40: {bst.search(40)}")
    print(f"Search for 90 (not present): {bst.search(90)}")
    print(f"Search for 50 (first instance): {bst.search(50)}")
    print("-" * 30)
    print("\n Min/Max elements ")
    min_node = bst.min_node()
    if min_node:
        print(f"Minimum key: {min_node.key}, Data: {min_node.data}")
    else:
        print("Tree is empty.")
    max_node = bst.max_node()
    if max_node:
        print(f"Maximum key: {max_node.key}, Data: {max_node.data}")
    else:
        print("Tree is empty.")
    print("-" * 30)
    print("\n Deleting elements ")
    print("\nDeleting 20 (leaf node):")
    bst.delete(20)
    print("In-order traversal after deleting 20:")
    print(bst.inorder_traversal())
    print("\nDeleting 60 (node with one child - right):")
    bst.delete(60)
    print("In-order traversal after deleting 60:")
    print(bst.inorder_traversal())
    print("\nDeleting 30 (node with two children):")
    bst.delete(30)
    print("In-order traversal after deleting 30:")
    print(bst.inorder_traversal())
    print("\nDeleting 50 (root node with two children):")
    bst.delete(50) # This will delete the first 50 encountered
    print("In-order traversal after deleting 50:")
    print(bst.inorder_traversal())
    print("\nAttempting to delete non-existent key (99):")
    print(f"Deletion successful: {bst.delete(99)}")
    print("In-order traversal (should be unchanged):")
    print(bst.inorder_traversal())
    print("\n Emptying the tree")
    keys_to_delete = [80, 40, 35, 45, 70, 75, 85, 50] 
    for key in keys_to_delete:
        print(f"Deleting {key}...")
        bst.delete(key)
    print("In-order traversal after emptying:")
    print(bst.inorder_traversal())
    print(f"Is tree empty? {bst.is_empty()}")
    print("-" * 30)
    print("\n Testing on an empty tree ")
    empty_bst = BinarySearchTree()
    print(f"Is empty_bst empty? {empty_bst.is_empty()}")
    print(f"Search for 10 in empty_bst: {empty_bst.search(10)}")
    print(f"Delete 10 from empty_bst: {empty_bst.delete(10)}")
    print("In-order traversal of empty_bst:")
    print(empty_bst.inorder_traversal())

Output:

Inserting elements
In-order traversal after insertions:
[(20, 'Date'), (30, 'Banana'), (35, 'Honeydew'), (40, 'Elderberry'), (45, 'Ice Cream'), (50, 'Apple'), (50, 'Another Apple'), (60, 'Fig'), (70, 'Cherry'), (75, 'Jalapeno'), (80, 'Grape'), (85, 'Kiwi')]
 Searching for elements
Search for 40: Elderberry
Search for 90 (not present): None
Search for 50 (first instance): Apple
 Min/Max elements 
Minimum key: 20, Data: Date
Maximum key: 85, Data: Kiwi
 Deleting elements 
Deleting 20 (leaf node):
In-order traversal after deleting 20:
[(30, 'Banana'), (35, 'Honeydew'), (40, 'Elderberry'), (45, 'Ice Cream'), (50, 'Apple'), (50, 'Another Apple'), (60, 'Fig'), (70, 'Cherry'), (75, 'Jalapeno'), (80, 'Grape'), (85, 'Kiwi')]
Deleting 60 (node with one child - right):
In-order traversal after deleting 60:
[(30, 'Banana'), (35, 'Honeydew'), (40, 'Elderberry'), (45, 'Ice Cream'), (50, 'Apple'), (50, 'Another Apple'), (70, 'Cherry'), (75, 'Jalapeno'), (80, 'Grape'), (85, 'Kiwi')]
Deleting 30 (node with two children):
In-order traversal after deleting 30:
[(35, 'Honeydew'), (40, 'Elderberry'), (45, 'Ice Cream'), (50, 'Apple'), (50, 'Another Apple'), (70, 'Cherry'), (75, 'Jalapeno'), (80, 'Grape'), (85, 'Kiwi')]
Deleting 50 (root node with two children):
In-order traversal after deleting 50:
[(35, 'Honeydew'), (40, 'Elderberry'), (45, 'Ice Cream'), (50, 'Another Apple'), (70, 'Cherry'), (75, 'Jalapeno'), (80, 'Grape'), (85, 'Kiwi')]
Attempting to delete non-existent key (99):
Deletion successful: False
In-order traversal (should be unchanged):
[(35, 'Honeydew'), (40, 'Elderberry'), (45, 'Ice Cream'), (50, 'Another Apple'), (70, 'Cherry'), (75, 'Jalapeno'), (80, 'Grape'), (85, 'Kiwi')]
 Emptying the tree
Deleting 80...
Deleting 40...
Deleting 35...
Deleting 45...
Deleting 70...
Deleting 75...
Deleting 85...
Deleting 50...
In-order traversal after emptying:
Is tree empty? True
 Testing on an empty tree 
Is empty_bst empty? True
Search for 10 in empty_bst: None
Delete 10 from empty_bst: False
In-order traversal of empty_bst:

Together with pointers to its left and right children, the key and data would be contained in the Node class. For debugging, you can add its str() function to display contents without displaying child nodes.

Building an Expression Tree

It is possible to create an expression tree from a postfix expression. Although whole subtrees are kept on a stack rather than operands, the method is comparable to evaluating a postfix expression. Upon encountering an operand (variable or number), the stack is pushed with the trivial tree that contains that value. A new subtree with the operator as the root is created by combining two subtrees from the stack when an operator is found.

Example:

class ExpressionTree:
    """
    Represents a node in an expression tree.
    Can be an operator or an operand (literal/variable).
    """
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.leftChild = left
        self.rightChild = right
    def attach(self, left, right):
        """
        Attaches left and right children to this node.
        Used when an operator node is created and its operands (subtrees) are identified.
        """
        self.leftChild = left
        self.rightChild = right
    def is_leaf(self):
        """
        Checks if this node is a leaf node (i.e., an operand).
        """
        return self.leftChild is None and self.rightChild is None
    def inorder_expression(self):
        """
        Performs an in-order traversal to reconstruct the infix expression.
        Adds parentheses to correctly represent operator precedence based on tree structure.
        """
        if self.is_leaf():
            return str(self.value)
        else:
            left_expr = self.leftChild.inorder_expression()
            right_expr = self.rightChild.inorder_expression()
            # For operators, put parentheses around the sub-expression
            return f"({left_expr} {self.value} {right_expr})"
# Conceptual steps based on build_expression_tree [73, 74]
def build_expression_tree(tokens):
    """
    Builds an expression tree from a list of tokens in postfix (Reverse Polish Notation) order.
    
    Args:
        tokens (list): A list of strings representing the postfix expression.
                       Operators: '+', '-', '*', '/'
                       Operands: Numbers or variables
    
    Returns:
        ExpressionTree: The root node of the constructed expression tree.
    """
    S = [] 
    operators = {'+', '-', '*', '/'} 
    for t in tokens:
        if t in operators: 
            # Pop right and left operands (subtrees)
            # Ensure there are enough operands on the stack
            if len(S) < 2:
                raise ValueError(f"Invalid postfix expression: Not enough operands for operator '{t}'")
            right_subtree = S.pop()
            left_subtree = S.pop()
            
            # Create a new tree with operator as root and subtrees as children
            new_tree = ExpressionTree(t) 
            new_tree.attach(left_subtree, right_subtree) 
            S.append(new_tree)
        else: 
            S.append(ExpressionTree(t)) 
    
    # After processing all tokens, the stack should contain the final expression tree
    if len(S) != 1:
        raise ValueError("Invalid postfix expression: Stack did not end with a single tree.")
    return S.pop()
# Program Usage and Output 
if _name_ == "_main_":
    print("--- Building Expression Trees ---")
    # Example 1: Basic addition
    # Postfix: 3 4 +  -> Infix: (3 + 4)
    tokens1 = ['3', '4', '+']
    print(f"\nPostfix Expression: {tokens1}")
    try:
        tree1 = build_expression_tree(tokens1)
        print(f"Infix Expression: {tree1.inorder_expression()}")
    except ValueError as e:
        print(f"Error: {e}")
    # Example 2: Multiplication and addition
    # Postfix: 3 4 + 5 * -> Infix: ((3 + 4) * 5)
    tokens2 = ['3', '4', '+', '5', '*']
    print(f"\nPostfix Expression: {tokens2}")
    try:
        tree2 = build_expression_tree(tokens2)
        print(f"Infix Expression: {tree2.inorder_expression()}")
    except ValueError as e:
        print(f"Error: {e}")
    # Example 3: More complex expression
    # Postfix: A B C * + D / -> Infix: ((A + (B * C)) / D)
    tokens3 = ['A', 'B', 'C', '*', '+', 'D', '/']
    print(f"\nPostfix Expression: {tokens3}")
    try:
        tree3 = build_expression_tree(tokens3)
        print(f"Infix Expression: {tree3.inorder_expression()}")
    except ValueError as e:
        print(f"Error: {e}")
    # Example 4: Expression with subtraction and multiple digits
    # Postfix: 10 2 - 3 * -> Infix: ((10 - 2) * 3)
    tokens4 = ['10', '2', '-', '3', '*']
    print(f"\nPostfix Expression: {tokens4}")
    try:
        tree4 = build_expression_tree(tokens4)
        print(f"Infix Expression: {tree4.inorder_expression()}")
    except ValueError as e:
        print(f"Error: {e}")
    print("\n Testing Error Cases ")
    # Error Case 1: Not enough operands
    tokens_error1 = ['3', '+']
    print(f"\nPostfix Expression (Error Expected): {tokens_error1}")
    try:
        build_expression_tree(tokens_error1)
    except ValueError as e:
        print(f"Error caught: {e}")
    # Error Case 2: Too many operands/not enough operators
    tokens_error2 = ['3', '4', '5', '+']
    print(f"\nPostfix Expression (Error Expected): {tokens_error2}")
    try:
        build_expression_tree(tokens_error2)
    except ValueError as e:
        print(f"Error caught: {e}")

Output:

Building Expression Trees 
Postfix Expression: ['3', '4', '+']
Infix Expression: (3 + 4)
Postfix Expression: ['3', '4', '+', '5', '*']
Infix Expression: ((3 + 4) * 5)
Postfix Expression: ['A', 'B', 'C', '*', '+', 'D', '/']
Infix Expression: ((A + (B * C)) / D)
Postfix Expression: ['10', '2', '-', '3', '*']
Infix Expression: ((10 - 2) * 3)
 Testing Error Cases
Postfix Expression (Error Expected): ['3', '+']
Error caught: Invalid postfix expression: Not enough operands for operator '+'
Postfix Expression (Error Expected): ['3', '4', '5', '+']
Error caught: Invalid postfix expression: Stack did not end with a single tree.

This illustrates how Python implements and uses various tree architectures, striking a balance between academic knowledge and real-world application.

Kowsalya
Kowsalya
Hi, I'm Kowsalya a B.Com graduate and currently working as an Author at Govindhtech Solutions. I'm deeply passionate about publishing the latest tech news and tutorials that bringing insightful updates to readers. I enjoy creating step-by-step guides and making complex topics easier to understand for everyone.
Index