Binary Search Tree Insert and Search: One Comparison per Level
7 min readBytePatterns
Binary search tree insert and search, explained: one comparison per level, why both walk the same path, why height is the real cost, and floor and ceiling.
A binary search tree keeps one promise: everything in a node's left subtree is smaller than the node, and everything in its right subtree is larger. Search and insert both follow from that promise, and they turn out to be the same walk. Interviewers use the pair as the first real tree question, because the code is short and the follow-ups are not: why the cost is the height and not log n, what sorted input does, and how to find the closest value when the exact one is missing.
The problem it solves
A sorted array answers "is 45 here?" in O(log n), but each insert shifts everything after the insertion point, O(n). A hash set is constant time on average, but has no order: it cannot give the largest value below 45, or list keys in order.
A BST sits in between. It keeps keys ordered like the sorted array, but a new key is linked in rather than shifted into place. Search, insert, minimum, maximum, floor, ceiling and in-order listing all come from one structure.
The intuition
At each node, one comparison decides everything. If the target is smaller, it can only be in the left subtree, so the whole right subtree is discarded; if larger, the left side goes. It is binary search on a linked structure: each step throws away one side.
Search repeats that step until it finds the value or falls off the tree into an empty slot. Falling off means the value is absent.
Insert is the same walk. It goes left or right exactly as a search for the new value would, and at the empty slot where that search would have failed, it hangs the new node. That is why insert and search always agree: a value is found later by the path that placed it.
The cost of both is one step per level, so it is O(h), the height of the tree. The height is where the promise can go wrong. Keys arriving in random order give a bushy tree whose height grows like log n. Keys arriving sorted all go right, one after another, and the tree becomes a linked list of height n. The BST property still holds; it is just useless. Balanced trees such as AVL and red-black trees add rotations to keep the height logarithmic; Java's TreeMap is a red-black tree, and C++'s std::map is usually one.
Duplicates need one rule: reject them, count them, or always send them the same way. The code below sends them right.
Watch it run
The animation runs the lesson's script. The tree starts as a single root, 50, and every insert walks down from there. Insert 30: 30 is less than 50, so go left; that slot is empty, so 30 lands there. Insert 70: greater, so right, and the slot is free. Insert 20: left at 50, and the whole right side is out of the question from here on; left again at 30, and 20 lands after two comparisons. Insert 40: left at 50, right at 30, and it lands. Now search(40), one comparison per level, the losing side discarded outright. At 30, go right; two of the five nodes are already gone, and 40 is found. The cost is O(h), the height, not O(n). Then search(45) walks the identical path, left at 50, right at 30, right at 40, and that slot is empty: False. The last frame points out that this empty slot is exactly where insert(45) would put it; insert and search walk the same path.
BST Insert and Search
Step 1 of 12
The tree starts as a single root, 50. Every insert walks down from here.
The same interactive animation as the lesson — step through it with the controls.
The code
An iterative insert, so a tall tree cannot hit Python's recursion limit, and a search that returns the path it walked. The two searches from the animation take the same path; inserting 45 then lands in the slot where the second one failed:
class Node:
def __init__(self, val):
self.val, self.left, self.right = val, None, None
def insert(root, val):
"""Walk the search path; hang the new node on the first empty slot."""
if root is None:
return Node(val)
node = root
while True:
if val < node.val:
if node.left is None:
node.left = Node(val)
return root
node = node.left
else: # duplicates go right: one fixed rule
if node.right is None:
node.right = Node(val)
return root
node = node.right
def search_path(root, val):
path, node = [], root
while node:
path.append(node.val)
if val == node.val:
return True, path
node = node.left if val < node.val else node.right # one side discarded
return False, path
root = None
for v in [50, 30, 70, 20, 40]:
root = insert(root, v)
print(search_path(root, 40)) # (True, [50, 30, 40])
print(search_path(root, 45)) # (False, [50, 30, 40])
root = insert(root, 45) # lands exactly where the search failed
print(root.left.right.right.val) # 45
Why the height is the bill. An in-order walk lists the keys sorted. Then the same 1,000 keys go in twice, once sorted and once shuffled with a fixed seed, and the heights and search paths are compared:
import random
def inorder(node, out):
if node:
inorder(node.left, out); out.append(node.val); inorder(node.right, out)
return out
def height(root):
best, stack = 0, [(root, 1)] if root else []
while stack:
node, depth = stack.pop()
best = max(best, depth)
stack += [(child, depth + 1) for child in (node.left, node.right) if child]
return best
print(inorder(root, [])) # [20, 30, 40, 45, 50, 70]
random.seed(25)
keys = list(range(1, 1_001))
chain = None
for k in keys: # sorted input: every key goes right
chain = insert(chain, k)
shuffled = keys[:]
random.shuffle(shuffled)
bushy = None
for k in shuffled:
bushy = insert(bushy, k)
print(height(chain), height(bushy)) # 1000 24
print(len(search_path(chain, 1_000)[1]), len(search_path(bushy, 1_000)[1])) # 1000 8
The follow-up that comes next: the largest key at or below x, and the smallest at or above it, in one walk. Every time the walk goes right, the current node is a candidate floor; every time it goes left, a candidate ceiling:
def floor_ceiling(root, x):
floor = ceiling = None
node = root
while node:
if node.val == x:
return x, x
if node.val < x:
floor, node = node.val, node.right # a floor; look for a bigger one
else:
ceiling, node = node.val, node.left # a ceiling; look for a smaller one
return floor, ceiling
print(floor_ceiling(root, 42), floor_ceiling(root, 10), floor_ceiling(root, 99))
# (40, 45) (None, 20) (70, None)
Checked on 1,500 seeded random trees with duplicates: the in-order walk must equal sorted, every search must agree with a membership test, floor and ceiling must equal a brute-force scan, and no search path may be longer than the height:
ok = True
for _ in range(1_500):
values = [random.randint(0, 40) for _ in range(random.randint(0, 25))]
tree = None
for v in values:
tree = insert(tree, v)
ok &= inorder(tree, []) == sorted(values)
for x in range(-2, 43):
ok &= search_path(tree, x)[0] == (x in values)
lo = max((v for v in values if v <= x), default=None)
hi = min((v for v in values if v >= x), default=None)
ok &= floor_ceiling(tree, x) == (lo, hi)
ok &= len(search_path(tree, 99)[1]) <= height(tree)
print(ok) # True
The complexity
- Search and insert:
O(h). On random inputhgrows likelog n; on sorted inputh = n. - Balanced trees:
O(log n)guaranteed, paid for with rotations on insert and delete. - Space:
O(n)nodes. The iterative versions needO(1)extra; recursive ones useO(h)stack. - In-order listing:
O(n), and it comes out sorted, which binary tree traversals covers in detail. The Big-O cheat sheet lists BST costs next to the other structures.
Where it goes wrong
- Calling it
O(log n)without the qualifier. Only for a balanced or random tree. - Recursive insert on a tall tree. A 1,000-deep chain hits Python's default recursion limit; walk it with a loop.
- No duplicate rule. Sending equal keys left sometimes and right other times makes search miss them.
- Checking only the parent. Validity is about whole subtrees, not just each node's children, as validate BST shows.
- Losing the root. Inserting into an empty tree must return it.
When it shows up in interviews
"Insert into a BST" and "search a BST" are warm-ups for questions built on the same walk: closest value, floor and ceiling, the lowest common ancestor in a BST, and the k-th smallest via in-order. Sorted input turning the tree into a chain is the classic follow-up, and the expected answer is a balanced tree.
How to say it in an interview
"At each node I compare once: smaller goes left, larger goes right, and the other subtree is discarded. Search stops on a match or an empty slot. Insert walks the same path and puts the new node in the empty slot where the search would have failed, so the two always agree. Both cost O(h). That is O(log n) for a balanced or random tree, but sorted input builds a chain with h = n, which is why production trees rebalance. I write it iteratively, so height never hits a recursion limit."