Binary search trees
On This Lesson 11 topics
Lesson Overview
In the last lesson a binary tree was only a shape: each node had a value and up to two children, and the values could be in any order. That is not much use for finding things. This lesson adds one ordering rule, and that rule turns the shape into a fast search structure, the binary search tree (BST).
Think of the "higher or lower" guessing game. You guess a number, and you are told to go higher or lower, so half of the possibilities disappear with each guess. A BST puts that game into a data structure: at each node, one comparison tells you to go left or right and discards a whole branch.
Why this matters: the BST is the simplest example of an ordered structure with fast lookup. The same idea sits behind TreeMap and TreeSet, which you met in the collections module, and behind the indexes that databases use.
This lesson builds on Lesson 1 (Node, null children, recursion over a tree) and on recursion with a base case from the call-stack module.
Learning Objectives
By the end of this lesson you will be able to:
- State the BST rule and check whether a drawn tree obeys it.
- Insert values into a BST by hand and draw the result.
- Write a recursive
insertand a recursivecontains, each starting with thenullcheck. - Trace the path a search follows, including a failed search.
- Explain why one search step discards a whole subtree and what that costs on a balanced tree.
- Say how duplicate values are handled in the code of this lesson.
Written Explanation
The BST rule
A binary search tree is a binary tree in which, 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 than the node's value.
The rule is about the whole subtree, not just the immediate children. In the tree below, 6 is smaller than 8 and sits in 8's left subtree, so it is allowed even though 6 is the right child of 3.
8
/ \
3 10
/ \ \
1 6 14What about equal values? Books differ: some put duplicates on the left, some on the right. In this lesson we keep each value at most once: inserting a value that is already present does nothing. That is also how a Set behaves.
Search
To look for a value v, start at the root and repeat:
- If the node is
null, the value is not in the tree. - If
vequals the node's value, you found it. - If
vis smaller, continue in the left subtree. If larger, continue in the right subtree.
Every comparison eliminates one whole side. This is why the structure is called a search tree. In Java, using the same recursive pattern as Lesson 1:
private boolean contains(Node n, int v) {
if (n == null) return false; // base case: fell off the tree
if (v == n.value) return true;
return v < n.value ? contains(n.left, v) : contains(n.right, v);
}The null check comes first. Without it, a failed search reaches a missing child, and the next line reads n.value on null.
Insert
Insertion is a search that ends by attaching a new node. You search for the place where the value would be, and when you reach null, you put the new node there. Every new value becomes a leaf, and existing nodes never move.
private Node insert(Node n, int v) {
if (n == null) return new Node(v); // found the empty spot
if (v < n.value) n.left = insert(n.left, v);
else if (v > n.value) n.right = insert(n.right, v);
// equal: already present, do nothing
return n;
}Each recursive call returns the root of the (possibly changed) subtree, and the caller stores it back: n.left = insert(n.left, v). When the subtree did not change, the same node comes back and the assignment changes nothing. When n.left was null, the new node is returned and gets attached. The public method starts the process with root = insert(root, v), which also handles the very first insert into an empty tree.
Cost: one level per step
Each step of search or insert moves one level down, so the number of steps is at most the height of the tree plus one. If the tree is balanced, with its nodes spread out, the height is about log base 2 of n. Doubling the number of values adds only one more level. A balanced tree of about a million nodes needs roughly 20 comparisons at the most. That is the O(log n) you learned in the Big O module.
The word "if" matters. The shape depends on the order of insertion, and a bad order produces a tall tree and slow searches. Lesson 4 shows how. For this lesson, remember that the cost of a BST operation is proportional to its height.
Finding the smallest value
The smallest value is reached by always going left until there is no left child. The largest is found by always going right. This uses the BST rule directly, and it is a loop, not a recursion:
public Integer min() {
if (root == null) return null;
Node n = root;
while (n.left != null) n = n.left;
return n.value;
}It returns Integer rather than int so that null can mean "the tree is empty".
Examples
Example 1: inserting by hand (simple)
Insert 8, 3, 10, 1, 6, 14, in that order, into an empty tree.
- 8 becomes the root.
- 3 is smaller than 8: left of 8.
- 10 is larger than 8: right of 8.
- 1 is smaller than 8, then smaller than 3: left of 3.
- 6 is smaller than 8, then larger than 3: right of 3.
- 14 is larger than 8, then larger than 10: right of 10.
The result is the tree drawn in the explanation. Insert the same values as 10, 14, 8, 6, 3, 1 and you get a different shape.
Example 2: words instead of numbers (practical)
The BST rule only needs "smaller" and "larger". Anything that can be compared works. For String values, compareTo says which comes first in alphabetical order (it returns a negative number, zero, or a positive number).
public class Words {
static class Node { String w; Node left, right; Node(String w) { this.w = w; } }
static Node insert(Node n, String w) {
if (n == null) return new Node(w);
int c = w.compareTo(n.w);
if (c < 0) n.left = insert(n.left, w);
else if (c > 0) n.right = insert(n.right, w);
return n;
}
static boolean contains(Node n, String w) {
if (n == null) return false;
int c = w.compareTo(n.w);
if (c == 0) return true;
return contains(c < 0 ? n.left : n.right, w);
}
public static void main(String[] a) {
Node r = null;
for (String s : new String[] {"mango", "apple", "pear", "cherry", "zebra"}) r = insert(r, s);
System.out.println(contains(r, "cherry") + " " + contains(r, "kiwi"));
}
}Output (compiled and run with JDK 21):
true falseThe code calls compareTo once and keeps the answer in c. TreeSet<String> decides where a word belongs in a similar way.
Example 3: a complete BST class and the search path (realistic)
Here is a small Bst class that wraps the pieces and adds a trace method that prints the nodes compared during a search. Only the added trace and main are new; insert, contains, and min are the methods shown above, with Node as a nested class.
public void trace(int v) {
Node n = root;
StringBuilder path = new StringBuilder();
while (n != null) {
path.append(n.value).append(" ");
if (v == n.value) { System.out.println(path + "-> found"); return; }
n = v < n.value ? n.left : n.right;
}
System.out.println(path + "-> not found (reached null)");
}
public static void main(String[] args) {
Bst t = new Bst();
for (int v : new int[] {8, 3, 10, 1, 6, 14}) t.insert(v);
t.insert(6); // duplicate: ignored
System.out.println(t.contains(6) + " " + t.contains(7) + " min=" + t.min());
t.trace(6);
t.trace(7);
t.trace(14);
System.out.println(new Bst().min());
System.out.println(new Bst().contains(1));
}Output (compiled and run with JDK 21):
true false min=1
8 3 6 -> found
8 3 6 -> not found (reached null)
8 10 14 -> found
null
falseSearching for 7 walks the same path as 6 and then reaches the empty right child of 6: that is where 7 would be inserted. The last two lines show that empty trees are handled.
Backend Insight
A BST is a model of how ordered lookup works, and you will use it in three ways:
TreeMapandTreeSet. The JDK documentation saysTreeMapis a red-black tree implementation, and thatget,put,remove, andcontainsKeyrun in guaranteed O(log n). A red-black tree is a BST that keeps itself balanced (Lesson 4). You choose it when you need keys in sorted order, whichHashMapdoes not give you.- Range questions. "All orders between two dates" is natural in a sorted structure. A hash table cannot answer it without scanning everything. Database indexes exist largely for this reason.
- Interviews and code review. "Is this tree a valid BST?" is a classic question, and the usual trap is checking only the immediate children.
The BST you write here lives in RAM, so nothing survives a restart. Real services keep the durable copy in a database.
Common Mistakes
Mistake 1: Missing the null check (the planned mistake)
Suppose insert is written without the base case:
static Node insert(Node n, int v) {
if (v < n.value) n.left = insert(n.left, v);
else if (v > n.value) n.right = insert(n.right, v);
return n;
}Building a root of 8 and then inserting 3 fails at once, because the left child of 8 is null and the recursive call reads value from it. Real output from JDK 21 (compiled without the debug flag, which is why the parameter has a generic name):
Exception in thread "main" java.lang.NullPointerException: Cannot read field "value" because "<parameter1>" is null
at Bad.insert(Bad.java:4)
at Bad.insert(Bad.java:4)
at Bad.main(Bad.java:10)The two insert frames show the recursion; the second received null. The fix is the base case if (n == null) return new Node(v); as the first line.
Mistake 2: Checking only the children
A tree is a BST only if every value in the left subtree is smaller than the node, not just the left child. A 9 under the left child of 8 breaks the rule even if it is larger than its parent 3. Check against all ancestors.
Mistake 3: Forgetting to store the returned node
insert(n.left, v); // wrong: result ignored, a new node is lost
n.left = insert(n.left, v); // rightWhen the left child is null, the new node is only reachable through the return value.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Insert 15, 8, 20, 3, 10, 17 into an empty BST. Draw the result.
Show solution A1
A1.
15
/ \
8 20
/ \ /
3 10 17A2. In your tree from A1, list the nodes visited when searching for 10 and when searching for 12. Which search fails, and where does it end?
Show solution A2
A2. Searching for 10 visits 15, 8, 10 and finds it. Searching for 12 visits 15, 8, 10, then goes right of 10, which is null: it fails there, and 12 would be inserted at that spot.
A3. In the same tree, which value is the minimum and which is the maximum? How do you find each without looking at every node?
Show solution A3
A3. The minimum is 3: start at the root and keep going left (15, 8, 3). The maximum is 20: keep going right (15, 20). Both follow one path, no matter how many nodes there are.
Level B: Application
B1. Write contains for a tree of String values (a node class with a String w). Use compareTo once per node.
Show solution B1
B1.
static boolean contains(Node n, String w) {
if (n == null) return false;
int c = w.compareTo(n.w);
if (c == 0) return true;
return contains(c < 0 ? n.left : n.right, w);
}B2. Write static int max(Node root) that returns the largest value in a non-empty BST, using a loop.
Show solution B2
B2.
static int max(Node n) {
while (n.right != null) n = n.right;
return n.value;
}B3. What happens in this lesson's insert if you add 8 to a tree that already contains 8? Name one reason to keep duplicates out and one way another design could store them.
Show solution B3
B3. The second 8 is ignored: neither if branch matches, so the same node is returned and nothing changes. Keeping each value once matches the behavior of a Set and keeps searches simple. A design that needs duplicates can store a count in each node, or consistently send equal values to one side (for example the left).
Level C: Challenge
C1. Write an iterative insert (a loop, no recursion) that returns the root. It must handle an empty tree and ignore duplicates.
Show solution C1
C1.
static Node insertIter(Node root, int v) {
if (root == null) return new Node(v);
Node n = root;
while (true) {
if (v == n.value) return root;
if (v < n.value) {
if (n.left == null) { n.left = new Node(v); return root; }
n = n.left;
} else {
if (n.right == null) { n.right = new Node(v); return root; }
n = n.right;
}
}
}Inserting 40, 20, 60, 10, 30, 50, 70 with it and calling max gives 70 (compiled and run with JDK 21).
C2. Write static int countBetween(Node n, int lo, int hi) that counts the values v with lo <= v <= hi in a BST. Use the BST rule to skip subtrees that cannot contain any such value.
Show solution C2
C2.
static int countBetween(Node n, int lo, int hi) {
if (n == null) return 0;
int c = (n.value >= lo && n.value <= hi) ? 1 : 0;
if (n.value > lo) c += countBetween(n.left, lo, hi);
if (n.value < hi) c += countBetween(n.right, lo, hi);
return c;
}If the node's value is lo or less, nothing in its left subtree can be in range, so the left side is skipped; the right side is skipped when the value is hi or more. On the tree built in C1, countBetween(root, 25, 60) returns 4 (30, 40, 50, 60).
Mini Challenge
Build a tiny phone-book index. Create a BST of String names (use the Words code from Example 2 as a starting point). Insert six names of your choice in any order, then:
- Draw the resulting tree on paper.
- Search for one name that is present and one that is absent, writing down the path for each.
- Insert the same six names in alphabetical order on paper and draw that tree. Compare the two drawings.
Check your work: every name in a node's left subtree comes before it alphabetically, and every name in its right subtree comes after it.
Summary & Next Lesson
Key takeaways
- A BST keeps smaller values in the left subtree and larger values in the right subtree of every node.
- Search compares once per level and goes left or right; insert searches and attaches a new leaf at the
nullspot. - Recursive tree code starts with the
nullcheck; skipping it gives aNullPointerException. - The cost of search and insert is proportional to the height: about log n on a balanced tree.
- The shape of a BST depends on the insertion order. Equal values are ignored in this lesson's code.
Next lesson
The next lesson, "Traversals", shows four ways to visit every node of a tree, and why one of them prints a BST in sorted order.
Self-check
- What is the BST rule?
- Why does a failed search end at the place where the value would be inserted?
- Where does a newly inserted value always end up: leaf, root, or middle?
- Which line must come first in a recursive
contains? - How do you find the smallest value in a BST?
Show answers
- For every node, values in the left subtree are smaller and values in the right subtree are larger.
- Search and insert follow the same path; search stops at
null, which is exactly the empty spot insert fills. - A leaf. Existing nodes are never moved.
- The check
if (n == null). - Start at the root and keep following the left child until there is none.
References
- OpenDSA (Virginia Tech): Binary Search Trees. It states the BST property, describes search and insert as walking down and attaching a leaf, and says cost depends on the tree's height. That page puts duplicates on the left; this lesson ignores duplicates instead, which is a design choice, not a rule.
- Oracle Java SE 21 API: TreeMap. It says
TreeMapis a red-black tree implementation with guaranteed log(n) time forcontainsKey,get,put, andremove, sorted by natural order or a comparator. - Every Java example above was compiled and run with JDK 21 and the output is copied from the real run.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.