5 min read

Binary Search Trees: Invariants, Successor Deletion & Traversal Mechanics

The Binary Search Tree (BST) is a foundational hierarchical data structure designed to combine the rapid O(log N) search capabilities of a sorted array with the dynamic insertion and deletion flexibility of a linked list. By enforcing a strict ordering invariant across all nodes, a BST enables ordered key retrieval, predecessor/successor lookups, and range queries.

The Global BST Invariant

A common misconception is that a binary tree is a BST if every node is simply greater than its left child and smaller than its right child. This local condition is insufficient.

Formal BST Invariant: For any given node N with key K:

  • Every single key in the entire left subtree of N must be strictly less than K (∀ u ∈ Left(N), key(u) < K).
  • Every single key in the entire right subtree of N must be strictly greater than K (∀ v ∈ Right(N), key(v) > K).
  • Both the left and right subtrees must themselves be valid binary search trees.
1. Anatomy of a Valid Binary Search Tree & Range Invariants

The diagram below displays a canonical Valid Binary Search Tree alongside the Global Subtree Validation Trap:

Valid Binary Search Tree Architecture & Subtree Range Invariants Diagram showing a complete valid BST with in-order monotonic traversal alongside the global min-max range propagation that catches invalid BST traps. BST Architecture: Canonical Structure & Global Range Invariants ✓ CANONICAL VALID BST Every left subtree < node < every right subtree 50 Root: (-∞, +∞) 30 80 20 40 65 90 • In-Order Traversal (L → Root → R): [ 20 → 30 → 40 → 50 → 65 → 80 → 90 ] ✓ Always yields a strictly monotonically ascending sorted array. ✓ Enables O(h + k) range querying and k-th smallest element retrieval. ✗ THE INVALID BST TRAP Why local child checks (left < node < right) fail 50 Root: (-∞, +∞) 30 80 45 90 Violation Breakdown: • Local check passes: 45 < 80 (left child < parent). • Global invariant violated: 45 < 50 (in root 50's right subtree!). • Fix: Validate recursively with range bounds: (minVal, maxVal). Left child inherits (min, node.val); Right inherits (node.val, max).

2. The Three Fundamental Node Deletion Cases

Deleting a node from a BST requires preserving the global ordering invariant. The diagram below illustrates all three structural deletion cases:

BST Node Deletion: The Three Fundamental Cases Diagram showing Case 1 (Leaf removal), Case 2 (Single child bypass), and Case 3 (Two children with in-order successor swap and deletion). BST Node Deletion: The Three Cases CASE 1 & CASE 2: 0 OR 1 CHILD Direct pointer replacement without key swapping Case 1: Leaf Node (0 Children) 50 20 Delete (20) 50 null Action: Set parent.left = null Case 2: Single Child (1 Child) 50 80 90 Bypass (80) 50 90 Action: parent.right = node.right CASE 3: NODE WITH 2 CHILDREN Delete target (50) → Replace with In-Order Successor (60) 1. Locate Target & Successor 50 Target 30 80 60 90 In-Order Successor Swap & Delete 2. Successor Promoted 60 New Root 30 80 null 90 Vacated Leaf Slot 3-Step Successor Deletion Protocol: 1. Find in-order successor S = min(target.right). 2. Overwrite target node's key with S.key (50 → 60). 3. Recursively delete S from right subtree (reduces to Case 1 or 2). ✓ Time Complexity: O(h) | In-place tree re-linking

The Three Fundamental Node Deletion Cases

Deleting a node from a BST requires preserving the global ordering invariant. There are three distinct structural cases:

  1. Case 1 (Leaf Node — 0 Children): The target node has neither left nor right children. The parent pointer referencing this node is simply updated to null.
  2. Case 2 (Single Child — 1 Child): The target node has exactly one child (either left or right). The parent pointer is updated to bypass the target node and point directly to its child.
  3. Case 3 (Two Children — 2 Children): The target node has both left and right children. Deleting it directly would fracture the tree. Instead:
    • Locate the in-order successor (the minimum key in the right subtree: min(node.right)).
    • Copy the successor's key into the target node.
    • Recursively delete the successor from the right subtree (which is guaranteed to have at most 1 child, reducing to Case 1 or Case 2).

In-Order Traversal & Monotonic Sorting

When performing a Depth-First Search (DFS) in-order traversal (Left → Node → Right) on a valid BST, the retrieved sequence is mathematically guaranteed to be strictly ascending:

In-Order Traversal sequence: [30, 50, 60, 80, 90] (Strictly Sorted)

This property provides an immediate O(N) sorting mechanism and forms the basis for finding the $k^{\text{th}}$ smallest element in O(h + k) time.

Production-Grade Java Implementation

public class BinarySearchTree {
    public static class Node {
        public int key;
        public Node left;
        public Node right;

        public Node(int key) {
            this.key = key;
        }
    }

    private Node root;

    // --- 1. SEARCH: O(h) Iterative ---
    public boolean contains(int key) {
        Node curr = root;
        while (curr != null) {
            if (key == curr.key) return true;
            curr = (key < curr.key) ? curr.left : curr.right;
        }
        return false;
    }

    // --- 2. INSERTION: O(h) Recursive ---
    public void insert(int key) {
        root = insertRecursive(root, key);
    }

    private Node insertRecursive(Node node, int key) {
        if (node == null) return new Node(key);

        if (key < node.key) {
            node.left = insertRecursive(node.left, key);
        } else if (key > node.key) {
            node.right = insertRecursive(node.right, key);
        }
        return node; // Duplicate keys ignored
    }

    // --- 3. DELETION: O(h) Covering All 3 Cases ---
    public void delete(int key) {
        root = deleteRecursive(root, key);
    }

    private Node deleteRecursive(Node node, int key) {
        if (node == null) return null;

        if (key < node.key) {
            node.left = deleteRecursive(node.left, key);
        } else if (key > node.key) {
            node.right = deleteRecursive(node.right, key);
        } else {
            // Case 1 & Case 2: 0 or 1 child
            if (node.left == null) return node.right;
            if (node.right == null) return node.left;

            // Case 3: 2 children -> replace with in-order successor
            Node successor = findMin(node.right);
            node.key = successor.key;
            node.right = deleteRecursive(node.right, successor.key);
        }
        return node;
    }

    private Node findMin(Node node) {
        while (node.left != null) node = node.left;
        return node;
    }

    // --- 4. VALIDATION: Check if binary tree is valid BST ---
    public boolean isValidBST() {
        return validate(root, null, null);
    }

    private boolean validate(Node node, Integer minVal, Integer maxVal) {
        if (node == null) return true;

        if ((minVal != null && node.key <= minVal) || (maxVal != null && node.key >= maxVal)) {
            return false;
        }

        return validate(node.left, minVal, node.key) && validate(node.right, node.key, maxVal);
    }
}

Python Implementation

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

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

    def search(self, key: int) -> bool:
        curr = self.root
        while curr:
            if key == curr.key:
                return True
            curr = curr.left if key < curr.key else curr.right
        return False

    def insert(self, key: int):
        def _insert(node: TreeNode | None, k: int) -> TreeNode:
            if not node:
                return TreeNode(k)
            if k < node.key:
                node.left = _insert(node.left, k)
            elif k > node.key:
                node.right = _insert(node.right, k)
            return node

        self.root = _insert(self.root, key)

    def delete(self, key: int):
        def _delete(node: TreeNode | None, k: int) -> TreeNode | None:
            if not node:
                return None
            if k < node.key:
                node.left = _delete(node.left, k)
            elif k > node.key:
                node.right = _delete(node.right, k)
            else:
                # 0 or 1 child
                if not node.left:
                    return node.right
                if not node.right:
                    return node.left
                # 2 children: get successor
                successor = node.right
                while successor.left:
                    successor = successor.left
                node.key = successor.key
                node.right = _delete(node.right, successor.key)
            return node

        self.root = _delete(self.root, key)

    def is_valid_bst(self) -> bool:
        def _validate(node: TreeNode | None, min_val: float, max_val: float) -> bool:
            if not node:
                return True
            if not (min_val < node.key < max_val):
                return False
            return _validate(node.left, min_val, node.key) and _validate(node.right, node.key, max_val)

        return _validate(self.root, float("-inf"), float("inf"))

Asymptotic Complexity & The Degeneration Problem

Operation Average Case (Balanced) Worst Case (Degenerate Linked List)
Search O(log N) O(N) (Sorted input like [1, 2, 3, 4])
Insertion O(log N) O(N)
Deletion O(log N) O(N)
Min / Max Key O(log N) O(N)
In-Order Traversal O(N) O(N)
Auxiliary Space O(log N) recursion stack O(N) recursion stack

Architecture Rule: A standard unconstrained Binary Search Tree guarantees O(log N) performance only when keys are inserted in random order. For production systems subject to ordered input sequences, always use self-balancing trees (such as AVL Trees or Red-Black Trees) that enforce O(log N) worst-case bounds through automatic tree rotations.

Related Guides:
← PreviousAVL TreesNext →Binary Trees