Module P-5·14 min read

Binary search tree invariants, insert/search/delete, and why balance is the difference between O(log n) and O(n).

Written by Jatin Jain Saraf · Architect of SupraScan

Module P-5 — Binary Search Trees: Insert, Search, Delete, and Validation

What this module covers: Module P-4 traversed a generic binary tree, whatever shape it happened to be. This module adds one invariant — every left subtree holds smaller values, every right subtree holds larger ones — and that single rule turns a tree into a structure that supports O(log n) search, exactly like Module F-11's binary search, but over a branching structure instead of a flat array. This module covers insert, search, delete (the trickiest of the three), and how to correctly validate that the invariant actually holds.


The BST Invariant

A binary search tree enforces one rule at every single node, not just at the root: everything in the left subtree is smaller than this node's value, and everything in the right subtree is larger.

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

Notice this holds recursively — not just "2 < 4 < 6" at the root, but also "1 < 2 < 3" at the node holding 2, and "5 < 6 < 7" at the node holding 6. This recursive property is exactly what makes Module P-4's inorder traversal produce sorted output: at every level, left-then-node-then-right is already the ascending order, because the invariant guarantees it.


Search: Binary Search, Applied to a Tree

Searching a BST follows the identical halving logic from Module F-11's binary search — at each node, one comparison rules out an entire subtree:

function search<T>(node: TreeNode<T> | null, target: T): boolean {
  if (node === null) return false;
  if (node.value === target) return true;
  return target < node.value
    ? search(node.left, target)   // target must be in the left subtree, if anywhere
    : search(node.right, target); // target must be in the right subtree, if anywhere
}

Just as Module F-11 discarded half the remaining array with each comparison, this discards an entire subtree with each comparison — if the tree is balanced (each subtree roughly half the size of its parent), this is O(log n), the same complexity class, same reasoning, now on a branching structure instead of a flat sorted array.


Insert: Find Where It Belongs, Then Attach

Inserting into a BST follows the same "compare and go left or right" logic as search — you simply keep going until you fall off the tree, then attach the new node there:

function insert<T>(node: TreeNode<T> | null, value: T): TreeNode<T> {
  if (node === null) return new TreeNode(value); // found the spot — attach here
  if (value < node.value) node.left = insert(node.left, value);
  else if (value > node.value) node.right = insert(node.right, value);
  return node; // unchanged if value already exists
}

This is O(log n) on a balanced tree, for the same reason search is — but there's an important caveat worth stating plainly: a BST built by inserting already-sorted data degenerates into a straight line, not a balanced tree, because every new value is larger (or smaller) than everything already inserted, so it always attaches at the same end.

let tree: TreeNode<number> | null = null;
for (const v of [1, 2, 3, 4, 5]) {
  tree = insert(tree, v); // every insert goes right, right, right... — a linked list in disguise
}
// Search is now O(n), not O(log n) — the tree has degenerated

This is the exact same shape as Module F-1's best/average/worst-case discussion: a plain BST is O(log n) on average, for reasonably random insertion order, but O(n) in the worst case. Self-balancing variants (AVL trees, Red-Black trees) exist specifically to guarantee O(log n) even in this worst case, by restructuring the tree during insertion/deletion to keep it roughly balanced — a mechanism this course doesn't implement in full, but worth knowing by name, because it's exactly what production-grade ordered structures (including the B-Trees covered in Module A-13) are built to guarantee.


Delete: Three Cases

Deletion is the operation that actually requires care, because removing a node must preserve the BST invariant for everything that's left. There are three distinct cases:

function deleteNode<T>(node: TreeNode<T> | null, value: T): TreeNode<T> | null {
  if (node === null) return null;
 
  if (value < node.value) {
    node.left = deleteNode(node.left, value);
  } else if (value > node.value) {
    node.right = deleteNode(node.right, value);
  } else {
    // Found the node to delete — now handle its three possible shapes:
 
    // Case 1: no children — just remove it
    if (node.left === null && node.right === null) return null;
 
    // Case 2: one child — replace this node with its only child
    if (node.left === null) return node.right;
    if (node.right === null) return node.left;
 
    // Case 3: two children — replace this node's value with its inorder successor
    // (the smallest value in the right subtree), then delete that successor from
    // the right subtree, where it's guaranteed to have at most one child.
    let successor = node.right;
    while (successor.left !== null) successor = successor.left;
    node.value = successor.value;
    node.right = deleteNode(node.right, successor.value);
  }
 
  return node;
}

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...