Binary Trees
A Binary Tree is a tree structure where every node has at most two children, referred to as the left child and the right child.
BinaryNode(key, value):
1class BinaryNode:2 def __init__(self, key, value=None):3 self.key = key # Search key / index value4 self.value = value # Associated data payload5 self.left = None # Reference to left child (BinaryNode)6 self.right = None # Reference to right child (BinaryNode)
To build a Binary Search Tree (BST), we insert keys recursively. At every node, we check if the new key is smaller (routing it left) or larger (routing it right), until an empty slot is found.
Insert(node, key):
1def insert(node, key, value=None) -> BinaryNode:2 # If the tree is empty, return a new node3 if node is None:4 return BinaryNode(key, value)56 # Otherwise, recur down the tree7 if key < node.key:8 node.left = insert(node.left, key, value)9 elif key > node.key:10 node.right = insert(node.right, key, value)1112 return node
Binary search trees allow fast lookup operations. At each node, we compare the target key. Since we discard half the remaining tree at each comparison, the search takes O(log N) time.
Search(node, target_key):
1def search(node, target_key) -> BinaryNode:2 # Base Cases: root is null or key is present at root3 if node is None or node.key == target_key:4 return node56 # Target is smaller than root's key7 if target_key < node.key:8 return search(node.left, target_key)910 # Target is greater than root's key11 return search(node.right, target_key)
Traversal means visiting all nodes of a tree in a specific order. For binary trees, we have three standard depth-first search (DFS) traversal orderings.
Traverse(node, order):
1def traverse(node, order_type="inorder"):2 if node is None:3 return45 if order_type == "preorder":6 print(node.key) # Visit Root7 traverse(node.left) # Left child8 traverse(node.right) # Right child910 elif order_type == "inorder":11 traverse(node.left) # Left child12 print(node.key) # Visit Root13 traverse(node.right) # Right child1415 elif order_type == "postorder":16 traverse(node.left) # Left child17 traverse(node.right) # Right child18 print(node.key) # Visit Root
Deleting a node in a BST (Hibbard Deletion) must preserve the sorted property. We handle three cases: leaves (remove node), single-child nodes (link child to parent), and two-children nodes (replace with inorder successor).
Delete(root, key):
1def delete_node(root, key) -> BinaryNode:2 if root is None:3 return root45 # 1. Recur down the tree6 if key < root.key:7 root.left = delete_node(root.left, key)8 elif key > root.key:9 root.right = delete_node(root.right, key)10 else:11 # Node with only one child or no child12 if root.left is None:13 return root.right14 elif root.right is None:15 return root.left1617 # Node with two children: get inorder successor18 temp = get_min_value_node(root.right)19 root.key = temp.key20 # Delete inorder successor21 root.right = delete_node(root.right, temp.key)2223 return root
Self-balancing trees (like AVL or Red-Black trees) perform local rotations to maintain a maximum height of $O(\log N)$. If a node becomes unbalanced (height difference > 1), we rotate nodes to restore balance.
Left Rotation (z):
1def rotate_left(z) -> BinaryNode:2 y = z.right3 T2 = y.left45 # Perform rotation6 y.left = z7 z.right = T289 # Update heights10 z.height = 1 + max(get_height(z.left), get_height(z.right))11 y.height = 1 + max(get_height(y.left), get_height(y.right))1213 return y # New root
Breadth-First Search (BFS) or Level-Order Traversal visits nodes level-by-level, starting from the root and going left-to-right. We implement it using a First-In-First-Out (FIFO) queue.
LevelOrder(root):
1def level_order(root) -> list:2 if root is None:3 return []45 result = []6 queue = [root] # FIFO queue78 while queue:9 node = queue.pop(0) # Dequeue10 result.append(node.key)1112 # Enqueue children13 if node.left:14 queue.append(node.left)15 if node.right:16 queue.append(node.right)1718 return result