DSA Binary Search Trees

A binary search tree keeps every left descendant smaller than its parent and every right descendant larger, an ordering invariant that makes search, insert, and delete all run in O(log n) time on a balanced tree.

The Binary Search Tree Invariant

A binary search tree (BST) is a binary tree that maintains one ordering rule at every node: every value in a node's left subtree must be smaller than the node's value, and every value in its right subtree must be larger. This rule holds recursively for every node in both subtrees, all the way down - not just for a node's immediate children. It's this invariant, not any particular shape, that makes a tree a BST.

The payoff is a fast lookup structure: at any node, comparing the target value against the node's value tells you which single subtree could possibly contain it, so the other subtree can be discarded entirely. On a reasonably balanced tree, this halves the remaining search space at every step, giving O(log n) search, insert, and delete - matching binary search on a sorted array for lookups, while still supporting efficient insertion and deletion that a plain array can't.

Searching and Inserting

Example: BST Search and Insert

class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None


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

    def insert(self, value):
        self.root = self._insert(self.root, value)

    def _insert(self, node, value):
        if node is None:
            return Node(value)
        if value < node.value:
            node.left = self._insert(node.left, value)
        elif value > node.value:
            node.right = self._insert(node.right, value)
        # equal values are ignored - no duplicates
        return node

    def search(self, value):
        return self._search(self.root, value)

    def _search(self, node, value):
        if node is None:
            return False
        if value == node.value:
            return True
        if value < node.value:
            return self._search(node.left, value)
        return self._search(node.right, value)


tree = BST()
for n in [8, 3, 10, 1, 6, 14, 4, 7, 13]:
    tree.insert(n)

print(tree.search(6))    # True
print(tree.search(11))   # False

Deleting a Node

Deletion is the trickiest BST operation because removing a node must not break the ordering invariant. There are three cases: deleting a leaf just removes it; deleting a node with a single child splices that child into the parent's place; and deleting a node with two children finds the in-order successor - the smallest value in the right subtree - copies that value into the node being deleted, and then deletes the successor instead, which is guaranteed to have at most one child.

Example: BST Delete

class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None


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

    def insert(self, value):
        self.root = self._insert(self.root, value)

    def _insert(self, node, value):
        if node is None:
            return Node(value)
        if value < node.value:
            node.left = self._insert(node.left, value)
        elif value > node.value:
            node.right = self._insert(node.right, value)
        return node

    def delete(self, value):
        self.root = self._delete(self.root, value)

    def _delete(self, node, value):
        if node is None:
            return None
        if value < node.value:
            node.left = self._delete(node.left, value)
        elif value > node.value:
            node.right = self._delete(node.right, value)
        else:
            # Found the node to delete.
            if node.left is None:      # 0 or 1 child (right only)
                return node.right
            if node.right is None:     # 1 child (left only)
                return node.left
            # 2 children: replace with the in-order successor
            successor = node.right
            while successor.left:
                successor = successor.left
            node.value = successor.value
            node.right = self._delete(node.right, successor.value)
        return node

    def inorder(self):
        result = []
        self._inorder(self.root, result)
        return result

    def _inorder(self, node, result):
        if node:
            self._inorder(node.left, result)
            result.append(node.value)
            self._inorder(node.right, result)


tree = BST()
for n in [8, 3, 10, 1, 6, 14, 4, 7, 13]:
    tree.insert(n)

tree.delete(3)            # two children -> replaced by successor 4
print(tree.inorder())
# [1, 4, 6, 7, 8, 10, 13, 14]

Example: Insertion Order Doesn't Affect Sorted Output

class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None


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

    def insert(self, value):
        self.root = self._insert(self.root, value)

    def _insert(self, node, value):
        if node is None:
            return Node(value)
        if value < node.value:
            node.left = self._insert(node.left, value)
        elif value > node.value:
            node.right = self._insert(node.right, value)
        return node

    def inorder(self):
        result = []
        self._inorder(self.root, result)
        return result

    def _inorder(self, node, result):
        if node:
            self._inorder(node.left, result)
            result.append(node.value)
            self._inorder(node.right, result)


import random

values = list(range(1, 21))
random.shuffle(values)      # insertion order shouldn't matter

tree = BST()
for v in values:
    tree.insert(v)

sorted_output = tree.inorder()
print(sorted_output)
print(sorted_output == sorted(values))   # True, regardless of insertion order
  • Left subtree values are always smaller than the node, right subtree values always larger
  • Search, insert, and delete all follow a single path from the root toward a target position
  • Average time complexity is O(log n) on a balanced tree, but O(n) in the worst case
  • In-order traversal of a BST always yields values in sorted order
  • Deleting a node with two children means replacing it with its in-order successor (or predecessor)
OperationAverage CaseWorst Case
SearchO(log n)O(n)
InsertO(log n)O(n)
DeleteO(log n)O(n)
Note: A BST built by inserting already-sorted data degenerates into a straight line - effectively a linked list - which pushes every operation to O(n). Self-balancing variants like AVL trees exist specifically to prevent this.

Exercise: DSA Binary Search Trees

In a binary search tree, how do values in a node's left subtree compare to that node?