Binary Tree
A binary tree is a hierarchical data structure where each node has at most two children, referred to as the left child and the right child. This implementation provides a general-purpose binary tree with comprehensive operations for construction, traversal, and manipulation.
Features
- Hierarchical structure - nodes organized in parent-child relationships
- Flexible insertion - insert nodes as left or right children
- Multiple traversals - inorder, preorder, postorder, and level-order
- Tree properties - height, size, balance checking, diameter calculation
- Search operations - find nodes and check existence
- Tree manipulation - mirroring, comparison, deletion
- Type safe - generic implementation with type checking
- Utility operations - leaf collection, tree validation
Time Complexities
| Operation | Average Case | Worst Case | Description |
|---|---|---|---|
| Search | O(log n) | O(n) | Find node by value |
| Insert | O(log n) | O(n) | Add new node |
| Delete | O(log n) | O(n) | Remove node |
| Traversal | O(n) | O(n) | Visit all nodes |
| Height | O(n) | O(n) | Calculate tree height |
| Size | O(1) | O(1) | Get number of nodes |
| Balance Check | O(n) | O(n) | Verify if balanced |
| Mirror | O(n) | O(n) | Reverse tree structure |
| Diameter | O(n) | O(n) | Find longest path |
Space Complexity: O(n) for storage, O(h) for recursion stack where h is height
Basic Usage
Creating and Initializing
from algo.data_structs.trees.binary_tree import BinaryTree, TreeNode
# Create empty tree
tree = BinaryTree()
print(tree.is_empty()) # True
print(tree.size()) # 0
print(tree.height()) # -1
# Create tree with root node
tree = BinaryTree(10)
print(tree.is_empty()) # False
print(tree.size()) # 1
print(tree.height()) # 0
print(tree.root.data) # 10
# Tree is generic - can specify type
int_tree = BinaryTree[int](42)
str_tree = BinaryTree[str]("root")
Creating Nodes
# Create individual nodes
node = TreeNode(5)
print(node.data) # 5
print(node.left) # None
print(node.right) # None
# Create node with children
left_child = TreeNode(3)
right_child = TreeNode(7)
parent = TreeNode(5, left_child, right_child)
print(parent.data) # 5
print(parent.left.data) # 3
print(parent.right.data) # 7
# Check node properties
print(node.is_leaf()) # True
print(parent.is_leaf()) # False
print(parent.has_both_children()) # True
Building Trees
# Build tree step by step
tree = BinaryTree(1)
# Insert left and right children
tree.insert_left(1, 2) # Insert 2 as left child of 1
tree.insert_right(1, 3) # Insert 3 as right child of 1
print(tree.size()) # 3
print(tree.height()) # 1
# Insert more nodes
tree.insert_left(2, 4) # Insert 4 as left child of 2
tree.insert_right(2, 5) # Insert 5 as right child of 2
print(tree.size()) # 5
print(tree.height()) # 2
# Tree structure:
# 1
# / \
# 2 3
# / \
# 4 5
Handling Existing Children
tree = BinaryTree(1)
tree.insert_left(1, 2)
# When inserting where child already exists, existing child becomes child of new node
tree.insert_left(1, 3) # 3 becomes new left child, 2 becomes left child of 3
print(tree.root.left.data) # 3
print(tree.root.left.left.data) # 2
# Tree structure:
# 1
# /
# 3
#/
#2
Tree Traversals
Inorder Traversal (Left-Root-Right)
tree = BinaryTree(1)
tree.insert_left(1, 2)
tree.insert_right(1, 3)
tree.insert_left(2, 4)
tree.insert_right(2, 5)
# Inorder: left subtree, root, right subtree
result = tree.inorder_traversal()
print(result) # [4, 2, 5, 1, 3]
# Common use: Get sorted order in BST
Preorder Traversal (Root-Left-Right)
# Preorder: root, left subtree, right subtree
result = tree.preorder_traversal()
print(result) # [1, 2, 4, 5, 3]
# Common use: Tree serialization, expression trees
Postorder Traversal (Left-Right-Root)
# Postorder: left subtree, right subtree, root
result = tree.postorder_traversal()
print(result) # [4, 5, 2, 3, 1]
# Common use: Tree deletion, calculating directory sizes
Level-order Traversal (Breadth-First)
# Level-order: visit nodes level by level
result = tree.level_order_traversal()
print(result) # [1, 2, 3, 4, 5]
# Common use: Tree serialization, finding shortest path
Traversal Comparisons
# Different traversals for same tree
tree = BinaryTree("A")
tree.insert_left("A", "B")
tree.insert_right("A", "C")
tree.insert_left("B", "D")
tree.insert_right("B", "E")
print("Inorder: ", tree.inorder_traversal()) # ['D', 'B', 'E', 'A', 'C']
print("Preorder: ", tree.preorder_traversal()) # ['A', 'B', 'D', 'E', 'C']
print("Postorder: ", tree.postorder_traversal()) # ['D', 'E', 'B', 'C', 'A']
print("Level-order:", tree.level_order_traversal()) # ['A', 'B', 'C', 'D', 'E']
Search Operations
Finding Nodes
tree = BinaryTree(10)
tree.insert_left(10, 5)
tree.insert_right(10, 15)
tree.insert_left(5, 3)
tree.insert_right(5, 7)
# Check if values exist
print(tree.find(10)) # True
print(tree.find(5)) # True
print(tree.find(99)) # False
# Search works with any data type
str_tree = BinaryTree("root")
str_tree.insert_left("root", "left")
str_tree.insert_right("root", "right")
print(str_tree.find("left")) # True
print(str_tree.find("missing")) # False
Working with Empty Trees
empty_tree = BinaryTree()
# All operations handle empty trees gracefully
print(empty_tree.find(42)) # False
print(empty_tree.inorder_traversal()) # []
print(empty_tree.get_leaves()) # []
print(empty_tree.is_balanced()) # True (empty tree is balanced)
Tree Properties
Size and Height
tree = BinaryTree(1)
tree.insert_left(1, 2)
tree.insert_right(1, 3)
tree.insert_left(2, 4)
print(f"Size: {tree.size()}") # Size: 4
print(f"Height: {tree.height()}") # Height: 2
# Height is number of edges from root to deepest leaf
# Empty tree has height -1, single node has height 0
Leaf Nodes
# Get all leaf nodes (nodes with no children)
leaves = tree.get_leaves()
print(f"Leaves: {leaves}") # Leaves: [4, 3]
# Leaves are collected in traversal order
Balance Checking
# Check if tree is height-balanced
# (heights of subtrees differ by at most 1)
# Balanced tree
balanced_tree = BinaryTree(1)
balanced_tree.insert_left(1, 2)
balanced_tree.insert_right(1, 3)
print(balanced_tree.is_balanced()) # True
# Unbalanced tree (like a linked list)
unbalanced_tree = BinaryTree(1)
unbalanced_tree.insert_left(1, 2)
unbalanced_tree.insert_left(2, 3)
unbalanced_tree.insert_left(3, 4)
print(unbalanced_tree.is_balanced()) # False
Diameter Calculation
# Diameter is longest path between any two nodes
tree = BinaryTree(1)
tree.insert_left(1, 2)
tree.insert_right(1, 3)
tree.insert_left(2, 4)
tree.insert_right(2, 5)
diameter = tree.diameter()
print(f"Diameter: {diameter}") # Diameter: 4
# Path: 4 -> 2 -> 1 -> 3 (or 5 -> 2 -> 1 -> 3)
Tree Manipulation
Mirroring Trees
tree = BinaryTree(1)
tree.insert_left(1, 2)
tree.insert_right(1, 3)
tree.insert_left(2, 4)
tree.insert_right(2, 5)
print("Original inorder:", tree.inorder_traversal()) # [4, 2, 5, 1, 3]
# Mirror the tree (swap all left and right children)
tree.mirror()
print("Mirrored inorder:", tree.inorder_traversal()) # [3, 1, 5, 2, 4]
# Mirror again to restore
tree.mirror()
print("Restored inorder:", tree.inorder_traversal()) # [4, 2, 5, 1, 3]
Tree Comparison
# Create two identical trees
tree1 = BinaryTree(1)
tree1.insert_left(1, 2)
tree1.insert_right(1, 3)
tree2 = BinaryTree(1)
tree2.insert_left(1, 2)
tree2.insert_right(1, 3)
print(tree1.are_identical(tree2)) # True
# Modify one tree
tree2.insert_left(2, 4)
print(tree1.are_identical(tree2)) # False
# Empty trees are identical
empty1 = BinaryTree()
empty2 = BinaryTree()
print(empty1.are_identical(empty2)) # True
Node Deletion
tree = BinaryTree(10)
tree.insert_left(10, 5)
tree.insert_right(10, 15)
tree.insert_left(5, 3)
tree.insert_right(5, 7)
print(f"Size before: {tree.size()}") # Size before: 5
# Delete leaf node
success = tree.delete(3)
print(f"Deleted 3: {success}") # Deleted 3: True
print(f"Size after: {tree.size()}") # Size after: 4
print(tree.find(3)) # False
# Delete node with children (uses inorder successor)
tree.delete(5)
print(f"Size after deleting 5: {tree.size()}")
# Try to delete non-existent node
success = tree.delete(99)
print(f"Deleted 99: {success}") # Deleted 99: False
Working with Different Types
String Trees
# Tree with string data
str_tree = BinaryTree("root")
str_tree.insert_left("root", "left")
str_tree.insert_right("root", "right")
str_tree.insert_left("left", "deep")
print(str_tree.inorder_traversal()) # ['deep', 'left', 'root', 'right']
print(str_tree.find("deep")) # True
print(str_tree.height()) # 2
Numeric Trees
# Tree with mixed numeric types
num_tree = BinaryTree(1.0)
num_tree.insert_left(1.0, 2)
num_tree.insert_right(1.0, 3.14)
print(num_tree.inorder_traversal()) # [2, 1.0, 3.14]
print(num_tree.find(3.14)) # True
Custom Object Trees
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __eq__(self, other):
return isinstance(other, Person) and self.name == other.name and self.age == other.age
def __str__(self):
return f"{self.name}({self.age})"
# Tree with custom objects
alice = Person("Alice", 30)
bob = Person("Bob", 25)
charlie = Person("Charlie", 35)
person_tree = BinaryTree(alice)
person_tree.insert_left(alice, bob)
person_tree.insert_right(alice, charlie)
print(person_tree.find(bob)) # True
print(person_tree.size()) # 3
# Traversal returns objects
people = person_tree.inorder_traversal()
for person in people:
print(person) # Bob(25), Alice(30), Charlie(35)
Tree Display and Debugging
String Representation
# Empty tree
empty_tree = BinaryTree()
print(str(empty_tree)) # Empty Tree
# Single node tree
single_tree = BinaryTree(42)
print(single_tree)
# Root: 42
# Multi-level tree
tree = BinaryTree(1)
tree.insert_left(1, 2)
tree.insert_right(1, 3)
tree.insert_left(2, 4)
print(tree)
# Root: 1
# L--- 2
# L--- 4
# R--- None
# R--- 3
# Detailed representation for debugging
print(repr(tree)) # BinaryTree(size=4, height=2, root=TreeNode(1))
Tree Visualization
def print_tree_info(tree, name="Tree"):
"""Print comprehensive tree information."""
print(f"\n{name} Information:")
print(f" Size: {tree.size()}")
print(f" Height: {tree.height()}")
print(f" Is empty: {tree.is_empty()}")
print(f" Is balanced: {tree.is_balanced()}")
print(f" Diameter: {tree.diameter()}")
print(f" Leaves: {tree.get_leaves()}")
print(f" Inorder: {tree.inorder_traversal()}")
print(f" Level-order: {tree.level_order_traversal()}")
# Example usage
tree = BinaryTree(10)
tree.insert_left(10, 5)
tree.insert_right(10, 15)
tree.insert_left(5, 3)
tree.insert_right(15, 20)
print_tree_info(tree, "Sample Binary Tree")
Error Handling
# Operations on empty tree
empty_tree = BinaryTree()
# Safe operations
print(empty_tree.is_empty()) # True
print(empty_tree.size()) # 0
print(empty_tree.height()) # -1
print(empty_tree.find(42)) # False
print(empty_tree.inorder_traversal()) # []
# Insertion operations that fail gracefully
print(empty_tree.insert_left(1, 2)) # False (no parent with value 1)
print(empty_tree.insert_right(1, 3)) # False
# Deletion operations
print(empty_tree.delete(42)) # False
# Operations with non-existent parents
tree = BinaryTree(1)
print(tree.insert_left(99, 2)) # False (no node with value 99)
print(tree.insert_right(99, 3)) # False
Common Patterns
Building from Lists
def build_complete_tree(values):
"""Build a complete binary tree from a list of values."""
if not values:
return BinaryTree()
tree = BinaryTree(values[0])
# Use level-order insertion for complete tree
from collections import deque
queue = deque([(tree.root, 0)]) # (node, index)
while queue:
node, index = queue.popleft()
left_index = 2 * index + 1
right_index = 2 * index + 2
if left_index < len(values):
left_child = TreeNode(values[left_index])
node.left = left_child
queue.append((left_child, left_index))
if right_index < len(values):
right_child = TreeNode(values[right_index])
node.right = right_child
queue.append((right_child, right_index))
return tree
# Build complete tree from list
values = [1, 2, 3, 4, 5, 6, 7]
complete_tree = build_complete_tree(values)
print(complete_tree.level_order_traversal()) # [1, 2, 3, 4, 5, 6, 7]
Tree Serialization
def serialize_tree(tree):
"""Serialize tree to list using preorder traversal."""
def serialize_node(node):
if node is None:
return [None]
return [node.data] + serialize_node(node.left) + serialize_node(node.right)
return serialize_node(tree.root)
def deserialize_tree(data):
"""Deserialize list back to tree."""
def deserialize_node():
val = next(iterator)
if val is None:
return None
node = TreeNode(val)
node.left = deserialize_node()
node.right = deserialize_node()
return node
iterator = iter(data)
root = deserialize_node()
tree = BinaryTree()
tree.root = root
# Note: Would need to update size manually or recalculate
return tree
# Example usage
tree = BinaryTree(1)
tree.insert_left(1, 2)
tree.insert_right(1, 3)
tree.insert_left(2, 4)
serialized = serialize_tree(tree)
print(f"Serialized: {serialized}")
# Deserialize back
restored_tree = deserialize_tree(serialized)
print(f"Restored: {restored_tree.preorder_traversal()}")
Tree Validation
def validate_binary_tree(tree):
"""Validate binary tree structure and properties."""
issues = []
if tree.is_empty():
return ["Tree is empty - no validation needed"]
# Check size consistency
def count_nodes(node):
if node is None:
return 0
return 1 + count_nodes(node.left) + count_nodes(node.right)
actual_size = count_nodes(tree.root)
if actual_size != tree.size():
issues.append(f"Size mismatch: reported {tree.size()}, actual {actual_size}")
# Check for cycles (shouldn't exist in proper tree)
visited = set()
def check_cycles(node):
if node is None:
return False
if id(node) in visited:
return True
visited.add(id(node))
return check_cycles(node.left) or check_cycles(node.right)
if check_cycles(tree.root):
issues.append("Cycle detected in tree structure")
# Check balance factor
if not tree.is_balanced():
issues.append("Tree is not height-balanced")
return issues if issues else ["Tree structure is valid"]
# Validate a tree
tree = BinaryTree(10)
tree.insert_left(10, 5)
tree.insert_right(10, 15)
validation_results = validate_binary_tree(tree)
for result in validation_results:
print(result)
Practical Applications
Expression Trees
class ExpressionTree(BinaryTree):
"""Binary tree for mathematical expressions."""
def __init__(self, expression=None):
super().__init__(expression)
def evaluate(self):
"""Evaluate the expression tree."""
return self._evaluate_node(self.root)
def _evaluate_node(self, node):
if node is None:
return 0
# If leaf node, return the number
if node.is_leaf():
return float(node.data)
# Evaluate left and right subtrees
left_val = self._evaluate_node(node.left)
right_val = self._evaluate_node(node.right)
# Apply operator
if node.data == '+':
return left_val + right_val
elif node.data == '-':
return left_val - right_val
elif node.data == '*':
return left_val * right_val
elif node.data == '/':
return left_val / right_val if right_val != 0 else float('inf')
return 0
# Build expression tree for: (3 + 4) * (5 - 2)
expr_tree = ExpressionTree('*')
expr_tree.insert_left('*', '+')
expr_tree.insert_right('*', '-')
expr_tree.insert_left('+', '3')
expr_tree.insert_right('+', '4')
expr_tree.insert_left('-', '5')
expr_tree.insert_right('-', '2')
result = expr_tree.evaluate()
print(f"Expression result: {result}") # 21.0
# Print the expression in infix notation
def infix_expression(tree):
def infix_node(node):
if node is None:
return ""
if node.is_leaf():
return str(node.data)
left = infix_node(node.left)
right = infix_node(node.right)
return f"({left} {node.data} {right})"
return infix_node(tree.root)
print(f"Expression: {infix_expression(expr_tree)}") # ((3 + 4) * (5 - 2))
Decision Trees
class DecisionTree(BinaryTree):
"""Simple decision tree for classification."""
def __init__(self, question=None):
super().__init__(question)
def classify(self, data):
"""Classify data point using decision tree."""
return self._classify_node(self.root, data)
def _classify_node(self, node, data):
if node is None:
return "Unknown"
# If leaf node, return the classification
if node.is_leaf():
return node.data
# Otherwise, evaluate the condition and go left or right
condition = node.data
if self._evaluate_condition(condition, data):
return self._classify_node(node.left, data)
else:
return self._classify_node(node.right, data)
def _evaluate_condition(self, condition, data):
"""Simple condition evaluation."""
# This would be more sophisticated in a real implementation
if "age >= 18" in condition:
return data.get("age", 0) >= 18
elif "income > 50000" in condition:
return data.get("income", 0) > 50000
return False
# Build decision tree for loan approval
decision_tree = DecisionTree("age >= 18")
decision_tree.insert_left("age >= 18", "income > 50000")
decision_tree.insert_right("age >= 18", "Not Eligible")
decision_tree.insert_left("income > 50000", "Approved")
decision_tree.insert_right("income > 50000", "Needs Guarantor")
# Test classification
applicant1 = {"age": 25, "income": 60000}
applicant2 = {"age": 16, "income": 30000}
applicant3 = {"age": 30, "income": 40000}
print(f"Applicant 1: {decision_tree.classify(applicant1)}") # Approved
print(f"Applicant 2: {decision_tree.classify(applicant2)}") # Not Eligible
print(f"Applicant 3: {decision_tree.classify(applicant3)}") # Needs Guarantor
File System Representation
class FileSystemTree(BinaryTree):
"""Represent file system structure as binary tree."""
def __init__(self, name=None, is_directory=True):
super().__init__({"name": name, "is_directory": is_directory})
def add_file(self, parent_name, file_name, is_directory=False):
"""Add a file or directory to the tree."""
parent_node = self._find_by_name(parent_name)
if parent_node and parent_node.data["is_directory"]:
file_data = {"name": file_name, "is_directory": is_directory}
# Simple insertion logic - left for directories, right for files
if is_directory:
if parent_node.left is None:
parent_node.left = TreeNode(file_data)
else:
# Find rightmost position in left subtree
current = parent_node.left
while current.right is not None:
current = current.right
current.right = TreeNode(file_data)
else:
if parent_node.right is None:
parent_node.right = TreeNode(file_data)
else:
# Find rightmost position in right subtree
current = parent_node.right
while current.right is not None:
current = current.right
current.right = TreeNode(file_data)
return True
return False
def _find_by_name(self, name):
"""Find node by file/directory name."""
def search_node(node):
if node is None:
return None
if node.data["name"] == name:
return node
left_result = search_node(node.left)
if left_result:
return left_result
return search_node(node.right)
return search_node(self.root)
def list_contents(self, directory_name):
"""List contents of a directory."""
dir_node = self._find_by_name(directory_name)
if dir_node and dir_node.data["is_directory"]:
contents = []
# Collect from left subtree (directories)
if dir_node.left:
self._collect_children(dir_node.left, contents)
# Collect from right subtree (files)
if dir_node.right:
self._collect_children(dir_node.right, contents)
return contents
return []
def _collect_children(self, node, contents):
"""Collect immediate children names."""
if node:
contents.append(node.data["name"])
# Also collect siblings
if node.right:
self._collect_children(node.right, contents)
# Build file system tree
fs_tree = FileSystemTree("root", True)
fs_tree.add_file("root", "documents", True)
fs_tree.add_file("root", "config.txt", False)
fs_tree.add_file("documents", "projects", True)
fs_tree.add_file("documents", "readme.md", False)
# List directory contents
print("Root contents:", fs_tree.list_contents("root"))
print("Documents contents:", fs_tree.list_contents("documents"))
Huffman Coding Tree
import heapq
from collections import defaultdict, Counter
class HuffmanNode:
def __init__(self, char=None, freq=0, left=None, right=None):
self.char = char
self.freq = freq
self.left = left
self.right = right
def __lt__(self, other):
return self.freq < other.freq
class HuffmanTree(BinaryTree):
"""Huffman coding tree for text compression."""
def __init__(self, text=""):
super().__init__()
if text:
self.build_from_text(text)
def build_from_text(self, text):
"""Build Huffman tree from text."""
# Count character frequencies
freq_map = Counter(text)
# Create leaf nodes for each character
heap = [HuffmanNode(char, freq) for char, freq in freq_map.items()]
heapq.heapify(heap)
# Build tree bottom-up
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
merged = HuffmanNode(
char=None, # Internal nodes have no character
freq=left.freq + right.freq,
left=left,
right=right
)
heapq.heappush(heap, merged)
# Set root
if heap:
self.root = heap[0]
def generate_codes(self):
"""Generate Huffman codes for each character."""
codes = {}
def generate_node_codes(node, code=""):
if node:
if node.char is not None: # Leaf node
codes[node.char] = code if code else "0" # Single char case
else: # Internal node
generate_node_codes(node.left, code + "0")
generate_node_codes(node.right, code + "1")
generate_node_codes(self.root)
return codes
def encode(self, text):
"""Encode text using Huffman codes."""
codes = self.generate_codes()
return ''.join(codes[char] for char in text)
def decode(self, encoded_text):
"""Decode Huffman-encoded text."""
if not self.root:
return ""
decoded = []
current = self.root
for bit in encoded_text:
if bit == '0':
current = current.left
else:
current = current.right
# If we reach a leaf node
if current.char is not None:
decoded.append(current.char)
current = self.root # Reset to root
return ''.join(decoded)
# Example usage
text = "hello world"
huffman_tree = HuffmanTree(text)
# Generate codes
codes = huffman_tree.generate_codes()
print("Huffman codes:")
for char, code in codes.items():
print(f" '{char}': {code}")
# Encode and decode
encoded = huffman_tree.encode(text)
decoded = huffman_tree.decode(encoded)
print(f"\nOriginal: '{text}'")
print(f"Encoded: '{encoded}'")
print(f"Decoded: '{decoded}'")
print(f"Compression ratio: {len(encoded) / (len(text) * 8):.2f}")
Use Cases
When to Use Binary Trees
Good for:
- Hierarchical data representation
- Expression parsing and evaluation
- Decision making systems
- File system structures
- Huffman coding and compression
- Game trees and AI decisions
- Syntax trees in compilers
Examples:
# Hierarchical organization
org_tree = BinaryTree("CEO")
org_tree.insert_left("CEO", "CTO")
org_tree.insert_right("CEO", "CFO")
# Mathematical expressions
expr_tree = BinaryTree("+")
expr_tree.insert_left("+", "3")
expr_tree.insert_right("+", "4")
# Decision processes
decision_tree = BinaryTree("condition")
decision_tree.insert_left("condition", "action_true")
decision_tree.insert_right("condition", "action_false")
When NOT to Use Binary Trees
Avoid for:
- Sequential data access (use arrays/lists)
- Key-value storage without ordering (use hash tables)
- Queue/stack operations (use specialized structures)
- Frequently changing size with balanced requirement (use self-balancing trees)
Better alternatives:
# For sequential access, use list
sequential_data = [1, 2, 3, 4, 5]
print(sequential_data[2]) # Direct access
# For key-value pairs, use dictionary
data_map = {"key1": "value1", "key2": "value2"}
print(data_map["key1"]) # O(1) lookup
# For balanced operations, use balanced tree implementations
# (AVL, Red-Black trees in other modules)
Performance Characteristics
Time Complexity Analysis
- Balanced tree operations: O(log n) for search, insert, delete
- Unbalanced tree operations: O(n) worst case (degrades to linked list)
- Traversal operations: O(n) - must visit all nodes
- Tree property calculations: O(n) - requires tree traversal
Space Complexity
- Storage: O(n) for n nodes
- Recursion stack: O(h) where h is height
- Worst case height: O(n) for skewed tree
- Best case height: O(log n) for balanced tree
Comparison with Other Tree Types
| Tree Type | Search | Insert | Delete | Balanced | Use Case |
|---|---|---|---|---|---|
| Binary Tree | O(n) | O(n) | O(n) | No | General hierarchy |
| Binary Search Tree | O(log n) | O(log n) | O(log n) | No | Sorted data |
| AVL Tree | O(log n) | O(log n) | O(log n) | Yes | Frequent searches |
| Red-Black Tree | O(log n) | O(log n) | O(log n) | Yes | Insertions/deletions |
| Heap | O(n) | O(log n) | O(log n) | Complete | Priority queues |
API Reference
Binary Tree Implementation
A binary tree is a hierarchical data structure where each node has at most two children, referred to as the left child and the right child. This implementation provides a general-purpose binary tree with common operations.
Key Features:
- Node insertion and deletion
- Tree traversals (inorder, preorder, postorder, level-order)
- Tree properties (height, size, balance checking)
- Search operations
- Tree validation and manipulation
Time Complexities (for a balanced tree):
- Search: O(log n) average, O(n) worst case
- Insert: O(log n) average, O(n) worst case
- Delete: O(log n) average, O(n) worst case
- Traversal: O(n)
Space Complexity: O(n) for storage, O(h) for recursion stack where h is height
BinaryTree(root_data=None)
Bases: BinaryTreeInterface[T]
A binary tree implementation with comprehensive operations.
This is a general binary tree (not necessarily a binary search tree), so it doesn't maintain any ordering properties.
Initialize a binary tree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root_data
|
Optional[T]
|
Data for the root node (None creates empty tree) |
None
|
__repr__()
Return detailed representation for debugging.
__str__()
Return string representation of the tree.
are_identical(other)
Check if this tree is identical to another tree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
BinaryTree[T]
|
Another binary tree to compare with |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if trees are identical, False otherwise |
delete(data)
Delete the first node with the given data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
Data of the node to delete |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if deletion successful, False if node not found |
diameter()
Calculate the diameter of the tree.
Diameter is the longest path between any two nodes in the tree.
Returns:
| Type | Description |
|---|---|
int
|
Diameter of the tree |
find(data)
Check if a node with the given data exists in the tree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
Data to search for |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if found, False otherwise |
get_leaves()
Get all leaf nodes in the tree.
Returns:
| Type | Description |
|---|---|
List[T]
|
List of data from leaf nodes |
height()
Return the height of the tree.
Height is defined as the number of edges on the longest path from root to a leaf. Empty tree has height -1.
inorder_traversal()
Perform inorder traversal (left, root, right).
Returns:
| Type | Description |
|---|---|
List[T]
|
List of node data in inorder sequence |
insert_left(parent_data, new_data)
Insert a new node as the left child of the node with parent_data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parent_data
|
T
|
Data of the parent node |
required |
new_data
|
T
|
Data for the new node |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if insertion successful, False if parent not found |
insert_right(parent_data, new_data)
Insert a new node as the right child of the node with parent_data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parent_data
|
T
|
Data of the parent node |
required |
new_data
|
T
|
Data for the new node |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if insertion successful, False if parent not found |
is_balanced()
Check if the tree is height-balanced.
A tree is balanced if the heights of the two subtrees of any node differ by at most one.
Returns:
| Type | Description |
|---|---|
bool
|
True if balanced, False otherwise |
is_empty()
Check if the tree is empty.
level_order_traversal()
Perform level-order traversal (breadth-first).
Returns:
| Type | Description |
|---|---|
List[T]
|
List of node data in level-order sequence |
mirror()
Mirror the tree (swap left and right subtrees recursively).
postorder_traversal()
Perform postorder traversal (left, right, root).
Returns:
| Type | Description |
|---|---|
List[T]
|
List of node data in postorder sequence |
preorder_traversal()
Perform preorder traversal (root, left, right).
Returns:
| Type | Description |
|---|---|
List[T]
|
List of node data in preorder sequence |
size()
Return the number of nodes in the tree.
TreeNode(data, left=None, right=None)
Bases: BinaryTreeNodeInterface[T]
A node in a binary tree.
Each node contains data and references to left and right children.
Initialize a tree node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
The data stored in this node |
required |
left
|
Optional[TreeNode[T]]
|
Reference to left child node |
None
|
right
|
Optional[TreeNode[T]]
|
Reference to right child node |
None
|
__repr__()
Return detailed string representation for debugging.
__str__()
Return string representation of the node.
has_both_children()
Check if this node has both left and right children.
has_left_child()
Check if this node has a left child.
has_one_child()
Check if this node has exactly one child.
has_right_child()
Check if this node has a right child.
is_leaf()
Check if this node is a leaf (has no children).