DataData/08 · Binary Search Tree
CHAPTER 08 · BST

Binary search tree BST

One rule added to a binary tree: left < node < right, and it must hold for the whole subtree, not only for the two children. Each comparison then drops an entire subtree from the search. It is binary search, growing on a tree you can insert into and delete from at any time.

§01

Why it exists: fast lookup and fast update at the same time

A sorted array and a linked list each fail at one of the two. A balanced BST does both in O(log n).

Look at what the two earlier structures are good at. A sorted array searches very quickly: binary search halves the range each time, O(log n). But inserting a new value means making room for it, and every element to its right moves one position, O(n). A linked list is the opposite. Once you already hold the position, linking a node in changes two pointers, O(1). Finding a value, though, means walking from the head, O(n). A linked list cannot even run binary search, because it has no O(1) random access: the step "jump to the middle" itself costs O(n).

StructureSearchInsert / deleteThe reason
Sorted arrayO(log n) (binary search)O(n) (shifting)Contiguous memory: any change moves the other elements
Linked listO(n) (walk from the head)O(1) (once you hold the position)No random access, so there is no way to jump to the middle
BST (while balanced)O(h)O(h)Has to be kept from degenerating (§05)

The idea of a BST is to freeze the decisions of binary search into the shape of a tree. Think of a library. The sign at the entrance says "A–M left, N–Z right". Inside the left wing another sign says "A–F left, G–M right". Each sign you read removes a large part of the building from your search. Every node of a BST is one of those signs. And because the nodes are connected by pointers (chapter 3), adding a new sign moves nothing that is already there.

RULE · the only rule
⚖️ left < node < right

For any node: every node in its left subtree is smaller than it, and every node in its right subtree is larger. The rule is about the whole subtree, not about the two children. That distinction is the most common misunderstanding in this chapter, and LC 98 in §06 is built to expose it.

POWER 01
Search is one walk down from the root

At each node you ask one question, smaller or larger, and the whole subtree on the other side is dropped. The number of comparisons equals the number of nodes on the path you walk, which is at most h + 1 (a path of h edges touches h + 1 nodes). When the tree is balanced, each step removes about half the remaining nodes.

POWER 02
Updates move nothing

Insert: follow the search path to an empty slot, attach the new node, change one pointer. Delete: at most one path is touched. Nothing is shifted along an array. This is the behavior inherited from the linked list.

A dictionary you can still write in

A printed dictionary is sorted but fixed: adding a word means printing a new edition. A hash table is the opposite: reads and writes are very fast, but the entries come out in no useful order. A BST is a dictionary with loose pages. You can add a page at any time, and reading it in order (in-order traversal) still gives you sorted entries. Whenever a requirement contains both changing data and order, a tree structure is usually the answer.

§02

The property: in-order traversal gives sorted keys

In-order output is ascending, search is one path down — then grow a lopsided tree yourself

The previous chapter covered four traversals. For a BST one of them matters more than the rest: in-order traversal (left → node → right) always outputs the keys in ascending order. Why? Draw the tree with each node above its own position on the line below. The rule puts every node of the left subtree left of the root and every node of the right subtree right of it, so the horizontal position of a node is its rank in sorted order:

Why in-order traversal comes out sorted: project every node straight down
50#430#270#620#140#360#580#720[0]30[1]40[2]50[3]60[4]70[5]80[6]
"left < node < right" holds for the whole subtree, so every node of the left subtree is drawn left of its root and every node of the right subtree is drawn right of it. The horizontal position of a node is therefore its rank. In-order traversal (left → node → right) reads the nodes from left to right, so its output is sorted. This is not a coincidence; it follows directly from the rule.

How is this used? Read it in both directions. To get the data out of a BST in order, traverse in order; no sorting step is needed. To check whether a tree is a BST, check that its in-order sequence is strictly increasing. To find the k-th smallest value, count to k during an in-order traversal. One property carries half of this chapter's problems.

Now search. Type a number into the lab below and press Search. It starts at the root, and after each comparison it continues on one side only. Try Insert as well: the new value always ends up in the empty slot where the search failed. Then press "Insert 1→5 in order" at least once and watch a tree that still obeys the rule but has lost all its speed.

BST lab — insert, search, and grow a lopsided tree on purpose
50302040706080
Type a whole number between 0 and 99, then insert or search it. Watch it start at the root and take one comparison per level.
nodes 7 · height 2 · min height ⌊log₂n⌋ = 2

That lopsided tree is the weak point of a BST

With sorted input every new value turns the same way, and the tree leans into a chain. The height h goes from about log n up to n − 1, and search stops removing subtrees and starts checking nodes one by one, exactly like a linked list. The rule (left smaller, right larger) guarantees correctness, not shape. Shape is the job of the balanced trees in §05.

§03

Core operations: every one of them is O(h)

Search and insert are straightforward. Delete has three cases, drawn one by one.

OperationComplexityWhy
searchO(h)Each comparison drops one subtree, so you walk at most one root-to-leaf path
insertO(h)A failed search, plus attaching the node in the empty slot (one pointer)
deleteO(h)O(h) to locate it, then repair; in the worst case a further walk to find the successor
minimum / maximumO(h)Go left (or right) until there is no child; no comparison is needed
in-order traversal (the sorted sequence)O(n)Every node has to be visited once

Why O(h) everywhere and not O(log n)? Because h, the height of the tree, is a variable. As in chapter 7, height is the number of edges on the longest root-to-leaf path, so a tree with only a root has height 0. When the tree is balanced, h = ⌊log₂n⌋, since each level can hold twice as many nodes as the one above it. When sorted input makes the tree degenerate into a chain, h = n − 1. Saying O(log n) without a condition is wrong. The complete answer is: O(h); that is log n when the tree is balanced and n in the worst case, and production code uses a red-black tree to keep h at log n.

You have already done search and insert by hand in the lab. The hard operation is delete. Removing a node leaves a hole in the tree, and the hole has to be filled without breaking the ordering. Split it by how many children the deleted node has:

CASE 1 · a leaf
Just remove it
50307020delete me40
20 has no children, so nothing depends on it. Set the parent pointer that reaches it to null. The rest of the tree is untouched.
CASE 2 · one child
The child moves up
503070delete me80moves up
70 has one child, 80. Let the parent, 50, point straight at 80, the same move as skipping a node in a linked list. Everything under 80 was already larger than 50, so the ordering still holds.
CASE 3 · two children
Replace it with the in-order successor
50delete me3070204060successor80
50 has a subtree on each side, so neither child can simply move up. Take the in-order successor 60, the smallest value in the right subtree: copy 60 into the root, then delete the original 60 from the right subtree.

Case 3 raises two questions worth answering. Why can the successor take the place? The in-order successor is the next key above the deleted one, with nothing in between. After it moves up, every node on the left is still smaller than it (it is larger than the deleted value), and every remaining node on the right is still larger than it (it was the smallest value there). The ordering holds on both sides, and no other key would do. Why is deleting the successor easy? The successor is the end of a walk left from the right child, so it has no left child. Deleting it falls into case 1 or case 2, and the recursion cannot continue for ever. The in-order predecessor (the largest value in the left subtree) works the same way, by symmetry.

Delete in one line

A leaf is removed; a node with one child is replaced by that child; a node with two children takes the successor's value, and the delete moves into the right subtree. That turns "remove a node in the middle" into "remove a node at the edge". Total cost: locate, find the successor, delete again — all of it walks downwards along one path, so O(h).

§04

Writing one: a complete BST in about 60 lines

insert / search / delete / inorder — all three delete cases included, and it runs

The node is the TreeNode from the binary tree chapter: a value plus a pointer to each child. Of the four methods, insert and delete are written recursively, and both return the root of the repaired subtree so the caller can reattach it. This pattern — return yourself so the parent links to you again — is the standard way to modify a tree by recursion, and it avoids having to track parent pointers.

bst.py
1class TreeNode: # the node from the binary tree chapter
2 def __init__(self, val=0):
3 self.val = val
4 self.left = None
5 self.right = None
6
7class BST:
8 def __init__(self):
9 self.root = None # keep the root; everything starts here
10
11 # search: one comparison per level, O(h)
12 def search(self, v: int) -> bool:
13 cur = self.root
14 while cur:
15 if v == cur.val:
16 return True
17 cur = cur.left if v < cur.val else cur.right # smaller left, larger right
18 return False # reached None: nowhere in the tree
19
20 # insert: follow the search path to an empty slot, O(h)
21 def insert(self, v: int) -> None:
22 def insert_at(node):
23 if node is None:
24 return TreeNode(v) # the empty slot is the new home
25 if v < node.val:
26 node.left = insert_at(node.left)
27 elif v > node.val:
28 node.right = insert_at(node.right)
29 return node # return self so the parent relinks
30 self.root = insert_at(self.root)
31
32 # delete: three cases, O(h)
33 def delete(self, v: int) -> None:
34 def delete_at(node, v):
35 if node is None:
36 return None # not found, return unchanged
37 if v < node.val:
38 node.left = delete_at(node.left, v)
39 return node
40 if v > node.val:
41 node.right = delete_at(node.right, v)
42 return node
43 # found it -- three cases
44 if node.left is None:
45 return node.right # 1 no child / 2 only a right child
46 if node.right is None:
47 return node.left # 2 only a left child
48 succ = node.right # 3 two children: the in-order successor
49 while succ.left: # = leftmost node of the right subtree
50 succ = succ.left
51 node.val = succ.val # copy the successor value up
52 node.right = delete_at(node.right, succ.val) # delete it there
53 return node
54 self.root = delete_at(self.root, v)
55
56 # in-order: left, node, right. The output is sorted. O(n)
57 def inorder(self) -> list[int]:
58 out = []
59 def walk(node):
60 if node is None:
61 return
62 walk(node.left)
63 out.append(node.val)
64 walk(node.right)
65 walk(self.root)
66 return out
Common mistake: the default recursion limit in CPython is about 1000. On a BST that has degenerated into a chain, a recursive search can exceed it. When the tree may be deep, write search and insert iteratively. delete is easier to keep recursive, because it has to relink parent and child.
§05

Balanced trees: AVL, red-black, and what each language gives you

A concept section. You are not asked to implement these, but you should be able to say why they exist and which one to use.

The lab in §02 showed it already: sorted input grows a BST into a chain. Real data is often sorted: log records written by timestamp, rows inserted by auto-increment id, word lists imported alphabetically. A plain BST will almost certainly degenerate in production. The idea behind the fix is direct: after an insert or a delete, if some part of the tree has become too deep on one side, rotate it back. Balanced trees differ only in how they define "too deep" and how far they let it go before acting.

05·A

AVL tree: no imbalance allowed

AVL (1962, named after its inventors Adelson-Velsky and Landis) gives every node a balance factor: the height of the left subtree minus the height of the right subtree. It requires that factor to stay in {-1, 0, 1} for every node. As soon as an insert pushes some node to |BF| = 2, a rotation repairs it. Here is the smallest example, inserting 3, 2, 1 in that order:

BEFORE · after inserting 1
3BF = +2, too far2BF = +11BF = 0
The left subtree of 3 has height 1, and its right subtree is empty, which counts as −1. So BF = 1 − (−1) = +2, over the limit. The shape is "left-left", and the repair is a right rotation.
AFTER · one right rotation at 3
2BF = 0, fixed1BF = 03BF = 0
A right rotation makes the left child, 2, the new root, and 3 becomes its right child. The in-order sequence is still 1, 2, 3: a rotation changes the shape, not the order, which is why it is allowed at all.

A real AVL also handles the "left-right" and "right-left" shapes, which need two rotations. The idea is the same: bring the middle of the three values up to be the root. AVL is the strict option. Its height stays close to log n, so lookups are as fast as they can be. The price is paid on writes.

05·B

Red-black tree: balanced enough, not perfect

A red-black tree takes a different route. Instead of watching the height difference, it gives each node a color and keeps five rules:

  • 1. Every node is either red or black.
  • 2. The root is black.
  • 3. Empty positions (the null children) count as black.
  • 4. The children of a red node must be black, so two reds can never be adjacent.
  • 5. From any node, every path down to an empty position contains the same number of black nodes.

Together these give a neat result. The longest path can only alternate red and black, the shortest path can be all black, and both contain the same number of black nodes by rule 5. Therefore the longest path is at most twice the shortest, and the height stays in O(log n). The tree is allowed to be somewhat uneven, and in exchange each update needs only a small fixed number of rotations: at most 2 for an insert and at most 3 for a delete. An AVL insert also needs at most one rotation, but an AVL delete may have to rebalance at every level on the way back to the root. Not perfectly balanced, just balanced enough — that is an engineering trade-off, and it is why the red-black tree ended up in the standard libraries. Use AVL when reads dominate; use red-black when reads and writes are mixed.

Why red and black?

Guibas and Sedgewick published the structure in 1978 at Xerox PARC. Sedgewick has said the colors came from the lab printer: it could print red and black, and red made the special nodes easiest to pick out in the paper. The ink available in one machine set the color scheme of textbooks for the next fifty years.

05·C

In practice: ordered containers in three languages

One question decides whether you want a tree at all: do you need order? For a plain lookup — is this key present, what is its value — a hash table wins, because it is O(1) on average and a tree is O(log n). You choose a balanced BST when the requirement mentions range queries, the nearest key above or below a value, or iterating in key order. A hash table cannot answer any of those without reading everything and sorting it.

LanguageOrdered containerWhat it really isWhen to use it
JavaTreeMap / TreeSetRed-black treeOrdered iteration, floorKey / ceilingKey (nearest key), and subMap range queries
PythonNone built in (third-party sortedcontainers)A list of short sorted blocks, not a treeSortedList / SortedDict, already installed in the LeetCode environment
JavaScriptNone built inMap only keeps insertion order. Use a sorted array with binary search, or write the tree yourself
ordered_map.py
1# The Python standard library has no balanced BST. Two options:
2
3# 1. sortedcontainers (already installed on LeetCode; fine to name in an interview)
4from sortedcontainers import SortedList, SortedDict
5
6sl = SortedList([30, 10, 20]) # always kept sorted: [10, 20, 30]
7sl.add(25) # insert, still sorted -> [10, 20, 25, 30]
8sl[0], sl[-1] # 10, 30 -- smallest / largest
9sl.bisect_left(25) # 2 -- position, for slicing out a range
10sl.irange(10, 25) # [10, 20, 25] -- iterate over a range
11
12sd = SortedDict({30: "c", 10: "a"})
13list(sd.keys()) # [10, 30] -- iteration is in key order
14
15# 2. Read-mostly data: the bisect module with a plain list
16import bisect
17arr = [10, 20, 30]
18bisect.bisect_left(arr, 25) # search O(log n)
19bisect.insort(arr, 25) # insert O(n) -- a list is still an array
Be precise about this one: bisect.insort searches quickly but still inserts in O(n), because a list is a dynamic array (chapter 1). SortedList is much faster on writes because it stores the values in many short blocks, so an insert only shifts one short block. It is not a balanced tree, and its insert is not O(log n) in theory, but for interview and contest workloads it is the practical choice.

In production: why do database indexes use a B+ tree instead of a red-black tree?

The index in MySQL (InnoDB) is a B+ tree. It is also an ordered tree, so why not the red-black tree that wins in memory? Because the setting is different. The data lives on disk, and disk is read one page at a time, usually 16KB, so reading 1 byte costs almost the same as reading 16KB. A red-black tree stores one key per node and has height about log₂n, so for a million rows one lookup walks a path of roughly 20 nodes, which means up to 20 disk reads. A B+ tree instead fills each node with a whole page of several hundred keys. With a few hundred children per node, a million rows fit in 3 or 4 levels: a short, wide tree means very few reads. A B+ tree also keeps all the data in the leaf level and links the leaves together, so a range scan (WHERE id BETWEEN …) follows that list instead of jumping around the tree. In one sentence: the red-black tree is designed for random access in memory, the B+ tree for page-sized reads from disk. Structures are not better or worse; they fit a particular medium.

§06

Patterns and walkthroughs: one key opens most BST questions

★ Interview core

When you see a BST, say it to yourself: in-order is sorted, and one comparison drops one subtree. Three problems, frame by frame.

Most BST problems fall into three patterns. First, use in-order = sorted (k-th smallest, minimum difference, validation, recovery). Second, use the comparison to drop a subtree (search, insert, delete, lowest common ancestor, pruning a range sum). Third, build in reverse (turn sorted data into a balanced BST). The three walkthroughs below take one pattern each.

Walkthrough A

LC 98 · Validate Binary Search Tree

MEDIUM

The task: decide whether a binary tree is a valid BST. The trap: almost everyone first writes "check that each node is larger than its left child and smaller than its right child". That is wrong. The rule covers the whole subtree, not one parent and its children. Here is the counterexample built to break it:

LC 98 · the range method, frame by frame
10515620
First the trap. Check each parent against its own children: 5<10 ✓, 15>10 ✓, 6<15 ✓, 20>15 ✓, so every pair passes. And yet this tree is not a BST. Where is the problem?
1 / 6
lc98_validate_bst.py
1class Solution:
2 def isValidBST(self, root: TreeNode | None) -> bool:
3 # every node must fall inside the open range (lo, hi) passed down by its ancestors
4 def check(node, lo, hi):
5 if node is None:
6 return True # an empty tree is valid
7 if not (lo < node.val < hi): # outside the range: fail
8 return False
9 return (check(node.left, lo, node.val) # tighten the upper bound
10 and check(node.right, node.val, hi)) # tighten the lower bound
11 return check(root, float("-inf"), float("inf"))
Convenient: the chained comparison lo < node.val < hi is a Python feature, and float("±inf") removes any worry about integer bounds.

Complexity and follow-up questions

Each node is checked once: O(n) time, O(h) space for the recursion stack. Follow-up one, "is there another solution?" — yes: traverse in order and check that the sequence strictly increases (keep prev, and fail as soon as cur ≤ prev). That is the property from §02, and you should know both. Follow-up two, "why is the range open at both ends?" — because the problem requires strictly smaller and strictly larger, so an equal value is also a violation; duplicates are not allowed.

Walkthrough B

LC 230 · Kth Smallest Element in a BST

MEDIUM

The task: return the k-th smallest value in a BST. The brute force: traverse in any order, collect every value, sort, take the k-th. That is O(n log n) and uses nothing about the BST. Why in-order: the sorting was already done when the tree was built. In-order traversal is the values in ascending order, so you only have to count as you go and stop at k:

LC 230 · counting during in-order traversal (k = 3)
537248
The goal is the k = 3 smallest value. In-order traversal reports the keys from smallest to largest, so it first walks left as far as it can to reach the smallest value in the tree.
1 / 5
lc230_kth_smallest.py
1class Solution:
2 def kthSmallest(self, root: TreeNode | None, k: int) -> int:
3 # iterative in-order with an explicit stack: return as soon as k is reached
4 stack = []
5 cur = root
6 count = 0
7 while stack or cur:
8 while cur: # push the whole left spine
9 stack.append(cur)
10 cur = cur.left
11 cur = stack.pop() # popping = visiting in ascending order
12 count += 1 # count this node
13 if count == k:
14 return cur.val # the k-th one is the answer
15 cur = cur.right # turn to the right subtree
16 return -1 # unreachable for a valid k
Why iterative here: stopping a recursion early means carrying a flag back up through every frame, while the iterative version just returns. This stack template is also the answer to LC 173, the BST iterator.

Complexity and follow-up questions

Time O(h + k): O(h) to reach the leftmost node, then k values are produced. Space O(h). The classic follow-up: "what if inserts and deletes are frequent and you also query the k-th smallest often?" — store the size of the left subtree in every node. To query: if k ≤ leftSize go left; if k = leftSize + 1 this node is the answer; otherwise go right with k − leftSize − 1. That is O(h) per query, and the counts are updated during insert and delete. It is binary search performed on the tree itself.

Walkthrough C

LC 108 · Convert Sorted Array to Binary Search Tree

EASY

The task: turn an ascending array into a height-balanced BST. What not to do: insert the elements one by one. The input is sorted, and the lab in §02 showed what that produces: a chain. The solution: balance means the two sides hold almost the same number of nodes, and in a sorted array it is obvious which value belongs in the middle. Take the midpoint as the root, and build the two halves recursively.

LC 108 · take the midpoint, then recurse
lo
mid
hi
-100
-31
02
53
94
For the tree to be balanced, the two subtrees must hold almost the same number of nodes. Take mid = 2: 0 becomes the root, the left half becomes the left subtree, the right half becomes the right subtree.
1 / 5
The resulting tree
0-105-39
5 nodes, 3 levels, height 2 = ⌊log₂5⌋ — exactly the minimum possible.
Why does the midpoint guarantee balance?

Splitting at the midpoint leaves two halves whose lengths differ by at most 1. Every level of the recursion preserves that, so the two subtrees of any node differ by at most one node, and their heights differ by at most 1. What you are really building is the decision tree of binary search: mid is the root, the two halves are the subtrees, and every path binary search could take becomes a path in the tree.

lc108_sorted_array_to_bst.py
1class Solution:
2 def sortedArrayToBST(self, nums: list[int]) -> TreeNode | None:
3 def build(lo, hi):
4 if lo > hi:
5 return None # empty range, attach None
6 mid = (lo + hi) // 2 # the midpoint is the root
7 root = TreeNode(nums[mid])
8 root.left = build(lo, mid - 1) # left half becomes the left subtree
9 root.right = build(mid + 1, hi) # right half becomes the right subtree
10 return root
11 return build(0, len(nums) - 1)

Complexity and follow-up questions

Each element becomes a root exactly once: O(n) time, O(log n) space for the recursion stack. Follow-up: "what if the input is a sorted linked list?" (LC 109). Finding the middle of a linked list costs O(n), so there are two approaches: use the slow and fast pointers to find the middle each time, which totals O(n log n); or build in in-order sequence — count the length first, then recurse over the index range while consuming the list nodes in order, which stays O(n). Explaining that step finishes this group of problems.

§07

Problem set: 9 BST questions

Interview regulars

Ordered as core operations, then using the sorted property, then changing the structure. Think for 30 seconds before opening the hint.

§08

Chapter quiz

✎ Quiz

Get all 7 right to light up this chapter

QUESTION 01 / 7

A tree: the root is 10, its left child is 5 and its right child is 15; the left child of 15 is 6 and its right child is 20. Every parent-child pair is fine (5<10, 15>10, 6<15, 20>15). Is it a BST?

QUESTION 02 / 7

Which traversal of a BST outputs the keys in ascending order? (Write the name of the traversal.)

QUESTION 03 / 7

You insert values one by one into an empty BST. Which order makes it degenerate into a chain?

QUESTION 04 / 7

What data structure backs TreeMap and TreeSet in Java?

QUESTION 05 / 7

In an interview, what is the most precise way to state the time complexity of search in a BST?

QUESTION 06 / 7

You delete a node that has two children. Which node takes its place?

QUESTION 07 / 7

In which situations should you choose TreeMap (an ordered map) over HashMap? (Select all that apply.)

What to take away from this chapter
  • One rule defines everything: left < node < right, for the whole subtree. When you validate or use a BST, remember that the constraints of the ancestors are passed down (the range method in LC 98).
  • In-order traversal gives the keys in sorted order. K-th smallest, minimum difference, validation, recovery — most BST problems reduce to this one property.
  • Search, insert, and delete are all O(h), with the height h counted in edges: h = ⌊log₂n⌋ while the tree is balanced, and h = n − 1 after sorted input degenerates it. State the complexity with h, or state the condition.
  • Three delete cases: remove a leaf; replace a node that has one child by that child; for two children, copy the in-order successor and delete it from the right subtree. The successor has no left child, so the hard case turns into an easy one.
  • A BST with no rebalancing is not used in production: Java has TreeMap / TreeSet (red-black, balanced enough), Python has the third-party sortedcontainers, JavaScript has nothing built in. On disk, database indexes use a B+ tree: short and wide to cut the number of reads, with linked leaves for range scans.
  • Choose between a hash table and a BST by one question: do you need order? Plain lookups go to the hash table. Range queries, nearest-key lookups, and iteration in key order go to the ordered structure.