Loading content...
You now understand the three cases of BST deletion and the role of inorder successors and predecessors. But understanding the theory and writing correct code are different challenges.
In this page, we'll build a complete, production-ready BST deletion implementation. More importantly, we'll develop the invariant-thinking mindset that ensures every step of your implementation preserves the BST property.
The goal isn't just code that works—it's code that is provably correct because it maintains the BST invariant at every operation.
BST deletion bugs are often latent. A broken implementation might appear to work for small trees or specific sequences, then fail catastrophically when:
• The tree grows larger • Certain deletion patterns emerge • Edge cases (root deletion, specific child configurations) occur
The only protection is rigorous invariant preservation—ensuring every step maintains left < node < right for ALL nodes.
Before diving into code, let's establish the complete algorithmic flow. BST deletion consists of two phases:
Phase 1: Find the node to delete
Phase 2: Remove the node based on its case
Pseudocode Overview:
DELETE(tree, value):
1. node, parent, isLeftChild = FIND_WITH_PARENT(tree.root, value)
2. if node is null:
return false // value not found
3. if node has two children:
// Replace with inorder successor
successor, succParent = FIND_SUCCESSOR_WITH_PARENT(node)
node.value = successor.value
// Now delete successor (which has at most 1 child)
node = successor
parent = succParent
isLeftChild = (succParent.left == successor)
4. // Now node has 0 or 1 child
child = node.left OR node.right OR null
5. if parent is null:
tree.root = child // Deleting root
else if isLeftChild:
parent.left = child
else:
parent.right = child
6. return true
Notice how two-child deletion reduces to the simpler cases by first replacing the value, then handling the successor as a one-child or leaf case.
This algorithm demonstrates a powerful pattern: reducing complex cases to simpler ones.
We don't have THREE different deletion implementations—we have ONE algorithm where the complex case (two children) transforms into a simpler case (zero or one child) by cleverly repositioning what we're actually deleting.
Here's a complete, thoroughly-commented implementation of BST deletion. We'll use the inorder successor approach for two-child deletion.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
interface TreeNode { value: number; left: TreeNode | null; right: TreeNode | null;} class BinarySearchTree { root: TreeNode | null = null; /** * Deletes a value from the BST. * Returns true if the value was found and deleted, false otherwise. * * INVARIANT: After deletion, the BST property (left < node < right) * is preserved for ALL nodes in the tree. */ delete(value: number): boolean { // Phase 1: Find the node and its parent const searchResult = this.findWithParent(value); if (searchResult === null) { return false; // Value not in tree } let { node, parent, isLeftChild } = searchResult; // Phase 2: Handle deletion based on case // CASE 3: Node has two children // Strategy: Replace with inorder successor, then delete successor if (node.left !== null && node.right !== null) { // Find inorder successor (leftmost in right subtree) const { successor, successorParent } = this.findSuccessorWithParent(node); // Copy successor's value to the node we're "deleting" // This preserves the BST property because: // - successor.value > all values in node's left subtree (proven earlier) // - successor.value < all values in node's right subtree except itself node.value = successor.value; // Now we need to delete the successor from its original position // The successor has at most one child (no left child by definition) node = successor; parent = successorParent; isLeftChild = (successorParent.left === successor); } // At this point, node has 0 or 1 child (CASE 1 or CASE 2) // We handle both uniformly: child is either the one child or null const child: TreeNode | null = (node.left !== null) ? node.left : node.right; // Reconnect: parent should now point to child instead of node if (parent === null) { // We're deleting the root this.root = child; } else if (isLeftChild) { parent.left = child; } else { parent.right = child; } // Node is now disconnected; garbage collection will reclaim it return true; } /** * Finds a node and its parent. * Returns null if value not found. */ private findWithParent(value: number): { node: TreeNode; parent: TreeNode | null; isLeftChild: boolean; } | null { let parent: TreeNode | null = null; let current: TreeNode | null = this.root; let isLeftChild = false; while (current !== null) { if (value === current.value) { return { node: current, parent, isLeftChild }; } parent = current; if (value < current.value) { current = current.left; isLeftChild = true; } else { current = current.right; isLeftChild = false; } } return null; // Not found } /** * Finds the inorder successor of a node (assuming it has a right child) * and returns both the successor and its parent. */ private findSuccessorWithParent(node: TreeNode): { successor: TreeNode; successorParent: TreeNode; } { // The successor is the leftmost node in the right subtree let successorParent: TreeNode = node; let successor: TreeNode = node.right!; // Guaranteed to exist in two-child case while (successor.left !== null) { successorParent = successor; successor = successor.left; } return { successor, successorParent }; } /** * Standard BST insertion for building test trees. */ insert(value: number): void { const newNode: TreeNode = { value, left: null, right: null }; if (this.root === null) { this.root = newNode; return; } let current = this.root; while (true) { if (value < current.value) { if (current.left === null) { current.left = newNode; return; } current = current.left; } else { if (current.right === null) { current.right = newNode; return; } current = current.right; } } } /** * Inorder traversal - should always produce sorted output if BST property holds. */ inorderTraversal(): number[] { const result: number[] = []; this.inorderHelper(this.root, result); return result; } private inorderHelper(node: TreeNode | null, result: number[]): void { if (node === null) return; this.inorderHelper(node.left, result); result.push(node.value); this.inorderHelper(node.right, result); }}Let's trace through a two-child deletion step by step to see how the algorithm preserves the BST property.
Initial Tree: Delete node 50 from this BST:
Step 1: Identify the case
Node 50 has two children (30 and 70). This is Case 3.
Step 2: Find the inorder successor
Go right to 70, then left as far as possible:
Successor = 55 (leftmost in right subtree)
Step 3: Copy successor's value
Replace 50 with 55. The node that held 50 now holds 55:
Step 4: Delete the successor from its original position
Node 55 (original position) is a leaf node with no children.
This is Case 1! We simply disconnect it from its parent (60).
Final Tree:
Let's verify the BST property at the modified positions:
At node 55 (formerly 50): • Left subtree: {20, 30, 35, 40, 45} — all < 55 ✓ • Right subtree: {60, 70, 85} — all > 55 ✓
At node 60: • Left subtree: empty (was 55, now deleted) ✓ • Right subtree: none ✓
Inorder traversal before: 20, 30, 35, 40, 45, 50, 55, 60, 70, 85 Inorder traversal after: 20, 30, 35, 40, 45, 55, 60, 70, 85
The tree remains a valid BST!
Robust implementations must handle edge cases that break naive approaches. Here are the critical scenarios:
Example: Successor is immediate right child
Here, 15 is both the right child AND the successor (no left subtree from 15).
After deletion:
How do we know our implementation is correct? We use invariant verification—checking that the BST property holds after every operation.
The BST Invariant: For every node N:
Verification Approaches:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
interface TreeNode { value: number; left: TreeNode | null; right: TreeNode | null;} /** * Verifies that the BST property holds for the entire tree. * Uses the range-based approach: track valid [min, max] for each node. */function isValidBST(root: TreeNode | null): boolean { return isValidBSTHelper(root, -Infinity, Infinity);} function isValidBSTHelper( node: TreeNode | null, minValue: number, maxValue: number): boolean { // Empty tree is valid if (node === null) return true; // Node must be within valid range if (node.value <= minValue || node.value >= maxValue) { return false; } // Left subtree: all values must be < node.value // Right subtree: all values must be > node.value return ( isValidBSTHelper(node.left, minValue, node.value) && isValidBSTHelper(node.right, node.value, maxValue) );} /** * Alternative verification: inorder traversal should be strictly increasing. */function isValidBSTByInorder(root: TreeNode | null): boolean { const values: number[] = []; inorderCollect(root, values); for (let i = 1; i < values.length; i++) { if (values[i] <= values[i - 1]) { return false; } } return true;} function inorderCollect(node: TreeNode | null, result: number[]): void { if (node === null) return; inorderCollect(node.left, result); result.push(node.value); inorderCollect(node.right, result);} /** * Example usage in tests: */function testDeletion(): void { const bst = new BinarySearchTree(); [50, 30, 70, 20, 40, 60, 85, 35, 45, 55].forEach(v => bst.insert(v)); console.log("Before deletion:", bst.inorderTraversal()); console.log("Is valid BST:", isValidBST(bst.root)); bst.delete(50); console.log("After deleting 50:", bst.inorderTraversal()); console.log("Is valid BST:", isValidBST(bst.root)); // Expected output: // Before deletion: [20, 30, 35, 40, 45, 50, 55, 60, 70, 85] // Is valid BST: true // After deleting 50: [20, 30, 35, 40, 45, 55, 60, 70, 85] // Is valid BST: true}For robust deletion testing:
BST deletion is notorious for subtle bugs. Here are the most common mistakes and how to avoid them:
When deletion seems broken, print the inorder traversal before and after. If the output isn't sorted (excluding the deleted value), the BST property has been violated. Then trace backwards to find where the invariant broke.
In the next and final page, we'll analyze the time complexity of BST deletion in depth. You'll understand why deletion is O(h), how tree shape affects performance, and when BST deletion becomes problematic—setting the stage for self-balancing trees.