Binary Search Trees

intermediate BST binary-search-tree data-structure

A Binary Search Tree (BST) is a binary tree with one extra rule: for every node, all values in its left subtree are smaller, and all values in its right subtree are larger. This one rule gives us the power of binary search on a tree structure.

Why does this matter? Because searching, inserting, and deleting all become O(log n) on average. That’s the same speed as binary search on a sorted array, but with the flexibility to insert and delete without shifting elements.

The BST Property

BST Property: Left < Node < Right
Valid BST
8
3
10
1
6
14
Inorder: 1, 3, 6, 8, 10, 14 (sorted!)
NOT a Valid BST
8
3
10
1
12
12 is in left subtree of 8, but 12 > 8!

The key insight: an inorder traversal of a BST gives us elements in sorted order. This is how many BST problems are solved.

Search in a BST

Searching is elegant. Compare the target with the current node. If it’s smaller, go left. If it’s larger, go right. If it matches, we found it.

function searchBST(node, target) {
  if (!node) return null;              // not found
  if (target === node.val) return node; // found it
  if (target < node.val)
    return searchBST(node.left, target);  // go left
  return searchBST(node.right, target);   // go right
}
def search_bst(node, target):
    if not node: return None              # not found
    if target == node.val: return node    # found it
    if target < node.val:
        return search_bst(node.left, target)  # go left
    return search_bst(node.right, target)     # go right
TreeNode searchBST(TreeNode node, int target) {
    if (node == null) return null;          // not found
    if (target == node.val) return node;    // found it
    if (target < node.val)
        return searchBST(node.left, target);  // go left
    return searchBST(node.right, target);     // go right
}

Time: O(h) where h is the height. For a balanced tree that’s O(log n). For a skewed tree, it degrades to O(n).

Insert into a BST

Inserting follows the same logic as search. We walk down the tree until we find an empty spot, then place the new node there.

function insertBST(node, val) {
  if (!node) return new TreeNode(val);  // found empty spot
  if (val < node.val)
    node.left = insertBST(node.left, val);   // go left
  else
    node.right = insertBST(node.right, val); // go right
  return node;
}
def insert_bst(node, val):
    if not node: return TreeNode(val)    # found empty spot
    if val < node.val:
        node.left = insert_bst(node.left, val)   # go left
    else:
        node.right = insert_bst(node.right, val) # go right
    return node
TreeNode insertBST(TreeNode node, int val) {
    if (node == null) return new TreeNode(val); // found empty spot
    if (val < node.val)
        node.left = insertBST(node.left, val);   // go left
    else
        node.right = insertBST(node.right, val); // go right
    return node;
}

Time: O(h) — same as search.

Delete from a BST

Deletion is the trickiest operation. There are three cases:

  1. Node is a leaf (no children) — just remove it.
  2. Node has one child — replace the node with its child.
  3. Node has two children — find the inorder successor (smallest node in right subtree), copy its value to the current node, then delete the successor.
function deleteBST(node, val) {
  if (!node) return null;
  if (val < node.val) node.left = deleteBST(node.left, val);
  else if (val > node.val) node.right = deleteBST(node.right, val);
  else {
    if (!node.left) return node.right;  // case 1 & 2
    if (!node.right) return node.left;  // case 2
    // case 3: find inorder successor
    let succ = node.right;
    while (succ.left) succ = succ.left;
    node.val = succ.val;
    node.right = deleteBST(node.right, succ.val);
  }
  return node;
}
def delete_bst(node, val):
    if not node: return None
    if val < node.val: node.left = delete_bst(node.left, val)
    elif val > node.val: node.right = delete_bst(node.right, val)
    else:
        if not node.left: return node.right   # case 1 & 2
        if not node.right: return node.left   # case 2
        # case 3: find inorder successor
        succ = node.right
        while succ.left: succ = succ.left
        node.val = succ.val
        node.right = delete_bst(node.right, succ.val)
    return node
TreeNode deleteBST(TreeNode node, int val) {
    if (node == null) return null;
    if (val < node.val) node.left = deleteBST(node.left, val);
    else if (val > node.val) node.right = deleteBST(node.right, val);
    else {
        if (node.left == null) return node.right;  // case 1 & 2
        if (node.right == null) return node.left;  // case 2
        // case 3: find inorder successor
        TreeNode succ = node.right;
        while (succ.left != null) succ = succ.left;
        node.val = succ.val;
        node.right = deleteBST(node.right, succ.val);
    }
    return node;
}

Validate a BST

A common interview problem. The trick is that we can’t just check left < node < right locally — we need to track the valid range for each node.

For example, in the invalid BST above, 12 is correctly to the right of 3, but it violates the rule that everything in the left subtree of 8 must be less than 8.

function isValidBST(node, min = -Infinity, max = Infinity) {
  if (!node) return true;
  if (node.val <= min || node.val >= max) return false;
  return isValidBST(node.left, min, node.val)    // left must be < node
      && isValidBST(node.right, node.val, max);  // right must be > node
}
def is_valid_bst(node, lo=float('-inf'), hi=float('inf')):
    if not node: return True
    if node.val <= lo or node.val >= hi: return False
    return (is_valid_bst(node.left, lo, node.val)    # left must be < node
        and is_valid_bst(node.right, node.val, hi))  # right must be > node
boolean isValidBST(TreeNode node, long min, long max) {
    if (node == null) return true;
    if (node.val <= min || node.val >= max) return false;
    return isValidBST(node.left, min, node.val)    // left must be < node
        && isValidBST(node.right, node.val, max);  // right must be > node
}
// Call: isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE)

Time: O(n) — we check every node. Space: O(h).

Kth Smallest Element

Since inorder traversal gives us sorted order, the kth smallest is just the kth element in an inorder traversal. We can stop early once we’ve counted k nodes.

function kthSmallest(root, k) {
  let count = 0, result = null;
  function inorder(node) {
    if (!node || result !== null) return;
    inorder(node.left);
    count++;
    if (count === k) { result = node.val; return; }
    inorder(node.right);
  }
  inorder(root);
  return result;
}
def kth_smallest(root, k):
    count, result = [0], [None]  # use lists to mutate in closure
    def inorder(node):
        if not node or result[0] is not None: return
        inorder(node.left)
        count[0] += 1
        if count[0] == k:
            result[0] = node.val
            return
        inorder(node.right)
    inorder(root)
    return result[0]
int kthSmallest(TreeNode root, int k) {
    int[] state = {k, 0}; // [remaining count, result]
    inorderK(root, state);
    return state[1];
}
void inorderK(TreeNode node, int[] state) {
    if (node == null) return;
    inorderK(node.left, state);
    state[0]--;
    if (state[0] == 0) { state[1] = node.val; return; }
    inorderK(node.right, state);
}

Time: O(h + k) — we go down to the leftmost node then visit k nodes. Space: O(h).

BST Complexity Summary

OperationAverageWorst (Skewed)
SearchO(log n)O(n)
InsertO(log n)O(n)
DeleteO(log n)O(n)

The worst case happens when the tree becomes a straight line (like inserting sorted data). Self-balancing trees (AVL, Red-Black) fix this by ensuring the height stays O(log n), but those are rarely asked in interviews.

Key Takeaways

  • The BST property (left < node < right) gives us O(log n) operations on average.
  • Inorder traversal of a BST = sorted order. This is the most important thing to remember.
  • Deletion has three cases. The two-children case uses the inorder successor.
  • Validating a BST requires tracking min/max bounds, not just comparing parent and child.

In simple language, a BST is like a sorted array that we can insert into and delete from efficiently. The tradeoff is that it can become unbalanced, but for interview purposes, we usually assume reasonable balance.