🚧 We are still building the lighthouse. Hang tight!

projectlighthouse logoprojectlighthouse

Sign in with your Google or GitHub account to get started

Binary Search Trees

BST property, searching, and why BSTs are useful. Understand the ordering invariant that makes BSTs efficient.

A Binary Search Tree (BST) is a binary tree with a special ordering property: for every node, all values in its left subtree are smaller, and all values in its right subtree are larger. This property enables efficient searching, insertion, and deletion.

The BST Property

For every node with value x:

  • All nodes in the left subtree have values < x
  • All nodes in the right subtree have values > x
Valid BST:              Not a BST:

       8                      8
      / \                    / \
     3   10                 3   10
    / \    \               / \    \
   1   6   14             1   9   14    <- 9 > 8 but in left subtree
      / \   /                / \   /
     4   7 13              4   7 13

Why BSTs?

The ordering property allows binary search in a tree structure:

  • Search: O(log n) average, O(n) worst
  • Insert: O(log n) average, O(n) worst
  • Delete: O(log n) average, O(n) worst

Compare to: | Data Structure | Search | Insert | Delete | |----------------|--------|--------|--------| | Sorted Array | O(log n) | O(n) | O(n) | | Linked List | O(n) | O(1) | O(1) | | Hash Table | O(1) | O(1) | O(1) | | BST (balanced) | O(log n) | O(log n) | O(log n) |

BSTs maintain sorted order while allowing efficient insertion and deletion.

Node Structure

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

class BST:
    def __init__(self):
        self.root = None

Searching

Follow the BST property to navigate:

def search(self, val):
    return self._search(self.root, val)

def _search(self, node, val):
    if node is None:
        return None

    if val == node.val:
        return node
    elif val < node.val:
        return self._search(node.left, val)
    else:
        return self._search(node.right, val)
Searching for 6 in BST:

       8
      / \
     3   10
    / \    \
   1   6   14
      / \
     4   7

Step 1: 6 < 8, go left
Step 2: 6 > 3, go right
Step 3: 6 == 6, found!

Iterative version:

def search_iterative(self, val):
    node = self.root

    while node:
        if val == node.val:
            return node
        elif val < node.val:
            node = node.left
        else:
            node = node.right

    return None

Finding Minimum and Maximum

Minimum: leftmost node. Maximum: rightmost node.

def find_min(self):
    if not self.root:
        return None

    node = self.root
    while node.left:
        node = node.left
    return node.val

def find_max(self):
    if not self.root:
        return None

    node = self.root
    while node.right:
        node = node.right
    return node.val

Inorder Traversal is Sorted

An inorder traversal of a BST visits nodes in sorted order. This property is useful for validating BSTs and finding the k-th smallest element.

Validating a BST

A tree is a valid BST if inorder traversal produces sorted values:

def is_valid_bst(root):
    def validate(node, min_val, max_val):
        if node is None:
            return True

        if node.val <= min_val or node.val >= max_val:
            return False

        return (validate(node.left, min_val, node.val) and
                validate(node.right, node.val, max_val))

    return validate(root, float('-inf'), float('inf'))

Or using inorder traversal:

def is_valid_bst_inorder(root):
    prev = float('-inf')

    def inorder(node):
        nonlocal prev

        if node is None:
            return True

        if not inorder(node.left):
            return False

        if node.val <= prev:
            return False
        prev = node.val

        return inorder(node.right)

    return inorder(root)

Successor and Predecessor

Successor: Next larger value Predecessor: Previous smaller value

def successor(root, val):
    """Find smallest value greater than val."""
    successor = None
    node = root

    while node:
        if val < node.val:
            successor = node
            node = node.left
        else:
            node = node.right

    return successor.val if successor else None

def predecessor(root, val):
    """Find largest value smaller than val."""
    predecessor = None
    node = root

    while node:
        if val > node.val:
            predecessor = node
            node = node.right
        else:
            node = node.left

    return predecessor.val if predecessor else None

Floor and Ceiling

Floor: Largest value ≤ key Ceiling: Smallest value ≥ key

def floor(root, key):
    """Largest value <= key."""
    result = None
    node = root

    while node:
        if key == node.val:
            return node.val
        elif key < node.val:
            node = node.left
        else:
            result = node.val
            node = node.right

    return result

def ceiling(root, key):
    """Smallest value >= key."""
    result = None
    node = root

    while node:
        if key == node.val:
            return node.val
        elif key > node.val:
            node = node.right
        else:
            result = node.val
            node = node.left

    return result

Time Complexity

| Operation | Average | Worst (Skewed) | |-----------|---------|----------------| | Search | O(log n) | O(n) | | Insert | O(log n) | O(n) | | Delete | O(log n) | O(n) | | Min/Max | O(log n) | O(n) |

Worst Case: Skewed Trees

If you insert sorted data into a BST, it becomes a linked list with O(n) operations. Self-balancing trees (AVL, Red-Black) guarantee O(log n) by maintaining balance.

Balanced vs Unbalanced

Balanced BST (height 2):          Skewed BST (height 4):

       4                               1
      / \                               \
     2   6                               2
    / \ / \                               \
   1  3 5  7                               3
                                            \
                                             4
                                              \
                                               5

Search for 5: 3 steps              Search for 5: 5 steps

Range Queries

Find all values in range [low, high]:

def range_query(root, low, high):
    result = []

    def inorder(node):
        if node is None:
            return

        if node.val > low:
            inorder(node.left)

        if low <= node.val <= high:
            result.append(node.val)

        if node.val < high:
            inorder(node.right)

    inorder(root)
    return result

k-th Smallest Element

def kth_smallest(root, k):
    count = 0
    result = None

    def inorder(node):
        nonlocal count, result

        if node is None or result is not None:
            return

        inorder(node.left)

        count += 1
        if count == k:
            result = node.val
            return

        inorder(node.right)

    inorder(root)
    return result

Applications

  1. Database indexes: Fast range queries
  2. Symbol tables: Compiler symbol lookup
  3. File systems: Directory organization
  4. Autocomplete: Prefix-based searching
  5. Scheduling: Priority management

Key Takeaways

  1. BST property: left < node < right
  2. Enables O(log n) search, insert, delete when balanced
  3. Inorder traversal yields sorted order
  4. Skewed trees degrade to O(n) performance
  5. Self-balancing variants (AVL, Red-Black) maintain O(log n)
  6. Supports efficient range queries and ordered operations

BSTs are fundamental for maintaining sorted data dynamically. Understanding their properties and limitations prepares you for more advanced tree structures and database indexing concepts.

Notes & Highlights

© 2025 projectlighthouse. All rights reserved.