Binary Search Trees
Quick reference
| search(root, target) | O(h) |
| insert(root, value) | O(h) |
| delete(root, value) | O(h) |
| findMin / findMax | O(h) |
| Inorder traversal | O(n) |
A binary search tree is a binary tree with exactly one extra rule bolted on: for every node, everything in its left subtree is smaller than it, and everything in its right subtree is bigger. Same Node shape as last topic, same left/right pointers β the only thing that changed is a promise about what those pointers mean.
That one rule turns the tree into a guessing game. Picture the number-guessing game where you pick a secret number and every guess gets answered 'higher' or 'lower' β never the exact distance, just a direction. Each answer throws away half of whatever range was still possible. A BST plays that exact game with real data: standing at any node, comparing your target to node.value tells you which entire subtree to search next, and lets you ignore the other one completely, without looking at a single value inside it.
That's the halving idea, and it's new here β nothing earlier in this course relied on it. A plain binary tree from the last topic has no such rule, so finding a value means checking nodes with no way to rule anything out in advance; you might have to look at all of them. A BST's ordering promise is what makes 'ignore half the tree' a safe move instead of a guess.
The node β same shape, new promise
structure Node:
value
left // -> Node, and every value in this subtree is < node.value
right // -> Node, and every value in this subtree is > node.value
// the ordering rule holds at every node, not just between direct parent and childsearch β compare, then go left or right
function search(node, target):
if node is NULL: // base case β ran out of tree, not found
return NULL
if target == node.value:
return node
if target < node.value:
return search(node.left, target) // rule out the entire right subtree
return search(node.right, target) // rule out the entire left subtreeinsert β navigate to the first empty spot, and place it there
function insert(node, value):
if node is NULL: // found an empty spot β this is where value belongs
return Node(value)
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 is already in the treedelete β three cases, by how many children the target has
function delete(node, value):
if node is NULL:
return NULL // value isn't in the tree β nothing to do
if value < node.value:
node.left = delete(node.left, value)
else if value > node.value:
node.right = delete(node.right, value)
else:
// this is the node to remove
if node.left is NULL:
return node.right // leaf (both NULL) or one right child β splice it up
if node.right is NULL:
return node.left // one left child β splice it up
// two children: steal the in-order successor's value, then delete the successor
successor = findMin(node.right) // smallest value in the right subtree
node.value = successor.value
node.right = delete(node.right, successor.value)
return nodefindMin / findMax β leftmost and rightmost node
function findMin(node):
while node.left is not NULL:
node = node.left
return node
function findMax(node):
while node.right is not NULL:
node = node.right
return nodevalidate β check the ordering rule with an inherited (min, max) range
function validate(node, min = -Infinity, max = +Infinity):
if node is NULL:
return true // an empty subtree can't violate anything
if node.value <= min or node.value >= max:
return false
return validate(node.left, min, node.value) and validate(node.right, node.value, max)search, insert, and delete all walk one path from root to some node, so all three cost O(h) time and O(h) recursion-stack space, where h is the tree's height β O(log n) if the tree is balanced, O(n) worst case if it's skewed (direct callback to Binary Trees' skewed-tree edge case). findMin/findMax walk the same kind of single path but iteratively, so O(h) time and O(1) space β no stack needed. Inorder traversal, inherited unchanged from Binary Trees, gets a new property here: on a BST it visits every value in sorted order, for free, still O(n).
| Operation | Time | Space | Why |
|---|---|---|---|
| search(root, target) | O(h) | O(h) | One comparison per level, one subtree discarded each time β O(log n) if balanced, O(n) if the tree has degenerated into a chain. |
| insert(root, value) | O(h) | O(h) | Same navigation as search, continued one step further to the first empty spot. |
| delete(root, value) | O(h) | O(h) | Locating the node is O(h); the two-children case adds one more O(h) walk to findMin and one more O(h) delete of the successor β still O(h) total, not O(hΒ²), since the successor search happens inside the right subtree's own height. |
| findMin / findMax | O(h) | O(1) | A plain iterative walk down one side of the tree β no recursion, so no stack frames pile up. |
| Inorder traversal | O(n) | O(h) | Same traversal as Binary Trees, every node visited once β but the BST's ordering rule means left-root-right now visits every value from smallest to largest, with no sorting step required. |
Inserting already-sorted data degenerates into a linked list
Inserting 1, 2, 3, 4, 5 in that order: 1 becomes the root, 2 is bigger so it becomes 1's right child, 3 is bigger than both so it goes further right, and so on β every node ends up with only a right child, the exact skewed shape Binary Trees warned about. search/insert/delete all degrade from the expected O(log n) to O(n), because h has grown to equal n. The BST gives you no protection against this on its own β keeping the tree balanced under sorted insertions needs extra machinery this topic doesn't cover.
Deleting a two-children node without finding the true successor
The replacement for a deleted two-children node has to be the smallest value in its right subtree specifically β not just "some" nearby value, and not the right child itself unless the right child happens to have no left child of its own. Grab the wrong node (say, the right child directly, skipping past its left subtree) and the ordering rule breaks silently: a later search for a value that's still technically in the tree can walk the wrong direction and report it missing.
Validating against only the immediate parent or children
A node can be bigger than its immediate left child and smaller than its immediate right child and still break the tree's ordering rule against an ancestor further up. Concretely: a right child's left grandchild must still be smaller than the original root, not just smaller than its own parent β a purely local, parent-to-child comparison misses violations like that entirely. The (min, max) bound from Build It's validate() is what catches it, because the bound accumulates from every ancestor, not just the nearest one.
Duplicate values with no consistent left-or-right rule
search/insert/delete all rely on strict less-than and greater-than to decide which single direction to go. If duplicates are allowed, insert has to pick one consistent side for equal values β always left, or always right β and every other operation must agree with that same choice. Pick inconsistently (some code paths send equal values left, others right) and search can walk past a value that's actually sitting in the tree, because it looked on the side nothing agreed to put it on.
Sign in to mark problems done β progress syncs across devices.