CrackedRuby CrackedRuby

Overview

A binary tree consists of nodes connected by edges, where each node contains data and references to at most two child nodes, designated as left and right. The topmost node serves as the root, and nodes without children are leaves. This structure provides logarithmic time complexity for many operations when balanced, making binary trees fundamental to algorithm design and data organization.

Binary trees originated from mathematical graph theory and became central to computer science in the 1960s with the development of binary search trees. The structure maps naturally to hierarchical relationships and enables divide-and-conquer algorithms that split problems into left and right subtrees.

The basic binary tree node contains three elements: a data value, a reference to the left child, and a reference to the right child. Null references indicate the absence of a child. This simple structure supports various tree types through different insertion and balancing rules.

class TreeNode
  attr_accessor :value, :left, :right
  
  def initialize(value)
    @value = value
    @left = nil
    @right = nil
  end
end

# Creating a simple binary tree
root = TreeNode.new(10)
root.left = TreeNode.new(5)
root.right = TreeNode.new(15)
root.left.left = TreeNode.new(3)
root.left.right = TreeNode.new(7)
# => Tree structure:
#        10
#       /  \
#      5    15
#     / \
#    3   7

Binary trees serve as the foundation for more specialized structures including binary search trees, heaps, expression trees, and Huffman coding trees. Each variant applies specific ordering or structural properties to optimize particular operations.

Key Principles

Node Structure: Each node stores data and maintains references to left and right children. The absence of a child is represented by nil. This uniform structure enables recursive algorithms that process nodes identically regardless of their position in the tree.

Tree Traversal: Three standard depth-first traversal orders exist. Inorder traversal visits the left subtree, then the node, then the right subtree. Preorder traversal visits the node before its subtrees. Postorder traversal visits the node after its subtrees. Breadth-first traversal processes nodes level by level.

# Inorder: left, root, right
def inorder(node, result = [])
  return result if node.nil?
  inorder(node.left, result)
  result << node.value
  inorder(node.right, result)
  result
end
# => [3, 5, 7, 10, 15]

# Preorder: root, left, right
def preorder(node, result = [])
  return result if node.nil?
  result << node.value
  preorder(node.left, result)
  preorder(node.right, result)
  result
end
# => [10, 5, 3, 7, 15]

# Postorder: left, right, root
def postorder(node, result = [])
  return result if node.nil?
  postorder(node.left, result)
  postorder(node.right, result)
  result << node.value
  result
end
# => [3, 7, 5, 15, 10]

Height and Depth: Tree height measures the longest path from root to leaf. Node depth measures distance from the root. A balanced tree maintains height proportional to log(n) where n is the node count. Height directly impacts operation time complexity.

Complete and Full Trees: A complete binary tree fills all levels except possibly the last, which fills left to right. A full binary tree has nodes with either zero or two children. These properties enable array-based implementations and predict performance characteristics.

Binary Search Property: Binary search trees maintain the invariant that left children contain smaller values and right children contain larger values. This property enables O(log n) search in balanced trees through binary search logic.

Ruby Implementation

Ruby does not provide built-in binary tree classes, requiring custom implementation. The node-based approach using classes and instance variables provides clarity and matches object-oriented design principles.

class BinaryTree
  attr_reader :root
  
  def initialize
    @root = nil
  end
  
  def insert(value)
    @root = insert_recursive(@root, value)
  end
  
  private
  
  def insert_recursive(node, value)
    return TreeNode.new(value) if node.nil?
    
    if value < node.value
      node.left = insert_recursive(node.left, value)
    else
      node.right = insert_recursive(node.right, value)
    end
    
    node
  end
end

tree = BinaryTree.new
[10, 5, 15, 3, 7, 12, 17].each { |val| tree.insert(val) }

Search Implementation: Binary search trees support efficient lookup by comparing the target value with the current node and recursing left or right.

class BinaryTree
  def search(value)
    search_recursive(@root, value)
  end
  
  private
  
  def search_recursive(node, value)
    return nil if node.nil?
    return node if node.value == value
    
    if value < node.value
      search_recursive(node.left, value)
    else
      search_recursive(node.right, value)
    end
  end
end

tree.search(7)  # => TreeNode with value 7
tree.search(20) # => nil

Deletion Handling: Node deletion requires handling three cases: nodes with no children, one child, or two children. The two-child case replaces the node with its inorder successor.

class BinaryTree
  def delete(value)
    @root = delete_recursive(@root, value)
  end
  
  private
  
  def delete_recursive(node, value)
    return nil if node.nil?
    
    if value < node.value
      node.left = delete_recursive(node.left, value)
    elsif value > node.value
      node.right = delete_recursive(node.right, value)
    else
      # Node found
      return node.right if node.left.nil?
      return node.left if node.right.nil?
      
      # Two children: get inorder successor
      min_node = find_min(node.right)
      node.value = min_node.value
      node.right = delete_recursive(node.right, min_node.value)
    end
    
    node
  end
  
  def find_min(node)
    node = node.left while node.left
    node
  end
end

Level Order Traversal: Breadth-first traversal uses a queue to process nodes level by level, requiring iterative implementation.

def level_order(root)
  return [] if root.nil?
  
  result = []
  queue = [root]
  
  until queue.empty?
    node = queue.shift
    result << node.value
    
    queue << node.left if node.left
    queue << node.right if node.right
  end
  
  result
end
# => [10, 5, 15, 3, 7, 12, 17]

Tree Properties: Computing height, size, and balance information requires recursive traversal.

def height(node)
  return -1 if node.nil?
  1 + [height(node.left), height(node.right)].max
end

def size(node)
  return 0 if node.nil?
  1 + size(node.left) + size(node.right)
end

def balanced?(node)
  return true if node.nil?
  
  left_height = height(node.left)
  right_height = height(node.right)
  
  (left_height - right_height).abs <= 1 &&
    balanced?(node.left) &&
    balanced?(node.right)
end

Path Operations: Finding paths and checking for path sums requires tracking state during traversal.

def find_path(node, target, path = [])
  return nil if node.nil?
  
  path << node.value
  return path if node.value == target
  
  left_result = find_path(node.left, target, path.dup)
  return left_result if left_result
  
  find_path(node.right, target, path.dup)
end

def has_path_sum?(node, sum)
  return false if node.nil?
  return sum == node.value if node.left.nil? && node.right.nil?
  
  remaining = sum - node.value
  has_path_sum?(node.left, remaining) || has_path_sum?(node.right, remaining)
end

Implementation Approaches

Binary Search Tree (BST): Maintains sorted order with left children smaller and right children larger than the parent. Supports O(log n) operations when balanced but degrades to O(n) when skewed.

class BST
  def insert(value)
    @root = insert_node(@root, value)
  end
  
  def find_range(min, max, result = [])
    range_search(@root, min, max, result)
    result
  end
  
  private
  
  def range_search(node, min, max, result)
    return if node.nil?
    
    range_search(node.left, min, max, result) if node.value > min
    result << node.value if node.value >= min && node.value <= max
    range_search(node.right, min, max, result) if node.value < max
  end
end

AVL Tree: Self-balancing BST that maintains height difference of at most one between subtrees through rotation operations. Guarantees O(log n) operations with higher insertion overhead.

class AVLNode
  attr_accessor :value, :left, :right, :height
  
  def initialize(value)
    @value = value
    @left = nil
    @right = nil
    @height = 1
  end
end

class AVLTree
  def insert(value)
    @root = insert_with_balance(@root, value)
  end
  
  private
  
  def insert_with_balance(node, value)
    return AVLNode.new(value) if node.nil?
    
    if value < node.value
      node.left = insert_with_balance(node.left, value)
    else
      node.right = insert_with_balance(node.right, value)
    end
    
    node.height = 1 + [get_height(node.left), get_height(node.right)].max
    balance = get_balance(node)
    
    # Left-Left case
    if balance > 1 && value < node.left.value
      return rotate_right(node)
    end
    
    # Right-Right case
    if balance < -1 && value > node.right.value
      return rotate_left(node)
    end
    
    # Left-Right case
    if balance > 1 && value > node.left.value
      node.left = rotate_left(node.left)
      return rotate_right(node)
    end
    
    # Right-Left case
    if balance < -1 && value < node.right.value
      node.right = rotate_right(node.right)
      return rotate_left(node)
    end
    
    node
  end
  
  def rotate_left(z)
    y = z.right
    t2 = y.left
    
    y.left = z
    z.right = t2
    
    z.height = 1 + [get_height(z.left), get_height(z.right)].max
    y.height = 1 + [get_height(y.left), get_height(y.right)].max
    
    y
  end
  
  def rotate_right(z)
    y = z.left
    t3 = y.right
    
    y.right = z
    z.left = t3
    
    z.height = 1 + [get_height(z.left), get_height(z.right)].max
    y.height = 1 + [get_height(y.left), get_height(y.right)].max
    
    y
  end
  
  def get_height(node)
    node.nil? ? 0 : node.height
  end
  
  def get_balance(node)
    node.nil? ? 0 : get_height(node.left) - get_height(node.right)
  end
end

Heap Structure: Complete binary tree maintaining heap property where parent nodes are smaller (min-heap) or larger (max-heap) than children. Supports O(log n) insertion and O(1) minimum/maximum access.

class MinHeap
  def initialize
    @heap = []
  end
  
  def insert(value)
    @heap << value
    bubble_up(@heap.length - 1)
  end
  
  def extract_min
    return nil if @heap.empty?
    
    min = @heap[0]
    @heap[0] = @heap.pop
    bubble_down(0) unless @heap.empty?
    min
  end
  
  private
  
  def bubble_up(index)
    return if index == 0
    
    parent_index = (index - 1) / 2
    if @heap[index] < @heap[parent_index]
      @heap[index], @heap[parent_index] = @heap[parent_index], @heap[index]
      bubble_up(parent_index)
    end
  end
  
  def bubble_down(index)
    left = 2 * index + 1
    right = 2 * index + 2
    smallest = index
    
    smallest = left if left < @heap.length && @heap[left] < @heap[smallest]
    smallest = right if right < @heap.length && @heap[right] < @heap[smallest]
    
    if smallest != index
      @heap[index], @heap[smallest] = @heap[smallest], @heap[index]
      bubble_down(smallest)
    end
  end
end

Trie (Prefix Tree): Tree where each node represents a character and paths from root to nodes form strings. Optimizes prefix matching and autocomplete operations.

class TrieNode
  attr_accessor :children, :is_end_of_word
  
  def initialize
    @children = {}
    @is_end_of_word = false
  end
end

class Trie
  def initialize
    @root = TrieNode.new
  end
  
  def insert(word)
    node = @root
    word.each_char do |char|
      node.children[char] ||= TrieNode.new
      node = node.children[char]
    end
    node.is_end_of_word = true
  end
  
  def search(word)
    node = find_node(word)
    node && node.is_end_of_word
  end
  
  def starts_with?(prefix)
    !find_node(prefix).nil?
  end
  
  private
  
  def find_node(prefix)
    node = @root
    prefix.each_char do |char|
      return nil unless node.children[char]
      node = node.children[char]
    end
    node
  end
end

Practical Examples

Expression Tree Evaluation: Binary trees represent arithmetic expressions where internal nodes store operators and leaves store operands. Postorder traversal evaluates the expression.

class ExpressionNode
  attr_accessor :value, :left, :right
  
  def initialize(value, left = nil, right = nil)
    @value = value
    @left = left
    @right = right
  end
  
  def operator?
    ['+', '-', '*', '/'].include?(@value)
  end
end

def evaluate_expression(node)
  return node.value.to_f unless node.operator?
  
  left_val = evaluate_expression(node.left)
  right_val = evaluate_expression(node.right)
  
  case node.value
  when '+' then left_val + right_val
  when '-' then left_val - right_val
  when '*' then left_val * right_val
  when '/' then left_val / right_val
  end
end

# Build tree for: (5 + 3) * (8 - 2)
root = ExpressionNode.new('*')
root.left = ExpressionNode.new('+', 
  ExpressionNode.new(5), 
  ExpressionNode.new(3))
root.right = ExpressionNode.new('-', 
  ExpressionNode.new(8), 
  ExpressionNode.new(2))

evaluate_expression(root)
# => 48.0

File System Representation: Hierarchical directory structures map naturally to binary trees where each node represents a file or directory.

class FileNode
  attr_accessor :name, :is_directory, :size, :left, :right
  
  def initialize(name, is_directory: false, size: 0)
    @name = name
    @is_directory = is_directory
    @size = size
    @left = nil
    @right = nil
  end
end

def calculate_total_size(node)
  return 0 if node.nil?
  return node.size unless node.is_directory
  
  node.size + calculate_total_size(node.left) + calculate_total_size(node.right)
end

def find_files_by_extension(node, extension, results = [])
  return results if node.nil?
  
  results << node.name if node.name.end_with?(extension)
  find_files_by_extension(node.left, extension, results)
  find_files_by_extension(node.right, extension, results)
  results
end

# Directory structure
root = FileNode.new('root', is_directory: true)
root.left = FileNode.new('docs', is_directory: true)
root.left.left = FileNode.new('readme.md', size: 1024)
root.left.right = FileNode.new('guide.md', size: 2048)
root.right = FileNode.new('config.yml', size: 512)

Lowest Common Ancestor: Finding the lowest common ancestor of two nodes requires tracking paths or using parent pointers.

def lowest_common_ancestor(root, p, q)
  return nil if root.nil?
  return root if root.value == p || root.value == q
  
  left = lowest_common_ancestor(root.left, p, q)
  right = lowest_common_ancestor(root.right, p, q)
  
  return root if left && right
  left || right
end

# For BST, more efficient approach using value ordering
def lca_bst(root, p, q)
  return nil if root.nil?
  
  if p < root.value && q < root.value
    lca_bst(root.left, p, q)
  elsif p > root.value && q > root.value
    lca_bst(root.right, p, q)
  else
    root
  end
end

Tree Serialization: Converting trees to and from string format enables storage and transmission.

def serialize(node)
  return 'null' if node.nil?
  "#{node.value},#{serialize(node.left)},#{serialize(node.right)}"
end

def deserialize(data)
  values = data.split(',')
  index = 0
  
  build_tree = lambda do
    return nil if index >= values.length || values[index] == 'null'
    
    node = TreeNode.new(values[index].to_i)
    index += 1
    node.left = build_tree.call
    node.right = build_tree.call
    node
  end
  
  build_tree.call
end

tree = TreeNode.new(1, TreeNode.new(2), TreeNode.new(3))
serialized = serialize(tree)
# => "1,2,null,null,3,null,null"
restored = deserialize(serialized)

Common Patterns

Recursive Traversal Pattern: Most tree operations follow the pattern of processing a node and recursing on children. Base case checks for nil, recursive case processes subtrees.

def process_tree(node)
  return base_case_value if node.nil?
  
  # Process current node
  current_result = process_node(node)
  
  # Recurse on children
  left_result = process_tree(node.left)
  right_result = process_tree(node.right)
  
  # Combine results
  combine(current_result, left_result, right_result)
end

# Example: Sum all values
def sum_tree(node)
  return 0 if node.nil?
  node.value + sum_tree(node.left) + sum_tree(node.right)
end

# Example: Count nodes
def count_nodes(node)
  return 0 if node.nil?
  1 + count_nodes(node.left) + count_nodes(node.right)
end

Iterative Traversal with Stack: Stack-based traversal avoids recursion limits and provides explicit control over traversal order.

def iterative_inorder(root)
  result = []
  stack = []
  current = root
  
  while current || !stack.empty?
    while current
      stack << current
      current = current.left
    end
    
    current = stack.pop
    result << current.value
    current = current.right
  end
  
  result
end

def iterative_preorder(root)
  return [] if root.nil?
  
  result = []
  stack = [root]
  
  until stack.empty?
    node = stack.pop
    result << node.value
    
    stack << node.right if node.right
    stack << node.left if node.left
  end
  
  result
end

Level-by-Level Processing: Processing nodes level by level requires tracking level boundaries in the queue.

def level_order_groups(root)
  return [] if root.nil?
  
  result = []
  queue = [root]
  
  until queue.empty?
    level_size = queue.length
    current_level = []
    
    level_size.times do
      node = queue.shift
      current_level << node.value
      
      queue << node.left if node.left
      queue << node.right if node.right
    end
    
    result << current_level
  end
  
  result
end
# => [[10], [5, 15], [3, 7, 12, 17]]

Parent Tracking: Maintaining parent references enables upward traversal and ancestor queries.

class NodeWithParent
  attr_accessor :value, :left, :right, :parent
  
  def initialize(value)
    @value = value
    @left = nil
    @right = nil
    @parent = nil
  end
end

def build_with_parents(node, parent = nil)
  return if node.nil?
  
  node.parent = parent
  build_with_parents(node.left, node)
  build_with_parents(node.right, node)
end

def find_path_to_root(node)
  path = []
  current = node
  
  while current
    path << current.value
    current = current.parent
  end
  
  path
end

Mirror/Inversion Pattern: Creating mirror images or checking for symmetry involves swapping left and right subtrees.

def mirror_tree(node)
  return nil if node.nil?
  
  left = mirror_tree(node.left)
  right = mirror_tree(node.right)
  
  node.left = right
  node.right = left
  node
end

def is_symmetric?(root)
  return true if root.nil?
  check_symmetry(root.left, root.right)
end

def check_symmetry(left, right)
  return true if left.nil? && right.nil?
  return false if left.nil? || right.nil?
  
  left.value == right.value &&
    check_symmetry(left.left, right.right) &&
    check_symmetry(left.right, right.left)
end

Performance Considerations

Time Complexity Analysis: Balanced binary search trees provide O(log n) search, insertion, and deletion. Unbalanced trees degrade to O(n) for these operations. Tree height determines performance, with height h yielding O(h) operations.

Complete binary trees maintain height floor(log n), ensuring logarithmic performance. Skewed trees have height n-1, reducing to linear time. Self-balancing trees like AVL and Red-Black trees guarantee O(log n) height through rotation operations.

Space Complexity: Recursive traversal uses O(h) stack space where h is tree height. Balanced trees use O(log n) space for recursion, while skewed trees use O(n). Iterative implementations with explicit stacks have the same space requirements.

# Stack space demonstration
def max_depth_space(node, depth = 0)
  return depth if node.nil?
  
  left_depth = max_depth_space(node.left, depth + 1)
  right_depth = max_depth_space(node.right, depth + 1)
  
  [left_depth, right_depth].max
end

# Iterative version uses explicit stack
def max_depth_iterative(root)
  return 0 if root.nil?
  
  max = 0
  stack = [[root, 1]]
  
  until stack.empty?
    node, depth = stack.pop
    max = depth if depth > max
    
    stack << [node.left, depth + 1] if node.left
    stack << [node.right, depth + 1] if node.right
  end
  
  max
end

Cache Locality: Array-based heap implementations provide better cache performance than pointer-based trees due to contiguous memory layout. Node-based trees suffer from pointer chasing and cache misses.

# Array-based complete binary tree (heap)
class ArrayHeap
  def initialize
    @array = []
  end
  
  def parent_index(i)
    (i - 1) / 2
  end
  
  def left_child_index(i)
    2 * i + 1
  end
  
  def right_child_index(i)
    2 * i + 2
  end
  
  # Better cache locality than pointer-based tree
  # Sequential array access vs random memory jumps
end

Balancing Overhead: Self-balancing trees trade insertion speed for guaranteed search performance. AVL trees perform more rotations than Red-Black trees but maintain stricter balance. Choose based on operation frequency.

# AVL: Stricter balance (faster searches, slower insertions)
# Height difference <= 1
# 1-2 rotations per insertion

# Red-Black: Relaxed balance (faster insertions, slightly slower searches)
# Black height property
# 0-3 rotations per insertion

Batch Operations: Building balanced trees from sorted data uses O(n) time through divide-and-conquer construction instead of n O(log n) insertions.

def sorted_array_to_bst(arr, start = 0, finish = arr.length - 1)
  return nil if start > finish
  
  mid = (start + finish) / 2
  node = TreeNode.new(arr[mid])
  
  node.left = sorted_array_to_bst(arr, start, mid - 1)
  node.right = sorted_array_to_bst(arr, mid + 1, finish)
  
  node
end

# O(n) construction vs O(n log n) sequential insertion
sorted = [1, 2, 3, 4, 5, 6, 7]
tree = sorted_array_to_bst(sorted)

Memory Overhead: Each node requires memory for data, left pointer, right pointer, and potential metadata (height, color, parent). Structure overhead can exceed data size for small values.

# Node memory breakdown
class TreeNode
  attr_accessor :value        # 8 bytes (reference)
  attr_accessor :left         # 8 bytes (reference)
  attr_accessor :right        # 8 bytes (reference)
  # Ruby object overhead: ~40 bytes
  # Total: ~64 bytes per node minimum
end

# For storing integers, overhead >> data size
# Consider packed arrays for small, complete trees

Common Pitfalls

Off-by-One Errors in Height Calculation: Tree height definitions vary between maximum edge count and maximum node count. Consistently choose one definition.

# Edge-based height (leaf = 0)
def height_edges(node)
  return -1 if node.nil?  # Empty tree has height -1
  1 + [height_edges(node.left), height_edges(node.right)].max
end

# Node-based height (leaf = 1)
def height_nodes(node)
  return 0 if node.nil?   # Empty tree has height 0
  1 + [height_nodes(node.left), height_nodes(node.right)].max
end

Forgetting Nil Checks: Every recursive call must check for nil nodes before dereferencing. Missing checks cause NoMethodError exceptions.

# Wrong: crashes on nil
def count_wrong(node)
  1 + count_wrong(node.left) + count_wrong(node.right)
end

# Correct: checks nil first
def count_correct(node)
  return 0 if node.nil?
  1 + count_correct(node.left) + count_correct(node.right)
end

Modifying Tree During Iteration: Inserting or deleting nodes while traversing breaks iteration logic. Collect modifications and apply after traversal completes.

# Wrong: modifying during traversal
def double_values_wrong(node)
  return if node.nil?
  node.value *= 2
  insert(node.value)  # Breaks traversal
  double_values_wrong(node.left)
  double_values_wrong(node.right)
end

# Correct: collect then modify
def double_values_correct(node, values = [])
  return values if node.nil?
  values << node.value
  double_values_correct(node.left, values)
  double_values_correct(node.right, values)
  values
end

values = double_values_correct(root)
values.each { |v| insert(v * 2) }

Incorrect BST Invariant Validation: Checking only immediate children misses violations deeper in the tree. Track valid range during recursion.

# Wrong: only checks immediate children
def valid_bst_wrong?(node)
  return true if node.nil?
  return false if node.left && node.left.value >= node.value
  return false if node.right && node.right.value <= node.value
  valid_bst_wrong?(node.left) && valid_bst_wrong?(node.right)
end

# Correct: tracks valid range
def valid_bst?(node, min = -Float::INFINITY, max = Float::INFINITY)
  return true if node.nil?
  return false if node.value <= min || node.value >= max
  
  valid_bst?(node.left, min, node.value) &&
    valid_bst?(node.right, node.value, max)
end

Memory Leaks from Circular References: Parent pointers create cycles that prevent garbage collection in languages without automatic cycle detection. Break references explicitly.

def delete_subtree(node)
  return if node.nil?
  
  delete_subtree(node.left)
  delete_subtree(node.right)
  
  # Break references to enable GC
  node.left = nil
  node.right = nil
  node.parent = nil if node.respond_to?(:parent)
end

Stack Overflow from Deep Recursion: Ruby's default stack size limits recursion depth to around 10,000 levels. Convert to iteration for very deep trees.

# Recursive: hits stack limit
def sum_recursive(node)
  return 0 if node.nil?
  node.value + sum_recursive(node.left) + sum_recursive(node.right)
end

# Iterative: handles arbitrary depth
def sum_iterative(root)
  return 0 if root.nil?
  
  sum = 0
  stack = [root]
  
  until stack.empty?
    node = stack.pop
    sum += node.value
    stack << node.left if node.left
    stack << node.right if node.right
  end
  
  sum
end

Confusing Tree Types: Binary trees, BSTs, balanced trees, and heaps have different properties and operations. Applying BST search logic to unordered binary trees produces incorrect results.

# BST search (requires ordering)
def bst_search(node, target)
  return nil if node.nil?
  return node if node.value == target
  
  if target < node.value
    bst_search(node.left, target)
  else
    bst_search(node.right, target)
  end
end

# General tree search (no ordering assumption)
def tree_search(node, target)
  return nil if node.nil?
  return node if node.value == target
  
  tree_search(node.left, target) || tree_search(node.right, target)
end

Reference

Node Structure

Component Description Typical Value
Value Data stored in node Any type
Left Reference to left child TreeNode or nil
Right Reference to right child TreeNode or nil
Parent Optional reference to parent TreeNode or nil

Traversal Methods

Method Order Use Case Time Space
Inorder Left, Root, Right Sorted output from BST O(n) O(h)
Preorder Root, Left, Right Tree copying, prefix expressions O(n) O(h)
Postorder Left, Right, Root Tree deletion, postfix expressions O(n) O(h)
Level Order Level by level Breadth-first operations O(n) O(w)

Tree Properties

Property Definition Balanced Unbalanced
Height Longest path from root to leaf O(log n) O(n)
Size Total number of nodes n n
Depth Distance from root Varies Varies
Width Maximum nodes at any level Varies 1 (skewed)

Common Tree Types

Type Property Search Insert Delete Space
Binary Tree At most 2 children O(n) O(n) O(n) O(n)
BST Left < Parent < Right O(h) O(h) O(h) O(n)
AVL Tree Balance factor <= 1 O(log n) O(log n) O(log n) O(n)
Red-Black Black height balanced O(log n) O(log n) O(log n) O(n)
Heap Parent vs children ordering O(n) O(log n) O(log n) O(n)

Operation Complexity

Operation Balanced Unbalanced Array Heap
Search O(log n) O(n) O(n)
Insert O(log n) O(n) O(log n)
Delete O(log n) O(n) O(log n)
Find Min/Max O(log n) O(n) O(1)
Traversal O(n) O(n) O(n)
Space O(n) O(n) O(n)

Rotation Operations

Rotation When Used Effect Complexity
Left Right-heavy tree Moves right child up O(1)
Right Left-heavy tree Moves left child up O(1)
Left-Right Left child right-heavy Two rotations O(1)
Right-Left Right child left-heavy Two rotations O(1)

Ruby Implementation Patterns

Pattern Code Example Use Case
Class-based node class TreeNode; attr_accessor :value, :left, :right; end Object-oriented design
Struct node Node = Struct.new(:value, :left, :right) Lightweight nodes
Recursive traversal def traverse(node); return if node.nil?; end Most operations
Iterative with stack stack = [root]; until stack.empty?; end Deep trees
Queue for level order queue = [root]; until queue.empty?; end Breadth-first

Common Validation Checks

Check Implementation Returns
Is empty root.nil? Boolean
Is leaf node.left.nil? && node.right.nil? Boolean
Has one child node.left.nil? XOR node.right.nil? Boolean
Is complete Check level by level Boolean
Is balanced abs(height(left) - height(right)) <= 1 Boolean
Is BST Check value ranges recursively Boolean