Data Structures & Algorithms (DSA) Module 9 – Trees and binary search trees
Red-black trees and B-trees (concepts)
The red-black rules that cap a tree at 2 log₂(n + 1) levels, their link to 2-3-4 trees, and why databases store indexes as B-trees of pages.
What you will learn
- State the red-black rules and the height bound they guarantee
- Map a red-black tree to the 2-3-4 tree it encodes
- Explain why databases store their indexes in B-trees and B+ trees
- Validate a coloured binary tree against every red-black rule
Before you start
What you’ll learn · Before you start
What you will learn
- State the red-black rules and the height bound they guarantee
- Map a red-black tree to the 2-3-4 tree it encodes
- Explain why databases store their indexes in B-trees and B+ trees
- Validate a coloured binary tree against every red-black rule
Before you start
On this page
Red-black trees and B-trees are the balanced search trees that most real software runs on. A red-black tree is a
binary search tree whose nodes carry one extra bit, a colour, and whose colours obey rules that keep every path from
the root within a factor of two of every other: its height is at most 2 log₂(n + 1). It accepts a less even shape than
an AVL tree in exchange for fewer rotations, at most two per insertion and three per deletion, and it sits inside
Java’s TreeMap and most C++ std::map implementations. B-trees keep hundreds of keys in each node, sized to a disk
or memory page, so a billion keys are only four or five levels deep; databases and file systems keep their indexes
in them.
The five red-black rules
- Every node is red or black.
- The root is black.
- Every empty subtree counts as a black leaf.
- A red node has no red child.
- Every path from a node down to an empty subtree passes the same number of black nodes.
Rule 5 makes the black nodes alone a perfectly balanced skeleton, and rule 4 lets at most one red node sit between two black ones, so no path from the root can be more than twice as long as another. That is where the bound 2 log₂(n + 1) comes from. Leo Guibas and Robert Sedgewick described the colouring in 1978.
Red-black trees are 2-3-4 trees in disguise
Merge every red node into its black parent and each group becomes one node of a 2-3-4 tree: a black node with no red child holds one key, with one red child two keys, with two red children three keys. Every path passes the same number of black nodes, so every leaf of that 2-3-4 tree sits at the same depth.
Each black node and its red children form one node of a 2-3-4 tree
Text description of the diagram
The diagram shows the red-black tree built in the lesson's run and the 2-3-4 tree it stands for, from top to bottom. Red nodes are drawn as circles marked R, black nodes as squares marked B.
- The red-black tree has the black root 14. Its children are 10 and 20, both red. Below 10 are the black nodes 5 and 12; 5 has the red child 1 and 12 has the red child 13. Below 20 are the black nodes 15 and 30; 30 has the red child 25.
- Every path from the root down to an empty subtree passes exactly two black nodes, and no red node has a red child.
- Merging each red node into its black parent gives the 2-3-4 tree: the root holds 10, 14 and 20 and has four children, the nodes 1 and 5, 12 and 13, 15 alone, and 25 and 30. Every leaf of the 2-3-4 tree is at the same depth, which is why the red-black tree stays balanced.
Insertion follows the same picture. A new key arrives red, which leaves every black count unchanged, so the only rule it can break is rule 4. If the red parent’s sibling, the uncle, is red too, the 2-3-4 node is full: recolouring splits it and moves the problem two levels up. If the uncle is black, one or two rotations rearrange the keys of that node, and the repair is over.
Inserting ten keys, traced
"""A red-black tree: insertion with each repair printed, a check of the rules, and the 2-3-4 tree it stands for."""
RED, BLACK = "red", "black"
class Node:
__slots__ = ("key", "color", "left", "right", "parent")
def __init__(self, key, color, nil):
self.key, self.color = key, color
self.left = self.right = self.parent = nil
class RedBlackTree:
def __init__(self):
self.nil = Node(None, BLACK, None) # one shared black leaf stands for every empty subtree
self.nil.left = self.nil.right = self.nil.parent = self.nil
self.root = self.nil
self.rotations = 0
self.log = []
def rotate_left(self, x):
y = x.right
x.right = y.left
if y.left is not self.nil:
y.left.parent = x
self._replace(x, y)
y.left, x.parent = x, y
self.rotations += 1
def rotate_right(self, y):
x = y.left
y.left = x.right
if x.right is not self.nil:
x.right.parent = y
self._replace(y, x)
x.right, y.parent = y, x
self.rotations += 1
def _replace(self, old, new):
"""Puts new where old hangs from its parent."""
new.parent = old.parent
if old.parent is self.nil:
self.root = new
elif old is old.parent.left:
old.parent.left = new
else:
old.parent.right = new
def insert(self, key):
parent, node = self.nil, self.root
while node is not self.nil:
if key == node.key:
return # keys are unique
parent, node = node, (node.left if key < node.key else node.right)
z = Node(key, RED, self.nil) # a new node is red, so no path gains a black node
z.parent = parent
if parent is self.nil:
self.root = z
elif key < parent.key:
parent.left = z
else:
parent.right = z
self._repair(z)
def _repair(self, z):
while z.parent.color == RED: # two reds in a row: the only rule an insertion can break
p, g = z.parent, z.parent.parent
uncle = g.right if p is g.left else g.left
if uncle.color == RED: # recolour and move the problem two levels up
p.color = uncle.color = BLACK
g.color = RED
self.log.append(f"red uncle at {g.key}: recolour")
z = g
continue
if p is g.left:
if z is p.right: # zig-zag: rotate it into a straight line first
self.rotate_left(p)
z, p = p, z
self.log.append(f"black uncle, zig-zag at {g.key}: two rotations")
else:
self.log.append(f"black uncle, line at {g.key}: one rotation")
self.rotate_right(g)
else:
if z is p.left:
self.rotate_right(p)
z, p = p, z
self.log.append(f"black uncle, zig-zag at {g.key}: two rotations")
else:
self.log.append(f"black uncle, line at {g.key}: one rotation")
self.rotate_left(g)
p.color, g.color = BLACK, RED # the middle key goes up black, the old grandparent becomes red
if self.root.color == RED:
self.root.color = BLACK
self.log.append("root painted black")
def check(self):
"""Black nodes on every path from the root to an empty leaf; fails on the first broken rule."""
assert self.root.color == BLACK, "the root is red"
def walk(n, low, high):
if n is self.nil:
return 0
assert low < n.key < high, f"{n.key} is out of order"
if n.color == RED:
assert n.left.color == BLACK and n.right.color == BLACK, f"red {n.key} has a red child"
left, right = walk(n.left, low, n.key), walk(n.right, n.key, high)
assert left == right, f"paths below {n.key} hold {left} and {right} black nodes"
return left + (n.color == BLACK)
return walk(self.root, float("-inf"), float("inf"))
def height(self, n=None):
n = self.root if n is None else n
return -1 if n is self.nil else 1 + max(self.height(n.left), self.height(n.right))
def sideways(self, n=None, depth=0, out=None):
n, out = (self.root if n is None else n), ([] if out is None else out)
if n is not self.nil:
self.sideways(n.right, depth + 1, out)
out.append(" " * depth + f"{n.key} {n.color}")
self.sideways(n.left, depth + 1, out)
return out
def as_234(self):
"""The 2-3-4 tree level by level: each black node merged with its red children into one node."""
levels, level = [], [self.root] if self.root is not self.nil else []
while level:
row, below = [], []
for b in level:
keys = []
for c in (b.left, b, b.right):
if c is b:
keys.append(b.key)
elif c.color == RED:
keys.append(c.key)
below += [c.left, c.right]
else:
below.append(c)
row.append("[" + " ".join(map(str, keys)) + "]")
levels.append(" ".join(row))
level = [x for x in below if x is not self.nil]
return levels
if __name__ == "__main__":
t = RedBlackTree()
for key in (10, 20, 30, 15, 25, 5, 1, 12, 14, 13):
t.log.clear()
t.insert(key)
t.check() # every rule holds after every insertion
print(f"insert {key:2}: {'; '.join(t.log) or 'parent is black: nothing to do'}")
print(f"10 keys: height {t.height()}, {t.check()} black nodes on every root-to-leaf path, {t.rotations} rotations")
print("\n".join(t.sideways()))
print("the same keys as a 2-3-4 tree, level by level:")
for row in t.as_234():
print(" ", row) Output
insert 10: root painted black
insert 20: parent is black: nothing to do
insert 30: black uncle, line at 10: one rotation
insert 15: red uncle at 20: recolour; root painted black
insert 25: parent is black: nothing to do
insert 5: parent is black: nothing to do
insert 1: red uncle at 10: recolour
insert 12: parent is black: nothing to do
insert 14: black uncle, zig-zag at 15: two rotations
insert 13: red uncle at 14: recolour; black uncle, zig-zag at 20: two rotations
10 keys: height 3, 2 black nodes on every root-to-leaf path, 5 rotations
30 black
25 red
20 red
15 black
14 black
13 red
12 black
10 red
5 black
1 red
the same keys as a 2-3-4 tree, level by level:
[10 14 20]
[1 5] [12 13] [15] [25 30]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rb.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
Each insertion prints its repair, and a check of the rules runs after every one. Key 13 needed both kinds: a recolouring that moved the problem up, then a double rotation. The ten keys cost 5 rotations and ended at height 3, with two black nodes on every path and the 2-3-4 root [10 14 20] above four leaves, as in the diagram.
B-trees: hundreds of keys per node
A B-tree of order m keeps up to m − 1 sorted keys and m children in each node, every node but the root at least half full, and every leaf at the same depth (Rudolf Bayer and Edward McCreight, 1972). A search reads one node per level and searches the sorted keys inside it. An insertion puts the key into a leaf; a leaf that overflows splits in two and pushes its middle key up into its parent, which may split in turn. When the root splits, a new root appears above it, so the tree grows at the top and its leaves stay level. A B-tree of order 4 is exactly a 2-3-4 tree:
"""A B-tree of order 4 (at most 3 keys and 4 children per node) built from 20 keys, with every split printed."""
import random
class BNode:
def __init__(self, keys=None, children=None):
self.keys = keys or []
self.children = children or [] # empty for a leaf
def insert(node, key, log, order):
"""Inserts key below node; returns (median, new right sibling) when node splits, else None."""
i = sum(k < key for k in node.keys) # the child (or slot) to the right of every smaller key
if i < len(node.keys) and node.keys[i] == key:
return None # keys are unique
if not node.children:
node.keys.insert(i, key)
else:
split = insert(node.children[i], key, log, order)
if split:
median, right = split
node.keys.insert(i, median)
node.children.insert(i + 1, right)
if len(node.keys) < order:
return None
mid = len(node.keys) // 2 # an overfull node holds `order` keys: keep the left half, send the middle key up
median = node.keys[mid]
right = BNode(node.keys[mid + 1 :], node.children[mid + 1 :])
log.append(f"[{' '.join(map(str, node.keys))}] splits, {median} moves up")
node.keys, node.children = node.keys[:mid], node.children[: mid + 1]
return median, right
def add(root, key, log, order=4):
split = insert(root, key, log, order)
if split: # the root itself split: a new root makes the tree one level taller
median, right = split
root = BNode([median], [root, right])
log.append(f"new root [{median}]")
return root
def levels(root):
rows, level = [], [root]
while level:
rows.append(" ".join("[" + " ".join(map(str, n.keys)) + "]" for n in level))
level = [c for n in level for c in n.children]
return rows
def build(keys, order=4):
root = BNode()
for key in keys:
root = add(root, key, [], order)
return root
if __name__ == "__main__":
keys = list(range(1, 21))
random.Random(4).shuffle(keys)
root = BNode()
for key in keys:
log = []
root = add(root, key, log)
if log:
print(f"insert {key:2}: {'; '.join(log)}")
print(f"{len(keys)} keys inserted in the order {keys}")
for depth, row in enumerate(levels(root)):
print(f"level {depth}: {row}") Output
insert 15: [11 15 18 20] splits, 18 moves up; new root [18] insert 17: [6 11 15 17] splits, 15 moves up insert 5: [5 6 11 12] splits, 11 moves up insert 7: [5 6 7 9] splits, 7 moves up; [7 11 15 18] splits, 15 moves up; new root [15] insert 2: [1 2 5 6] splits, 5 moves up insert 4: [1 2 3 4] splits, 3 moves up; [3 5 7 11] splits, 7 moves up 20 keys inserted in the order [18, 20, 11, 15, 6, 19, 17, 12, 5, 9, 7, 1, 14, 2, 3, 16, 13, 4, 10, 8] level 0: [7 15] level 1: [3 5] [11] [18] level 2: [1 2] [4] [6] [8 9 10] [12 13 14] [16 17] [19 20]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 btree.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
Six of the twenty insertions split a node; two of them split all the way up and gave the tree a new root.
Why databases choose B-trees: page reads
On disk, on an SSD and even in memory, a lookup costs roughly one read or cache miss per node it touches. A node sized to a 4 KB page holds about 256 keys and child pointers, so the tree branches 256 ways:
"""How many nodes a lookup visits: a B-tree with 4 KB pages against binary trees, for 10^3 to 10^9 keys."""
import math
PAGE, ENTRY = 4096, 16 # bytes per page; one entry = an 8-byte key and an 8-byte page number
fanout = PAGE // ENTRY # children per full node: 256
def btree_levels(n, children):
"""Levels of a B-tree whose nodes are all full (the best case) and half full (the worst a B-tree allows)."""
return math.ceil(math.log(n + 1, children))
print(f"page {PAGE} bytes, entry {ENTRY} bytes: up to {fanout} children per node, at least {fanout // 2}")
print("nodes one lookup visits, at most:")
print(f"{'keys':>15} {'perfect binary':>15} {'red-black worst':>16} {'B-tree, full':>13} {'B-tree, half full':>18}")
for n in (10**3, 10**6, 10**9):
perfect = math.ceil(math.log2(n + 1))
rb_worst = math.floor(2 * math.log2(n + 1))
print(f"{n:>15,} {perfect:>15} {rb_worst:>16} {btree_levels(n, fanout):>13} {btree_levels(n, fanout // 2):>18}") Output
page 4096 bytes, entry 16 bytes: up to 256 children per node, at least 128
nodes one lookup visits, at most:
keys perfect binary red-black worst B-tree, full B-tree, half full
1,000 10 19 2 2
1,000,000 20 39 3 3
1,000,000,000 30 59 4 5
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 page_reads.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 billion keys fit in 4 levels of full nodes and 5 of half-full ones, where a red-black tree may need 59 nodes on one path. That is why PostgreSQL creates B-tree indexes by default and SQLite stores every table and index as a B-tree.
AVL trees, red-black trees and B-trees compared
| Criterion | AVL tree | Red-black tree | B-tree of order m |
|---|---|---|---|
| Keys per node | 1 | 1 | up to m − 1 |
| Height for n keys | below about 1.44 log₂ n | at most 2 log₂(n + 1) | about log base m/2 of n |
| Repair after an insertion | one single or double rotation | recolourings, at most 2 rotations | splits up the path |
| Typical home | in-memory indexes with many lookups | language libraries: TreeMap, std::map | database and file-system indexes |
| When to choose | Lookups far outnumber updates | General-purpose ordered maps in memory | Data on pages: disk, SSD or cache lines |
Red-black trees are the usual default for ordered maps in memory, and B-trees win whenever reading a node is the expensive step.
Exercise
Exercise · Medium · Python, JavaScript
Check every red-black rule and return the black-height
A coloured binary tree is made of Node(key, color, left, right) objects, where color is "red" or "black" and an empty subtree is None (JavaScript: null), which counts as a black leaf. Write black_height(root) (JavaScript: blackHeight(root)). For a valid red-black tree it returns the number of black nodes on every path from the root down to an empty subtree (an empty tree has 0). Otherwise it raises ValueError (JavaScript: throws an Error) whose message is the name of the rule that is broken:
"colour": a node's colour is neither"red"nor"black";"root": the root is red;"order": a key is not strictly between the keys above it that bound it, so the tree is not a search tree;"red-red": a red node has a red child;"black-height": two paths from some node down to empty subtrees hold different numbers of black nodes.
The tests break one rule at a time, so your checks may run in any order. The function must take O(n) time: one pass over the tree, with no recomputation of a subtree's black count.
Python · Starter code · rb_check.py
class Node:
def __init__(self, key, color, left=None, right=None):
self.key = key
self.color = color # "red" or "black"
self.left = left
self.right = right
def black_height(root):
"""Black nodes on every root-to-empty path of a valid red-black tree; raises ValueError naming a broken rule."""
raise NotImplementedError("write black_height") The sample tests · test_rb_check.py
from rb_check import Node, black_height
def B(key, left=None, right=None):
return Node(key, "black", left, right)
def R(key, left=None, right=None):
return Node(key, "red", left, right)
def broken_rule(root):
try:
black_height(root)
except ValueError as e:
return str(e)
return None
def perfect_black(lo, hi):
"""A perfect all-black tree on the keys lo..hi (a valid red-black tree)."""
if lo > hi:
return None
mid = (lo + hi) // 2
return B(mid, perfect_black(lo, mid - 1), perfect_black(mid + 1, hi))
def test_small_valid_trees():
"""an empty tree has black-height 0, a black root 1, and the 10-key tree of the lesson 2"""
assert black_height(None) == 0
assert black_height(B(7)) == 1
lesson = B(14, R(10, B(5, R(1)), B(12, None, R(13))), R(20, B(15), B(30, R(25))))
assert black_height(lesson) == 2
def test_large_valid_tree():
"""a perfect black tree of 1,023 keys has black-height 10"""
assert black_height(perfect_black(1, 1023)) == 10
def test_root_and_colour():
"""a red root breaks "root", and a colour other than red or black breaks "colour\""""
assert broken_rule(R(7)) == "root"
assert broken_rule(B(7, Node(3, "pink"))) == "colour"
def test_order_uses_bounds():
"""12 below the left child of 10 breaks "order", even though it is larger than its own parent 5"""
assert broken_rule(B(10, B(5, None, R(3)), B(15))) == "order"
assert broken_rule(B(10, B(5, None, R(12)), B(15))) == "order"
def test_red_red_and_black_height():
"""two reds in a row break "red-red"; an extra black node on one side breaks "black-height\""""
assert broken_rule(B(10, R(5, R(3)), R(15))) == "red-red"
assert broken_rule(B(10, B(5, B(3)), B(15))) == "black-height" JavaScript · Starter code · rb_check.mjs
export class Node {
constructor(key, color, left = null, right = null) {
this.key = key;
this.color = color; // "red" or "black"
this.left = left;
this.right = right;
}
}
/** Black nodes on every root-to-empty path of a valid red-black tree; throws an Error naming a broken rule. */
export function blackHeight(root) {
throw new Error('write blackHeight');
} The sample tests · rb_check.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { Node, blackHeight } from './rb_check.mjs';
const B = (key, left = null, right = null) => new Node(key, 'black', left, right);
const R = (key, left = null, right = null) => new Node(key, 'red', left, right);
function brokenRule(root) {
try {
blackHeight(root);
} catch (e) {
return e.message;
}
return null;
}
function perfectBlack(lo, hi) {
if (lo > hi) return null;
const mid = Math.floor((lo + hi) / 2);
return B(mid, perfectBlack(lo, mid - 1), perfectBlack(mid + 1, hi));
}
test('an empty tree has black-height 0, a black root 1, and the 10-key tree of the lesson 2', () => {
assert.equal(blackHeight(null), 0);
assert.equal(blackHeight(B(7)), 1);
const lesson = B(14, R(10, B(5, R(1)), B(12, null, R(13))), R(20, B(15), B(30, R(25))));
assert.equal(blackHeight(lesson), 2);
});
test('a perfect black tree of 1,023 keys has black-height 10', () => {
assert.equal(blackHeight(perfectBlack(1, 1023)), 10);
});
test('a red root breaks "root", and a colour other than red or black breaks "colour"', () => {
assert.equal(brokenRule(R(7)), 'root');
assert.equal(brokenRule(B(7, new Node(3, 'pink'))), 'colour');
});
test('12 below the left child of 10 breaks "order", even though it is larger than its own parent 5', () => {
assert.equal(brokenRule(B(10, B(5, null, R(3)), B(15))), 'order');
assert.equal(brokenRule(B(10, B(5, null, R(12)), B(15))), 'order');
});
test('two reds in a row break "red-red"; an extra black node on one side breaks "black-height"', () => {
assert.equal(brokenRule(B(10, R(5, R(3)), R(15))), 'red-red');
assert.equal(brokenRule(B(10, B(5, B(3)), B(15))), 'black-height');
}); A hint
Write one recursive helper that takes a node and the open interval (low, high) its key must lie in, and returns the black count of its subtree: 0 for None, and otherwise the common count of its two children plus one if the node is black. Raise "black-height" as soon as the two children's counts differ, and pass (low, key) to the left child and (key, high) to the right one.
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.
The height proof and the deletion cases, worked and stress-tested Pro: opens with a Pro, Premium or Ultimate pass
The 2 log₂(n + 1) bound follows from counting black nodes, and deletion is where red-black code gets hard. This part proves the bound, maps every deletion case to a borrow or a merge in the 2-3-4 tree, and checks the code on 50,000 random operations.
In this part:
- Why black nodes bound the height: the proof
- Deletion: the four repair cases as borrows and merges
1 runnable example (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).
Red-black trees in five languages, and against AVL trees, measured Pro: opens with a Pro, Premium or Ultimate pass
The insertion code is the same in every language; what changes is how nodes are owned and how the empty leaf is represented. This part runs it in JavaScript, Java, C++ and Go, then measures red-black and AVL trees on the same keys.
In this part:
- The same insertion in JavaScript, Java, C++ and Go
- AVL or red-black: heights, depths and rotations measured
- What the libraries promise
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).
B-tree deletion, B+ tree fan-out and SQLite's B-trees, measured Pro: opens with a Pro, Premium or Ultimate pass
Deletion mirrors insertion: a node that falls below half full borrows a key from a sibling or merges with one, and the tree can lose a level at the root. This part works through deletion, puts numbers on B+ trees, and measures SQLite’s own B-trees for a million rows.
In this part:
- Deletion: borrow, merge, and a root that disappears
- B+ trees: records in the leaves, worked numbers
- SQLite’s B-trees for a million rows, measured
- PostgreSQL’s default index
3 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).
Interview questions
-
Fresher · Warm-up
What does the red-black colouring guarantee about a tree's height, and why?
A red-black tree with n keys is at most 2 log₂(n + 1) nodes tall, so search, insertion and deletion take O(log n) time in the worst case. Two rules give the guarantee. Every path from a node down to an empty subtree passes the same number of black nodes, so the black nodes alone form a perfectly balanced skeleton. And a red node never has a red child, so at most every second node on a path is red. Together they mean that the longest path from the root is at most twice as long as the shortest one. The tree is therefore less evenly balanced than an AVL tree, but it needs fewer rotations to stay that way: at most two per insertion and three per deletion.
-
Mid to senior
An in-memory index of one million keys serves 50 lookups for every insert or delete. A colleague insists on an AVL tree because it is shorter than a red-black tree. How much shorter is it, does that matter for lookups, and which would you choose?
-
Mid to senior
A table holds one billion rows with 8-byte keys, indexed by a B-tree on 8 KB pages. Estimate the index's height, the page reads for a point lookup and for a range of 10,000 consecutive keys, and compare with a red-black tree stored on disk.
-
Mid to senior
A Java service swapped HashMap for TreeMap to get its keys in sorted order, and p99 latency rose. With one million keys, estimate how much more work a lookup now does, and propose ways to keep sorted iteration without paying for it on every request.
Key takeaways
- A red-black tree balances only its black nodes and allows at most one red between two blacks, so its height is at most 2 log₂(n + 1).
- Merging each red node into its black parent turns a red-black tree into a 2-3-4 tree with every leaf at one depth.
- B-trees put hundreds of keys in a node sized to a page, so a lookup reads only three to five nodes.
References
- A dichromatic framework for balanced trees (L. J. Guibas and R. Sedgewick, 19th Annual Symposium on Foundations of Computer Science, 1978) (IEEE)
- Organization and maintenance of large ordered indexes (R. Bayer and E. McCreight, Acta Informatica 1, 1972) (Springer)
- Red-black tree (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- B-tree (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- Class TreeMap (Java SE 25 API) (Oracle)
- std::map (cppreference.com)
- Red-black Trees (rbtree) in Linux (The Linux Kernel documentation)
- BTreeMap in std::collections (The Rust Project)
- Database File Format: B-tree pages (SQLite)
- The DBSTAT Virtual Table (SQLite)
- PostgreSQL 18 documentation: Index Types (The PostgreSQL Global Development Group)
- PostgreSQL 18 documentation: Database Page Layout (The PostgreSQL Global Development Group)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress