Data Structures & Algorithms (DSA) Module 9 – Trees and binary search trees
Binary search trees: search, insert, delete
How the search-tree ordering makes search, insert and delete one walk from the root, the three deletion cases, and why sorted input builds a path.
What you will learn
- Maintain the BST ordering invariant through insertions and deletions
- Delete nodes with zero, one and two children
- Validate a BST and find floor, ceiling and k-th smallest
Before you start
What you’ll learn · Before you start
What you will learn
- Maintain the BST ordering invariant through insertions and deletions
- Delete nodes with zero, one and two children
- Validate a BST and find floor, ceiling and k-th smallest
Before you start
On this page
A binary search tree (BST) keeps its keys in order: a node’s key is larger than all the keys below it on the left and smaller than all the keys below it on the right. That one rule turns search into a single walk from the root: compare, go left or right, and stop at the key or at an empty place. Insertion makes the same walk and attaches the new key where it fell off the tree. Deletion has three cases, decided by how many children the node has. Each of these operations costs O(h) for a tree of height h. That is about log2 n when the tree stays bushy, but n − 1 when the keys arrive in sorted order, as the measurements below show. And because of the rule, an inorder traversal lists the keys in increasing order.
The invariant
NIST’s Dictionary of Algorithms and Data Structures defines a binary search tree as a binary tree in which every node’s left subtree holds smaller keys and its right subtree larger ones. The rule applies to whole subtrees, not only to a node’s two children: every key below 30 on the left of 50 must also be smaller than 50. Each key is stored once in this lesson, so inserting a key that is already present changes nothing. Because the left subtree holds the smaller keys and the right one the larger, inorder visits a search tree’s keys in sorted order.
Search and insert, traced
"""A binary search tree: search, insert and the three cases of delete, with the tree printed after each step."""
class Node:
def __init__(self, key):
self.key, self.left, self.right = key, None, None
def search(root, key, path):
"""Walks down from the root; path collects the keys compared on the way."""
node = root
while node is not None:
path.append(node.key)
if key == node.key:
return node
node = node.left if key < node.key else node.right
return None
def insert(node, key):
"""Returns the subtree with key added; an existing key is left alone (each key is stored once)."""
if node is None:
return Node(key)
if key < node.key:
node.left = insert(node.left, key)
elif key > node.key:
node.right = insert(node.right, key)
return node
def delete(node, key):
"""Returns the subtree with key removed."""
if node is None:
return None # the key is not in the tree
if key < node.key:
node.left = delete(node.left, key)
elif key > node.key:
node.right = delete(node.right, key)
elif node.left is None: # no child, or only a right child: the child takes the node's place
return node.right
elif node.right is None: # only a left child
return node.left
else: # two children: take the inorder successor's key, delete it below
successor = node.right
while successor.left is not None:
successor = successor.left
node.key = successor.key
node.right = delete(node.right, successor.key)
return node
def inorder(node, out):
if node:
inorder(node.left, out)
out.append(node.key)
inorder(node.right, out)
return out
def shape(node):
"""The tree as nested brackets: key(left, right), with - for an empty child."""
if node is None:
return "-"
if node.left is None and node.right is None:
return str(node.key)
return f"{node.key}({shape(node.left)}, {shape(node.right)})"
root = None
for k in (50, 30, 70, 20, 40, 60, 80, 55, 65):
root = insert(root, k)
print("tree:", shape(root))
for key in (65, 58):
path = []
found = search(root, key, path)
print(f"search {key}: compared {path}, {'found' if found else 'not found'}")
root = insert(root, 58)
print("after insert 58:", shape(root))
for key, case in ((20, "a leaf"), (55, "one child"), (50, "two children")):
root = delete(root, key)
print(f"after delete {key} ({case}):", shape(root))
print("inorder:", inorder(root, [])) Output
tree: 50(30(20, 40), 70(60(55, 65), 80)) search 65: compared [50, 70, 60, 65], found search 58: compared [50, 70, 60, 55], not found after insert 58: 50(30(20, 40), 70(60(55(-, 58), 65), 80)) after delete 20 (a leaf): 50(30(-, 40), 70(60(55(-, 58), 65), 80)) after delete 55 (one child): 50(30(-, 40), 70(60(58, 65), 80)) after delete 50 (two children): 58(30(-, 40), 70(60(-, 65), 80)) inorder: [30, 40, 58, 60, 65, 70, 80]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bst.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
A search for 65 compares it with 50, 70, 60 and 65: right, left, right, found. A search for 58 compares it with 50, 70, 60 and 55 and then reaches an empty place, so 58 is not in the tree, and that empty place, the right child of 55, is exactly where an insertion puts it. Insertion never moves an existing key, so it keeps the invariant: the new key lies between the same bounds as the empty place it fills.
Delete: three cases
Deleting a key with two children by moving up its inorder successor
Text description of the diagram
The diagram shows the same binary search tree before and after deleting its root, 50, which has two children. Edges are labelled left and right.
- Before: 50 has a left child 30 (with children 20 and 40) and a right child 70 (with children 60 and 80). 60 has a left child 55 and a right child 65. The inorder successor of 50 is 55, the smallest key in its right subtree, found by going right once and then left as far as possible.
- The key 55 is copied into the root, and the node that held 55 is removed. That node had no left child, so removing it is one of the easy cases.
- After: 55 is the root, with 30 (children 20 and 40) on its left and 70 (children 60 and 80) on its right; 60 now has only a right child, 65. Every key on the left is still smaller than 55 and every key on the right larger.
- No children: remove the node. Deleting 20 just empties 30’s left child.
- One child: the child takes the node’s place. Deleting 55, whose only child is 58, puts 58 under 60.
- Two children: copy in the key of the node’s inorder successor, the smallest key of its right subtree, then delete the successor’s old node. The successor is the leftmost node of the right subtree, so it has no left child, and removing it is case 1 or case 2.
The successor is the right replacement because it is the next key in sorted order: larger than every key on the left, and smaller than every other key on the right. The run deletes 50 this way, 58 takes its place, and the inorder list stays sorted.
How tall the tree gets, measured
"""The height of a binary search tree after 4,096 insertions: keys in random order against keys in sorted order."""
import random
class Node:
__slots__ = ("key", "left", "right")
def __init__(self, key):
self.key, self.left, self.right = key, None, None
def insert(root, key):
"""Walks down from the root without recursion and attaches the key where the search fell off the tree."""
if root is None:
return Node(key)
node = root
while True:
if key < node.key:
if node.left is None:
node.left = Node(key)
return root
node = node.left
elif key > node.key:
if node.right is None:
node.right = Node(key)
return root
node = node.right
else:
return root # already present: this tree keeps each key once
def height_and_depths(root):
"""Height in edges and the average depth of a node (the comparisons a search for a stored key makes, minus one)."""
total, height, level, depth = 0, -1, [root], 0
while level:
height += 1
total += depth * len(level)
level = [c for n in level for c in (n.left, n.right) if c]
depth += 1
return height, total
N = 4_096
rng = random.Random(4096)
keys = list(range(N))
shuffled = keys[:]
rng.shuffle(shuffled)
for label, order in (("random order", shuffled), ("sorted order", keys)):
root = None
for k in order:
root = insert(root, k)
h, total = height_and_depths(root)
print(f"{N:,} keys in {label}: height {h:,}, average depth {total / N:,.2f}")
heights, depths = [], []
for _ in range(200):
rng.shuffle(shuffled)
root = None
for k in shuffled:
root = insert(root, k)
h, total = height_and_depths(root)
heights.append(h)
depths.append(total / N)
print(f"200 random orders: height from {min(heights)} to {max(heights)}, mean {sum(heights) / 200:.2f};"
f" average depth {sum(depths) / 200:.2f}")
print(f"for comparison: floor(log2 {N:,}) = {N.bit_length() - 1}, the lowest possible height; {N - 1:,}, the highest") Output
4,096 keys in random order: height 23, average depth 13.09 4,096 keys in sorted order: height 4,095, average depth 2,047.50 200 random orders: height from 23 to 36, mean 26.61; average depth 13.82 for comparison: floor(log2 4,096) = 12, the lowest possible height; 4,095, the highest
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 heights.py
The same 4,096 keys built a tree of height 23 in random order and 4,095 in sorted order, where every key became the right child of the one before. Over 200 random orders the height ranged from 23 to 36, with a mean of 26.61, while the lowest possible height is 12. The average depth decides what a search for a stored key costs: about 13.82 for random orders against 2,047.5 for sorted input, so the same lookup took about 15 comparisons in one tree and about 2,048 in the other.
| Operation | Time | Extra space | Note |
|---|---|---|---|
| search, insert, delete | O(h) | O(1) walking, O(h) recursing | h is about log2 n when bushy, n − 1 for a path |
| minimum, maximum, successor | O(h) | O(1) | walk left, walk right, or climb |
| inorder listing | O(n) | O(h) | the keys in sorted order |
Built on the same invariant
Validating a tree passes a lower and an upper bound down from the root, because comparing each node only with its
children misses bad trees. Floor and ceiling find the nearest key below or above a value in one walk. The k-th
smallest key comes from an inorder walk that stops early. Balanced trees such as AVL and red-black trees keep the
height near log2 n, and Java’s TreeMap is a red-black tree.
Exercise: a search tree of your own
The exercise asks for insert, delete, contains and inorder, checked on each deletion case and on 3,000 random operations against a plain set.
Exercise · Medium · Python, JavaScript
A binary search tree with insert, delete, contains and inorder
Complete the class SearchTree, a binary search tree of distinct integer keys built from Node objects (key, left, right). The tree starts empty (root is None; JavaScript: null).
insert(key)adds the key; inserting a key that is already there changes nothing.contains(key)returns whether the key is in the tree.delete(key)removes the key if it is there and does nothing otherwise. A node with two children takes the key of its inorder successor, the smallest key in its right subtree, and that successor's node is removed instead.inorder()returns all keys in increasing order, as a list (JavaScript: an array).
The tests check each deletion case on the lesson's tree, deletion of the root, and 3,000 random operations against a plain set. They also read root to check that a two-child deletion used the successor.
Python · Starter code · search_tree.py
class Node:
def __init__(self, key):
self.key, self.left, self.right = key, None, None
class SearchTree:
"""A binary search tree of distinct keys."""
def __init__(self):
self.root = None
def insert(self, key):
raise NotImplementedError("write insert")
def contains(self, key):
raise NotImplementedError("write contains")
def delete(self, key):
raise NotImplementedError("write delete")
def inorder(self):
raise NotImplementedError("write inorder") The sample tests · test_search_tree.py
import random
from search_tree import SearchTree
def lesson_tree():
t = SearchTree()
for k in (50, 30, 70, 20, 40, 60, 80, 55, 65):
t.insert(k)
return t
def test_insert_and_inorder():
"""inorder lists the keys in increasing order, and a repeated insert changes nothing"""
t = lesson_tree()
t.insert(40)
assert t.inorder() == [20, 30, 40, 50, 55, 60, 65, 70, 80]
assert t.root.key == 50 and t.root.left.key == 30 and t.root.right.left.key == 60
def test_contains():
"""contains finds stored keys and rejects others"""
t = lesson_tree()
assert all(t.contains(k) for k in (50, 20, 65, 80))
assert not any(t.contains(k) for k in (0, 58, 100))
assert not SearchTree().contains(1)
def test_delete_leaf_and_one_child():
"""deleting a leaf, then a node with one child"""
t = lesson_tree()
t.delete(20)
assert t.inorder() == [30, 40, 50, 55, 60, 65, 70, 80] and t.root.left.left is None
t.delete(30)
assert t.root.left.key == 40
def test_delete_two_children_uses_successor():
"""deleting 70 moves its successor 80 up; deleting the root 50 moves 55 up"""
t = lesson_tree()
t.delete(70)
assert t.root.right.key == 80 and t.inorder() == [20, 30, 40, 50, 55, 60, 65, 80]
t.delete(50)
assert t.root.key == 55 and t.inorder() == [20, 30, 40, 55, 60, 65, 80]
def test_delete_missing_key_and_emptying():
"""deleting a missing key does nothing; deleting every key leaves an empty tree"""
t = lesson_tree()
t.delete(58)
assert t.inorder() == [20, 30, 40, 50, 55, 60, 65, 70, 80]
for k in (50, 30, 70, 20, 40, 60, 80, 55, 65):
t.delete(k)
assert t.root is None and t.inorder() == []
def test_random_operations():
"""3,000 random inserts, deletes and searches agree with a plain set"""
rng = random.Random(5)
t, reference = SearchTree(), set()
for _ in range(3_000):
key = rng.randrange(400)
op = rng.choice(("insert", "insert", "delete", "contains"))
if op == "insert":
t.insert(key)
reference.add(key)
elif op == "delete":
t.delete(key)
reference.discard(key)
else:
assert t.contains(key) == (key in reference), f"contains({key})"
assert t.inorder() == sorted(reference) JavaScript · Starter code · search_tree.mjs
export class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
/** A binary search tree of distinct keys. */
export class SearchTree {
constructor() {
this.root = null;
}
insert(key) {
throw new Error('write insert');
}
contains(key) {
throw new Error('write contains');
}
delete(key) {
throw new Error('write delete');
}
inorder() {
throw new Error('write inorder');
}
} The sample tests · search_tree.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { SearchTree } from './search_tree.mjs';
function lessonTree() {
const t = new SearchTree();
for (const k of [50, 30, 70, 20, 40, 60, 80, 55, 65]) t.insert(k);
return t;
}
test('inorder lists the keys in increasing order, and a repeated insert changes nothing', () => {
const t = lessonTree();
t.insert(40);
assert.deepEqual(t.inorder(), [20, 30, 40, 50, 55, 60, 65, 70, 80]);
assert.ok(t.root.key === 50 && t.root.left.key === 30 && t.root.right.left.key === 60);
});
test('contains finds stored keys and rejects others', () => {
const t = lessonTree();
assert.ok([50, 20, 65, 80].every((k) => t.contains(k)));
assert.ok(![0, 58, 100].some((k) => t.contains(k)));
assert.equal(new SearchTree().contains(1), false);
});
test('deleting a leaf, then a node with one child', () => {
const t = lessonTree();
t.delete(20);
assert.deepEqual(t.inorder(), [30, 40, 50, 55, 60, 65, 70, 80]);
assert.equal(t.root.left.left, null);
t.delete(30);
assert.equal(t.root.left.key, 40);
});
test('deleting 70 moves its successor 80 up; deleting the root 50 moves 55 up', () => {
const t = lessonTree();
t.delete(70);
assert.equal(t.root.right.key, 80);
assert.deepEqual(t.inorder(), [20, 30, 40, 50, 55, 60, 65, 80]);
t.delete(50);
assert.equal(t.root.key, 55);
assert.deepEqual(t.inorder(), [20, 30, 40, 55, 60, 65, 80]);
});
test('deleting a missing key does nothing; deleting every key leaves an empty tree', () => {
const t = lessonTree();
t.delete(58);
assert.deepEqual(t.inorder(), [20, 30, 40, 50, 55, 60, 65, 70, 80]);
for (const k of [50, 30, 70, 20, 40, 60, 80, 55, 65]) t.delete(k);
assert.equal(t.root, null);
assert.deepEqual(t.inorder(), []);
});
test('3,000 random inserts, deletes and searches agree with a plain set', () => {
let s = 5;
const next = (m) => { s = (s * 1103515245 + 12345) % 2147483648; return s % m; };
const t = new SearchTree();
const reference = new Set();
const ops = ['insert', 'insert', 'delete', 'contains'];
for (let i = 0; i < 3000; i++) {
const key = next(400);
const op = ops[next(4)];
if (op === 'insert') { t.insert(key); reference.add(key); }
else if (op === 'delete') { t.delete(key); reference.delete(key); }
else assert.equal(t.contains(key), reference.has(key), `contains(${key})`);
}
assert.deepEqual(t.inorder(), [...reference].sort((a, b) => a - b));
}); A hint
Write delete as a helper that takes a subtree and returns the subtree after the deletion, then set self.root = self._delete(self.root, key). In the helper, walk left or right until you reach the key. With no left child, return the right child; with no right child, return the left one; with both, find the leftmost node of the right subtree, copy its key into the node, and delete that key from the right subtree.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Validating a search tree and answering ordered queries, with the classic bug reproduced Pro: opens with a Pro, Premium or Ultimate pass
The most common search-tree bug is a validator that compares each node only with its children. This part builds a tree that such a validator accepts, fixes the check with bounds and proves it is enough, measures how often the bug lets damaged trees through, and checks floor, ceiling and k-th smallest against a sorted list.
In this part:
- The children-only check, and a tree it accepts wrongly
- Bounds passed down the tree: the fix, and why it is enough
- How often the bug slips through, measured on 2,000 damaged trees
- Floor, ceiling and the k-th smallest key, checked against a sorted list
1 runnable example (Python) · about 2 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Deletion proved correct, stress-tested on 100,000 operations, and duplicate keys Pro: opens with a Pro, Premium or Ultimate pass
Deletion is where search trees break most often, because it is the one operation that moves keys. This part proves that the successor rule keeps the invariant, replays 100,000 random operations against a set while checking the invariant, and reproduces how duplicate keys stored as separate nodes get lost after a deletion.
In this part:
- Why the successor keeps the invariant: the proof
- 100,000 random operations against a set
- Duplicate keys: the “equal goes left” rule broken by a deletion
2 runnable examples (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Balanced builds, the library ordered maps, and the search tree in five languages Pro: opens with a Pro, Premium or Ultimate pass
Sorted input does not have to build a path, and most languages already ship an ordered map that never does. This part builds a balanced tree from sorted keys in O(n), compares the ordered structures of five standard libraries, and runs the lesson’s tree in JavaScript, Java, C++ and Go with floor and ceiling from each library.
In this part:
- Sorted keys built middle-first: the lowest possible height
- What the standard libraries offer
- When a hand-written search tree is still the right choice
- The lesson’s tree in JavaScript, Java, C++ and Go
3 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher · Warm-up
How do you delete a node with two children from a binary search tree, and why does its replacement never have two children itself?
Copy the key of the node's inorder successor into it, then delete the successor's node instead. The successor is the smallest key in the right subtree: go right once, then left until there is no left child. It is the right replacement because it is the next key in sorted order, larger than every key on the left and smaller than every other key on the right, so the ordering rule still holds once it moves up. It never has two children, because a left child would hold an even smaller key of the same subtree and would be the successor instead. So removing it is one of the easy cases: it is a leaf, or its right child takes its place.
-
Fresher to mid
A teammate's validator checks that every node's left child is smaller and its right child larger, and it passes the team's unit tests. Construct a tree it wrongly accepts, fix the check, and explain why hand-written tests rarely catch the bug.
-
Mid to senior
User records are inserted into a plain binary search tree keyed by an auto-increment ID. After a million sign-ups, lookups take milliseconds instead of microseconds. Explain what the tree looks like, put numbers on it, and give two fixes.
-
Mid to senior
Two binary search trees must become one: a balanced tree of 1 million keys and a balanced tree of 1,000 keys. Compare inserting the small tree's keys one by one with walking both trees in order and rebuilding, with costs, and say which you would choose.
Key takeaways
- Every key in a node’s left subtree is smaller and every key in its right subtree larger; inorder therefore lists the keys in sorted order.
- Search and insert are one walk from the root, and insertion fills the empty place where the search ended.
- Delete a leaf by removing it, a one-child node by lifting its child, and a two-child node by copying in its inorder successor and deleting that instead.
- Every operation costs O(h): 4,096 random keys gave height 23, the same keys in sorted order 4,095.
References
- Dictionary of Algorithms and Data Structures: binary search tree (National Institute of Standards and Technology (NIST))
- GATE syllabus for Computer Science and Information Technology (CS) (Indian Institute of Technology Madras (GATE organising institute))
- Class TreeMap (Java SE 25 API) (Oracle)
- std::set::lower_bound (cppreference.com)
- Package slices: BinarySearch (The Go Authors)
- bisect: array bisection algorithm (Python Software Foundation)
- Dictionary of Algorithms and Data Structures: Dictionary home (AVL tree, red-black tree) (National Institute of Standards and Technology (NIST))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress