Skip to content

Intermediate: core algorithms · Trees and binary search trees · topic 34 of 61 · 40 min

Binary search trees

Explanation

A binary search tree (BST) keeps every value in a node's left subtree smaller than the node and every value in its right subtree larger. That rule lets search, insert and delete walk one path, O(h). An inorder traversal of a BST gives the values in sorted order, which is the key to validating a BST, finding the k-th smallest value and building a balanced BST from a sorted array.

Why a BST

A sorted array gives fast search with binary search, O(log n), but inserting a new value means shifting items, O(n). A hash set gives fast insert and lookup, but no order: it cannot answer "what is the next larger value?" or "list everything between 100 and 200". A binary search tree aims for both: ordered data with search, insert and delete in O(h), where h is the height.

The BST property

For EVERY node: all values in its left subtree are smaller than the node's value, and all values in its right subtree are larger. Note the words "all values in the subtree", not just "the children". In this module a BST has no duplicate values. Real systems either forbid duplicates, keep a count in the node, or send equal values consistently to one side; say which one you choose.

output
        50
       /  \
     30    70
    /  \   /  \
   20  40 60  80
A valid BST. Every value left of 50 is smaller than 50; every value right of it is larger. The same holds at 30 and 70.

Search and insert: walk one path

To search, start at the root. If the target is smaller, go left; if larger, go right; if equal, you found it. Each step goes one level down, so search is O(h). Insert walks the same path until it falls off the tree at a None, and puts the new node there.

python
def search(node, target):
    while node is not None and node.val != target:
        node = node.left if target < node.val else node.right
    return node

def insert(node, val):
    if node is None:
        return TreeNode(val)
    if val < node.val:
        node.left = insert(node.left, val)
    elif val > node.val:
        node.right = insert(node.right, val)
    return node                    # equal: ignore the duplicate

def inorder(node):
    return inorder(node.left) + [node.val] + inorder(node.right) if node else []

root = None
for v in [50, 30, 70, 20, 40, 60, 80]:
    root = insert(root, v)
print(inorder(root))                                # Output: [20, 30, 40, 50, 60, 70, 80]
print(search(root, 60) is not None, search(root, 65))   # Output: True None

The shape depends on the insert order. Inserting 1, 2, 3, 4, 5 in sorted order gives a chain leaning right, with height n − 1, so every operation becomes O(n). Self-balancing trees (Topic 5) fix this by reshaping the tree after inserts and deletes.

Inorder gives sorted order

Inorder visits the left subtree (all smaller values), then the node, then the right subtree (all larger values). Applied at every node, that prints the values in increasing order. This one fact solves many problems: validating a BST (the inorder list must be strictly increasing), finding the k-th smallest value (stop after k visits), and converting a BST to a sorted list in O(n).

Delete: three cases

  1. The node is a leaf: just remove it (return None to its parent).
  2. The node has one child: replace the node with that child.
  3. The node has two children: find its inorder successor, the smallest value in its right subtree (go right once, then left as far as possible). Copy that value into the node, then delete the successor from the right subtree. The successor has no left child, so that second delete is case 1 or 2.
python
def delete(node, key):
    if node is None:
        return None
    if key < node.val:
        node.left = delete(node.left, key)
    elif key > node.val:
        node.right = delete(node.right, key)
    else:
        if node.left is None:          # cases 1 and 2
            return node.right
        if node.right is None:         # case 2
            return node.left
        succ = node.right              # case 3: two children
        while succ.left:
            succ = succ.left
        node.val = succ.val
        node.right = delete(node.right, succ.val)
    return node

for key in (20, 30, 50):               # a leaf, one child, two children
    root = delete(root, key)
print(inorder(root), root.val)         # Output: [40, 60, 70, 80] 60
Deleting 50 copies its successor 60 into the root and removes the old 60 node.

Validating a BST: the classic trap

The common wrong answer checks only that node.left.val < node.val < node.right.val at each node. Look at build([5, 4, 6, None, None, 3, 7]): 3 is the left child of 6, so the local check passes, but 3 sits in the RIGHT subtree of 5 and is smaller than 5. Not a BST.

The fix is to pass down the range each node must fit in. The root may be anything (−∞, +∞). Going left, the upper bound becomes the parent's value; going right, the lower bound becomes the parent's value. Each node is checked once: O(n) time, O(h) space. A second correct method is an inorder walk that checks each value is larger than the previous one.

python
def is_valid_bst(node, low=float("-inf"), high=float("inf")):
    if node is None:
        return True
    if not (low < node.val < high):
        return False
    return is_valid_bst(node.left, low, node.val) and is_valid_bst(node.right, node.val, high)

print(is_valid_bst(build([5, 4, 6, None, None, 3, 7])))   # Output: False

Two more classics

K-th smallest: run an iterative inorder and count; stop at the k-th popped node. It costs O(h + k), not O(n), because you stop early. Building a balanced BST from a sorted array: make the middle value the root, build the left half into the left subtree and the right half into the right subtree. Both halves differ in size by at most one, so the tree is height-balanced. It visits each value once: O(n).

Common mistakes

  • Validating with children only. Fix: pass (low, high) bounds down, or check that the inorder list is strictly increasing.
  • Using 0 or a big number as the starting bounds; values can be negative or huge. Fix: float("-inf") and float("inf"), or None meaning "no bound".
  • Forgetting to return the (possibly new) root from insert and delete, so the parent's link is never updated.
  • Assuming O(log n). A plain BST built from sorted input is a chain, O(n) per operation. Say "O(h), which is O(log n) only if balanced".
  • Choosing the wrong replacement in delete. Use the inorder successor (smallest on the right) or the predecessor (largest on the left), never an arbitrary child.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Binary search trees · 1/8

Binary search trees

This lesson is about binary search trees: trees kept in order so that search is fast.

Prefer a video by a teacher? Find free videos on “binary search tree insert delete validate bst kth smallest interview” on YouTube ↗

In real life: Ordered lookups: prices, dates and roll numbers

Some questions need ORDER, not just membership: "the cheapest flight above ₹4,000", "all bookings between 1 and 15 March", "the next free roll number". Systems that must answer these while data keeps changing often use a sorted map built on a balanced search tree, such as Java's TreeMap. In Python, the standard library has no tree map, but for mostly-read data a sorted list with bisect answers the same range questions in O(log n).

python
from bisect import bisect_left, bisect_right

fares = sorted([5200, 3900, 4100, 6100, 4500, 3999])
lo, hi = bisect_left(fares, 4000), bisect_right(fares, 5000)
print(fares[lo:hi])               # fares from 4000 to 5000
print(fares[bisect_right(fares, 4000)])   # cheapest fare above 4000
Shows [4100, 4500] and then 4100.

Practice (0/4 done)

Task 1 · Code · Python

Write two functions. bst_insert(root, val) inserts val into the BST and returns the root (a new TreeNode when root is None); if val is already in the tree, change nothing. inorder(root) returns the list of values in inorder. Example: inserting 50, 30, 70, 20, 40, 60, 80 one by one into an empty tree and calling inorder gives [20, 30, 40, 50, 60, 70, 80].

Task 2 · Code · Python

Write is_valid_bst(root) that returns True if the tree is a valid BST: for every node, ALL values in its left subtree are strictly smaller and ALL values in its right subtree are strictly larger (no duplicates allowed). An empty tree is valid. Example: is_valid_bst(build([5, 4, 6, None, None, 3, 7])) returns False, because 3 is in the right subtree of 5.

Task 3 · Code · Python

Write kth_smallest(root, k) that returns the k-th smallest value in the BST (k is 1-based and 1 ≤ k ≤ number of nodes). Use an iterative inorder walk and stop as soon as you reach the k-th value. Example: for build([5, 3, 6, 2, 4, None, None, 1]), kth_smallest(root, 3) returns 3.

Task 4 · Code · Python

Write sorted_array_to_bst(nums) where nums is sorted in strictly increasing order. Return the root of a HEIGHT-BALANCED BST that holds exactly these values (at every node the subtree heights differ by at most 1). Return None for []. Any balanced answer is accepted; the tests check the inorder order and the balance. Hint: the middle value becomes the root.

Quick quiz

Question 1

1.Which traversal of a BST lists its values in increasing order?

Question 2

2.You insert 10, 20, 30, 40, 50 in that order into an empty BST. What is its height in edges?

Question 3

3.Why is checking only node.left.val < node.val < node.right.val not enough to validate a BST?

Question 4

4.Deleting a node with two children: which value usually replaces it?

Question 5

5.What is the time of finding the k-th smallest value with an iterative inorder that stops early?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which traversal of a BST lists its values in increasing order?

Answer: Inorder

Left subtree (smaller), node, right subtree (larger), applied at every node, gives sorted order.

You insert 10, 20, 30, 40, 50 in that order into an empty BST. What is its height in edges?

Answer: 4

Each value is larger than all before it, so it goes right every time: a chain of 5 nodes with 4 edges.

Why is checking only node.left.val < node.val < node.right.val not enough to validate a BST?

Answer: A value deep in a subtree can break the rule with an ancestor, not its parent

In build([5, 4, 6, None, None, 3, 7]), 3 is fine next to 6 but is smaller than 5 while in 5's right subtree.

Deleting a node with two children: which value usually replaces it?

Answer: The smallest value in its right subtree (the inorder successor)

The successor is larger than everything on the left and smaller than everything else on the right, so the BST rule still holds.

What is the time of finding the k-th smallest value with an iterative inorder that stops early?

Answer: O(h + k)

You go down about h nodes to reach the smallest, then pop k nodes before stopping.

In a BST, where are keys smaller than a node's key?Related

Answer: In its left subtree

That is the BST property.

From Binary search trees

The tree is build([1, 2, 3, None, 4]). What is its inorder traversal?Related

Answer: [2, 4, 1, 3]

Node 2 has only a right child 4. Inorder: left of 2 (nothing), 2, then 4, then root 1, then 3.

From Traversals: DFS and BFS

In a BST, p = 3 and q = 9, and the current node is 6. What is the LCA?Related

Answer: 6

One value is smaller and one is larger, so they split here: 6 is the lowest node with both below it.

From Lowest common ancestor, serialisation and balanced trees

What is the extra space of a recursive tree function on a balanced tree of n nodes?Related

Answer: O(log n)

The recursion stack holds one path from the root, and a balanced tree has height O(log n).

From Recursive tree patterns

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Trees and binary search trees” before its exam: Lowest common ancestor, serialisation and balanced trees.
  2. Module 7 exam: Trees and binary search trees · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Heaps and greedy algorithms. 61 topics left in the course before the final exam.

← Previous: Recursive tree patterns