Intermediate: core algorithms · Trees and binary search trees · topic 34 of 61 · 40 min
Binary search trees
Explanation
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.
50
/ \
30 70
/ \ / \
20 40 60 80Search 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.
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 NoneThe 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
- The node is a leaf: just remove it (return None to its parent).
- The node has one child: replace the node with that child.
- 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.
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] 60Validating 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.
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: FalseTwo 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
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).
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 4000Practice (0/4 done)
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].
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.
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.
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Lowest common ancestor, serialisation and balanced treesTrees and binary search trees · same module
- Binary search treesC Programming (from zero to systems) · Data structures in C
- Arrays: traversal and in-place updatesArrays and strings
- Binary searchSorting and searching
- Big-O and searchingPython Programming · Algorithms, data structures and testing
- Tries (prefix trees)Tries, union-find and bit manipulation
Questions people ask about this
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
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.
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).
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 more topic in “Trees and binary search trees” before its exam: Lowest common ancestor, serialisation and balanced trees.
- Module 7 exam: Trees and binary search trees · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Heaps and greedy algorithms. 61 topics left in the course before the final exam.