Chapter 10: Search Trees

Data Structures
Binary search treesSearch and insertionDeletionO(h) analysis Balance and rotationAVL treesRed-black treesSplay treesB-trees and B+ trees
Sercan KÜLCÜ · Giresun University · Computer Engineering

Introduction

Learning Outcomes

BST operations

Carry out search, insertion, and deletion in a binary search tree.

Why balance

Explain how the height of a tree drives the cost of its operations.

Rebalancing

Read AVL rotations and apply the red-black tree rules.

Comparison

Choose among BSTs, AVL, red-black, splay, and B-trees for a given use.

Search Trees

Search Trees

  • Each node holds an item, and each item has a key.
  • Items are stored in order of their keys.
  • A search starts at the root and follows a single branch at each node.
  • The cost is proportional to the height h of the tree: O(h).
  • With n nodes, h is ≈ log2 n at best and n − 1 at worst.
6 2 1 4 3 8depth 0depth 1depth 2depth 3
The whole story of search trees: operations take O(h), so keeping h small (a balanced tree) makes them O(log n).

Search Trees

Basic Operations

SearchReturns true if the key is in the tree, false otherwise.search(root, 5) → true
InsertionAdds a new item as a leaf in the right place, preserving the order.insert(root, 7)
DeletionRemoves the item with the given key and keeps the order intact.delete(root, 40)
TraversalVisits the items in a fixed order; inorder traversal yields the keys sorted.inorder(root) → 1 2 3 4 6 8

Also: the smallest / largest key, a key's successor / predecessor, and range queries. All O(h).

Search Trees

Kinds of Search Trees

Binary search tree (BST)Each key is greater than every key in its left subtree and less than every key in its right subtree. No balance guarantee.63148
AVL treeAt every node the subtree heights differ by at most 1 (the balance factor). Rotations keep it balanced.40201030605070
Red-black treeNode colors and a few rules cap the height at 2·log2(n+1).201051530
Splay treeRotates each accessed node up to the root; O(log n) amortized per operation.53148← last accessed

Search Trees

Binary Search Trees

For every node x: all keys in the left subtree < x.key < all keys in the right subtree.

6 2 1 4 3 8 6 2 1 4 3 7 8✗
  • The property covers the whole subtree, not just the children: on the right, 7 > 4 holds, but 7 cannot sit left of 6.
  • Duplicate keys are rejected or always sent to one side (say, the right).

Search Trees

Minimum, Maximum, and Sorted Traversal

6 2 1 4 3 8min: always leftmax: always right
An inorder traversal (left → root → right) yields the keys in sorted order:
1 2 3 4 6 8
// Precondition: node != null
BinaryTreeNode<E> minValue(
        BinaryTreeNode<E> node) {
    while (node.leftChild != null)
        node = node.leftChild; // go left
    return node;  // the smallest
}
// maxValue: the same, with rightChild

void inorder(BinaryTreeNode<E> d) {
    if (d == null) return;
    inorder(d.leftChild);   // left
    System.out.print(d.data + " ");
    inorder(d.rightChild);  // right
}

Search Trees · Step through the code

Code Trace: minValue and maxValue

BinaryTreeNode<E> minValue(
        BinaryTreeNode<E> node) {
    while (node.leftChild != null)
        node = node.leftChild;  // go left
    return node;   // the smallest
}
BinaryTreeNode<E> maxValue(
        BinaryTreeNode<E> node) {
    while (node.rightChild != null)
        node = node.rightChild; // go right
    return node;   // the largest
}
6node21438

minValue(root): node starts at the root (6). Precondition: the tree is not empty.

6node21438

node.leftChild = 2 ≠ null → keep looping.

62node1438

node = node.leftChild → 2. Moving left.

62node1438

node.leftChild = 1 ≠ null → keep looping.

621node438

node = node.leftChild → 1. Moving left.

621node438

node.leftChild == null → the loop ends. 1 has no left child.

621node438

return 1: the smallest key. (3 is deeper but lies left of 4; the minimum is always the leftmost node.)

6node21438

maxValue(root): node starts at the root (6). Precondition: the tree is not empty.

6node21438

node.rightChild = 8 ≠ null → keep looping.

621438node

node = node.rightChild → 8. Moving right.

621438node

node.rightChild == null → the loop ends. 8 has no right child.

621438node

return 8: the largest key. No comparisons, just links followed: O(h).

Search Trees · Step through the code

Code Trace: inorder(root)

6d21438

inorder(6): d ≠ null. Left subtree first: inorder(2); 6 waits.

62d1438

inorder(2): d ≠ null. Left subtree first: inorder(1); 2 waits.

621d438

inorder(1): no left child; inorder(null) returns at line 2.

621d438

Left side done → print 1. (Every key on its left is smaller.)

621d438

No right child → the frame closes; inorder(2) resumes.

62d1438

Left side done → print 2. (Every key on its left is smaller.)

62d1438

Now the right subtree: inorder(4).

6214d38

inorder(4): d ≠ null. Left subtree first: inorder(3); 4 waits.

62143d8

inorder(3): no left child; inorder(null) returns at line 2.

62143d8

Left side done → print 3. (Every key on its left is smaller.)

62143d8

No right child → the frame closes; inorder(4) resumes.

6214d38

Left side done → print 4. (Every key on its left is smaller.)

6214d38

No right child → the frame closes; inorder(2) resumes.

62d1438

2's right subtree is done too → inorder(2) closes, and its caller resumes.

6d21438

Left side done → print 6. (Every key on its left is smaller.)

6d21438

Now the right subtree: inorder(8).

621438d

inorder(8): no left child; inorder(null) returns at line 2.

621438d

Left side done → print 8. (Every key on its left is smaller.)

621438d

No right child → the frame closes; inorder(6) resumes.

621438

All calls done; the output is sorted: 1 2 3 4 6 8. Each node printed once → O(n).

void inorder(BinaryTreeNode<E> d) {
    if (d == null) return;
    inorder(d.leftChild);   // left
    System.out.print(d.data + " ");
    inorder(d.rightChild);  // right
}

Search Trees

The Tree Node

leftdatarightleft childright child
int keys
class TreeNode {
    int data;
    TreeNode left, right;

    TreeNode(int data) {
        this.data = data;
        this.left = null;
        this.right = null;
    }
}
generic
class BinaryTreeNode<E extends Comparable<E>> {
    E data;                    // the object
    BinaryTreeNode<E> leftChild;  // left child
    BinaryTreeNode<E> rightChild; // right child

    BinaryTreeNode(E data) {
        this.data = data;
        // children start out null
    }
}

Keys are compared with compareTo, so E must be Comparable<E>.

BST Operations

Searching for a Key

  • Start at the root; if the keys are equal, we are done.
  • If smaller, continue in the left subtree; if larger, in the right.
  • Reaching null means the key is not in the tree.
6 4 2 5 8 7 95 < 65 > 4
boolean search(BinaryTreeNode<E> root, E data) {
    if (root == null)
        return false;            // not found
    int k = data.compareTo(root.data);
    if (k == 0)
        return true;             // found
    else if (k < 0)
        return search(root.leftChild, data);
    else
        return search(root.rightChild, data);
}
Compare objects with compareTo (or equals), not ==, which only checks whether two references are identical.

BST Operations · Step through the code

Code Trace: search(root, 5) and search(root, 10)

boolean search(BinaryTreeNode<E> root, E data) {
    if (root == null)
        return false;            // not found
    int k = data.compareTo(root.data);
    if (k == 0)
        return true;             // found
    else if (k < 0)
        return search(root.leftChild, data);
    else
        return search(root.rightChild, data);
}
6root425879

search(root, 5): root is node 6, not null. k = compareTo = -1 since 5 < 6.

6root425879

k < 0 → go left: search(root.leftChild, 5) opens a new frame (root = 4).

64root25879

root ≠ null. k = compareTo = 1 since 5 > 4.

64root25879

k > 0 → go right: search(root.rightChild, 5) opens a new frame (root = 5).

6425root879

root ≠ null. k = compareTo = 0 since 5 = 5.

6425root879

k == 0 → key found: return true. This frame closes.

64root25879

search(5, 5) returned true; the frame at 4 simply returns it.

6root425879

search(4, 5) returned true; the frame at 6 simply returns it.

6425879

main gets true. 3 nodes compared = depth of the found node + 1.

6root425879

search(root, 10): root is node 6, not null. k = compareTo = 1 since 10 > 6.

6root425879

k > 0 → go right: search(root.rightChild, 10) opens a new frame (root = 8).

64258root79

root ≠ null. k = compareTo = 1 since 10 > 8.

64258root79

k > 0 → go right: search(root.rightChild, 10) opens a new frame (root = 9).

6425879root

root ≠ null. k = compareTo = 1 since 10 > 9.

6425879root

k > 0 → go right: search(root.rightChild, 10) opens a new frame (root = null).

nullroot6425879

root == null: 9 has no right child, so 10 is not in the tree → return false.

6425879root

search(null, 10) returned false; the frame at 9 simply returns it.

64258root79

search(9, 10) returned false; the frame at 8 simply returns it.

6root425879

search(8, 10) returned false; the frame at 6 simply returns it.

6425879

Result: false, after 3 nodes. At most h + 1 nodes → O(h).

BST Operations

Inserting a Key

  • Descend as if searching; put the new leaf in the null spot where you land.
  • The new node is always a leaf; O(h).
6 2 1 4 3 5 8insert 5
BinaryTreeNode<E> insert(
        BinaryTreeNode<E> root, E data) {
    if (root == null)
        return new BinaryTreeNode<>(data);
    int k = data.compareTo(root.data);
    if (k < 0)
        root.leftChild = insert(root.leftChild, data);
    else if (k > 0)
        root.rightChild = insert(root.rightChild, data);
    // k == 0: key already present, skip it
    return root;
}
// usage: root = insert(root, 5);
The recursive call's result is assigned back to the child link, so the new node replaces the null link.

BST Operations · Step through the code

Code Trace: insert(root, 5)

BinaryTreeNode<E> insert(
        BinaryTreeNode<E> root, E data) {
    if (root == null)
        return new BinaryTreeNode<>(data);
    int k = data.compareTo(root.data);
    if (k < 0)
        root.leftChild = insert(root.leftChild, data);
    else if (k > 0)
        root.rightChild = insert(root.rightChild, data);
    // k == 0: key already present, skip it
    return root;
}
// usage: root = insert(root, 5);
621438

main calls root = insert(root, 5). Like search, insertion starts at the root.

6root21438

root ≠ null (6). k = -1 since 5 < 6.

6root21438

Go left: insert(root.leftChild, 5); root = 2. The result will go into root.leftChild.

62root1438

root ≠ null (2). k = 1 since 5 > 2.

62root1438

Go right: insert(root.rightChild, 5); root = 4. The result will go into root.rightChild.

6214root38

root ≠ null (4). k = 1 since 5 > 4.

6214root38

Go right: insert(root.rightChild, 5); root = null. The result will go into root.rightChild.

6214385new

root == null: an empty spot. new creates node 5 and returns it; no link points to it yet.

6214root358

Back in 4's frame: root.rightChild = node(5). The new node replaces the null link (orange).

6214root358

return root: 4 goes back to its parent (2).

62root14358

At 2, root.rightChild = node(4): same link, no change. Then return 2.

6root214358

At 6, root.leftChild = node(2) (the same link), then return 6.

6root214358

main: root = 6 (unchanged). A new node is always a leaf; one call per node on the path → O(h).

BST Operations

Successor and Predecessor

1) With a right subtreesuccessor = the smallest key in the right subtree: one step right, then left all the way.6x425879
2) With no right subtreesuccessor = the nearest ancestor we reach from its left (5 → 4 → 6: 5 is in 6's left subtree).6425x879
OperationMeaningJava TreeMapIn the tree above
successorsmallest key > xhigherKey(x)successor(5) = 6
predecessorlargest key < xlowerKey(x)predecessor(6) = 5
ceilingsmallest key ≥ xceilingKey(x)ceiling(3) = 4
floorlargest key ≤ xfloorKey(x)floor(3) = 2

Each follows one path from the root: O(h). A deleted node with two children is replaced by its successor.

BST Operations · Step through the code

Code Trace: successor(root, 5) and successor(root, 6)

6root425879

successor(root, 5): no candidate yet (candidate = null); start at the root (6).

6candidate4root25879

5 < 6 → 6 is a candidate (> x); anything smaller is on the left → go left.

6candidate425root879

5 ≥ 4 → 4 and everything to its left are ruled out; larger keys are on the right → go right.

nullroot6candidate425879

5 ≥ 5 → 5 and everything to its left are ruled out; larger keys are on the right → go right. Now root = null.

6candidate425879

The loop ends (root == null) → return node(6): successor(5) = 6.

6root425879

Second call, successor(root, 6): no candidate yet (candidate = null); start at the root (6).

64258root79

6 ≥ 6 → 6 and everything to its left are ruled out; larger keys are on the right → go right.

64258candidate7root9

6 < 8 → 8 is a candidate (> x); anything smaller is on the left → go left.

nullroot642587candidate9

6 < 7 → 7 is a candidate (> x); anything smaller is on the left → go left. Now root = null.

642587candidate9

The loop ends (root == null) → return node(7): successor(6) = 7.

TreeNode successor(TreeNode root, int x) {
    TreeNode candidate = null; // smallest key above x
    while (root != null) {
        if (x < root.data) {
            candidate = root;  // root is a candidate
            root = root.left;  // smaller ones are left
        } else
            root = root.right; // keys above x are right
    }
    return candidate;
}

BST Operations

Deleting a Key: Three Cases

First find the node (O(h)). Then, by number of children:

1. LeafNo children. Set the parent's link to null.delete 70
2. One childBypass the node: link the parent straight to the only child.delete 45 · delete 60
3. Two childrenReplace the value with the inorder successor (smallest on the right) or predecessor (largest on the left).delete 40
50 40 30 45 42 60 70

Example tree

BST Operations

Deletion: A Leaf or One Child

50 40 30 45 42 60 70

Leaf: 70 has no children. Its parent, 60, sets its right link to null.

50 40 30 45 42 60

70 is gone; the rest of the tree is unchanged.

50 40 30 45 42 60 70

One child (left): 45 has only a left child, 42. (We start again from the original tree.)

50 40 30 42 60 70

42 takes 45's place as 40's right child: the parent links straight to its grandchild.

50 40 30 45 42 60 70

One child (right): 60 has only a right child, 70.

50 40 30 45 42 70

70 takes 60's place as 50's right child. The order is preserved.

BST Operations

Deletion: A Node with Two Children

50 40 30 45 42 60 70

Two children: 40 has a left (30) and a right (45) child, so it cannot simply be removed.

50 40 30 45 42 60 70

Inorder successor = smallest key in the right subtree: step to 45, then left all the way → 42.

50 42 30 45 42 60 70

The successor's value (42) is copied into 40's node. The old 42 is now a duplicate.

50 42 30 45 60 70

Delete the old 42 from the right subtree; the successor has no left child, so this is always Case 1 or 2.

50 30 45 42 60 70

Alternatively, use the inorder predecessor (largest key on the left = 30). Both give a valid BST.

BST Operations

The Deletion Code

BinaryTreeNode<E> delete(
        BinaryTreeNode<E> root, E data) {
    if (root == null) return null;   // not found
    int k = data.compareTo(root.data);
    if (k < 0)
        root.leftChild = delete(root.leftChild, data);
    else if (k > 0)
        root.rightChild = delete(root.rightChild, data);
    else if (root.leftChild == null)
        return root.rightChild;  // leaf / right child only
    else if (root.rightChild == null)
        return root.leftChild;   // left child only
    else {                       // two children
        root.data = minValue(root.rightChild).data;
        root.rightChild = delete(root.rightChild, root.data);
    }
    return root;
}
The return value goes into the parent's link: returning the only child or null cuts the node out. Usage: root = delete(root, 40); (if the root is deleted, this assignment installs the new one).

BST Operations · Step through the code

Code Trace: delete(root, 70) — a Leaf

BinaryTreeNode<E> delete(
        BinaryTreeNode<E> root, E data) {
    if (root == null) return null;   // not found
    int k = data.compareTo(root.data);
    if (k < 0)
        root.leftChild = delete(root.leftChild, data);
    else if (k > 0)
        root.rightChild = delete(root.rightChild, data);
    else if (root.leftChild == null)
        return root.rightChild;  // leaf / right child only
    else if (root.rightChild == null)
        return root.leftChild;   // left child only
    else {                       // two children
        root.data = minValue(root.rightChild).data;
        root.rightChild = delete(root.rightChild, root.data);
    }
    return root;
}
50403045426070

main calls root = delete(root, 70). First, find the node.

50root403045426070

root ≠ null. k = 1 since 70 > 50.

50root403045426070

Go right: delete(root.rightChild, 70). The result will go into root.rightChild.

504030454260root70

root ≠ null. k = 1 since 70 > 60.

504030454260root70

Go right: delete(root.rightChild, 70). The result will go into root.rightChild.

50403045426070root

root ≠ null. k = 0 since 70 = 70.

50403045426070root

k = 0 → found the node to delete. No left child → return root.rightChild = null. (A leaf returns null.)

504030454260root70

Changed link: root.rightChild = null. Nothing points to the deleted node now; the garbage collector reclaims it.

504030454260root70

return root: 60 goes back to its parent.

50root4030454260

root.rightChild = node(60): same link, no change. Then return root (50).

504030454260

main: root = 50. 70 was a leaf, so only 60's right link changed (to null).

BST Operations · Step through the code

Code Trace: delete(root, 45) — One Child

BinaryTreeNode<E> delete(
        BinaryTreeNode<E> root, E data) {
    if (root == null) return null;   // not found
    int k = data.compareTo(root.data);
    if (k < 0)
        root.leftChild = delete(root.leftChild, data);
    else if (k > 0)
        root.rightChild = delete(root.rightChild, data);
    else if (root.leftChild == null)
        return root.rightChild;  // leaf / right child only
    else if (root.rightChild == null)
        return root.leftChild;   // left child only
    else {                       // two children
        root.data = minValue(root.rightChild).data;
        root.rightChild = delete(root.rightChild, root.data);
    }
    return root;
}
50403045426070

main calls root = delete(root, 45). First, find the node.

50root403045426070

root ≠ null. k = -1 since 45 < 50.

50root403045426070

Go left: delete(root.leftChild, 45). The result will go into root.leftChild.

5040root3045426070

root ≠ null. k = 1 since 45 > 40.

5040root3045426070

Go right: delete(root.rightChild, 45). The result will go into root.rightChild.

50403045root426070

root ≠ null. k = 0 since 45 = 45.

50403045root426070

k = 0 → found. Left child, no right child → return root.leftChild = node(42): the only child goes to the parent.

5040root3042607045

Changed link: root.rightChild = 42. The parent bypasses the deleted node and links to its child.

5040root3042607045

return root: 40 goes back to its parent.

50root4030426070

root.leftChild = node(40): same link, no change. Then return root (50).

504030426070

main: root = 50. 45 is gone; its only child, 42, is now 40's right child. Inorder sequence preserved.

BST Operations · Step through the code

Code Trace: delete(root, 40) — Two Children

BinaryTreeNode<E> delete(
        BinaryTreeNode<E> root, E data) {
    if (root == null) return null;   // not found
    int k = data.compareTo(root.data);
    if (k < 0)
        root.leftChild = delete(root.leftChild, data);
    else if (k > 0)
        root.rightChild = delete(root.rightChild, data);
    else if (root.leftChild == null)
        return root.rightChild;  // leaf / right child only
    else if (root.rightChild == null)
        return root.leftChild;   // left child only
    else {                       // two children
        root.data = minValue(root.rightChild).data;
        root.rightChild = delete(root.rightChild, root.data);
    }
    return root;
}
50403045426070

main calls root = delete(root, 40). First, find the node.

50root403045426070

root ≠ null. k = -1 since 40 < 50.

50root403045426070

Go left: delete(root.leftChild, 40). The result will go into root.leftChild.

5040root3045426070

root ≠ null. k = 0 since 40 = 40.

5040root3045426070

k = 0 → found, but it has two children (30 and 45), so it cannot simply be removed.

5040root304542successor6070

minValue(root.rightChild): one step right (45), then left all the way → successor = 42 (smallest on the right).

5042root3045426070

root.data = 42: the successor's value is copied into the target node. 42 now appears twice; the lower copy must go.

5042root3045426070

delete(root.rightChild, 42): delete the old 42 from the right subtree. The successor has no left child, so this is always the leaf or one-child case.

50423045root426070

root ≠ null. k = -1 since 42 < 45.

50423045root426070

Go left: delete(root.leftChild, 42). The result will go into root.leftChild.

5042304542root6070

root ≠ null. k = 0 since 42 = 42.

5042304542root6070

k = 0 → found the node to delete. No left child → return root.rightChild = null. (A leaf returns null.)

50423045root607042

Changed link: root.leftChild = null. Nothing points to the deleted node now; the garbage collector reclaims it.

50423045root607042

return root: 45 goes back to its parent.

5042root30456070

root.rightChild = node(45) (the same link), return root. 42 now stands where the deleted 40 was.

50root4230456070

root.leftChild = node(42): same link, no change. Then return root (50).

504230456070

main: root = 50. 40 is gone and its successor 42 stands in its place; one node fewer, still a BST. Cost: O(h).

BST Operations

Analyzing BST Operations

  • Search, insertion, and deletion take time proportional to the depth of the deepest node reached: O(h).
4 2 1 3 6 5 7balanced: h = 2 ≈ log₂ n 1 2 3 4 5degenerate (chain): h = n − 1
CaseHeight hSearch / Insert / DeleteSteps for n = 106
Balanced (best)≈ log2 nO(log n)≈ 20
Random order (average)≈ 2.99 log2 nO(log n)≈ 28 (average)
Degenerate (worst, sorted input)n − 1O(n)106

With random insertion order, the average depth of a node is ≈ 2 ln n ≈ 1.39 log2 n (≈ 28 steps for n = 106), while the expected height is ≈ 4.31 ln n ≈ 2.99 log2 n (≈ 60).

BST Operations · Step through the code

Code Trace: the Range Query range(root, 5, 10)

842657121014lo = 5, hi = 10

range(8): 5 < 8 → the left may hold keys in range: range(k.left).

842657121014lo = 5, hi = 10

range(4): 5 < 4 is false → every key on the left is < 4 ≤ lo: left subtree pruned (never visited).

842657121014lo = 5, hi = 10

Left subtree pruned (4 ≤ lo = 5), no call made; range(4) continues. 4 is out of range, not printed. 4 < 10 → try the right too.

842657121014lo = 5, hi = 10

range(6): 5 < 6 → the left may hold keys in range: range(k.left).

842657121014lo = 5, hi = 10

range(5): 5 < 5 is false → every key on the left is < 5 ≤ lo: left subtree pruned (never visited).

842657121014lo = 5, hi = 10

Left subtree pruned (5 ≤ lo = 5), no call made; range(5) continues. 5 ∈ [5, 10] → print. 5 < 10 → try the right too.

null842657121014lo = 5, hi = 10

range(null): empty subtree → return at once.

842657121014lo = 5, hi = 10

Left side done; back in range(6). 6 ∈ [5, 10] → print. 6 < 10 → try the right too.

842657121014lo = 5, hi = 10

range(7): 5 < 7 → the left may hold keys in range: range(k.left).

null842657121014lo = 5, hi = 10

range(null): empty subtree → return at once.

842657121014lo = 5, hi = 10

Left side done; back in range(7). 7 ∈ [5, 10] → print. 7 < 10 → try the right too.

null842657121014lo = 5, hi = 10

range(null): empty subtree → return at once.

842657121014lo = 5, hi = 10

Left side done; back in range(8). 8 ∈ [5, 10] → print. 8 < 10 → try the right too.

842657121014lo = 5, hi = 10

range(12): 5 < 12 → the left may hold keys in range: range(k.left).

842657121014lo = 5, hi = 10

range(10): 5 < 10 → the left may hold keys in range: range(k.left).

null842657121014lo = 5, hi = 10

range(null): empty subtree → return at once.

842657121014lo = 5, hi = 10

Left side done; back in range(10). 10 ∈ [5, 10] → print. 10 ≥ 10 → right subtree pruned.

842657121014lo = 5, hi = 10

Left side done; back in range(12). 12 is out of range, not printed. 12 ≥ 10 → right subtree pruned.

842657121014lo = 5, hi = 10

Done. The output is in sorted (inorder) order: 5 6 7 8 10. 2 and 14 were never visited (faded). Cost: O(h + s), s = keys printed.

void range(TreeNode k, int lo, int hi) {
    if (k == null) return;
    if (lo < k.data)           // candidates on the left
        range(k.left, lo, hi);
    if (lo <= k.data && k.data <= hi)
        System.out.print(k.data + " ");
    if (k.data < hi)           // candidates on the right
        range(k.right, lo, hi);
}

BST Operations · Step through the code

Code Trace: Is This Tree a BST? isBST()

6(−∞, ∞)214378

isBST(6): 6 ∈ (−∞, ∞) ✓. Left subtree gets upper bound 6: isBST(k.left, −∞, 6).

62(−∞, 6)14378

isBST(2): 2 ∈ (−∞, 6) ✓. Left subtree gets upper bound 2: isBST(k.left, −∞, 2).

621(−∞, 2)4378✓

isBST(1): 1 ∈ (−∞, 2) ✓. Leaf: both null children return true → return true.

62(−∞, 6)14378✓

Left side true. Right subtree gets lower bound 2: isBST(k.right, 2, 6).

6214(2, 6)378✓

isBST(4): 4 ∈ (2, 6) ✓. Left subtree gets upper bound 4: isBST(k.left, 2, 4).

62143(2, 4)78✓✓

isBST(3): 3 ∈ (2, 4) ✓. Leaf: both null children return true → return true.

6214(2, 6)378✓✓

Left side true. Right subtree gets lower bound 4: isBST(k.right, 4, 6).

621437(4, 6)8✓✓✗

isBST(7), allowed range (4, 6): 7 ≥ 6 → return false. Right of 4, 7 must exceed 4 (✓); in 6's left subtree, it must also be less than 6.

6214(2, 6)378✓✓✗✗

false from the right → isBST(4) = true && false = false.

62(−∞, 6)14378✓✓✗✗✗

false from the right → isBST(2) = true && false = false.

6(−∞, ∞)214378✓✓✗✗✗✗

Left result false → && short-circuits: the right subtree is never checked; isBST(6) returns false.

6214378✓✓✗✗✗✗

Result: false; not a BST. A check of parent–child pairs alone (4 < 7 ✓) would wrongly say true; the range must be passed down. Cost: O(n).

boolean isBST(TreeNode k, long min, long max) {
    if (k == null) return true;
    if (k.data <= min || k.data >= max)
        return false;          // out of range
    return isBST(k.left, min, k.data)
        && isBST(k.right, k.data, max);
}
// first call: isBST(root, Long.MIN_VALUE,
//                   Long.MAX_VALUE)

BST Operations

Application: Student Records

The student ID is the key; each node carries the whole student object.

public class Student {
    int id;     // key
    String fullName;

    public Student(int id,
                   String fullName) {
        this.id = id;
        this.fullName = fullName;
    }
}
public class StudentNode {
    Student st;           // data
    StudentNode leftChild;
    StudentNode rightChild;

    public StudentNode(Student st) {
        this.st = st;
        // children start out null
    }
}
Java's TreeMap<Integer, Student> packages the same idea: a key → value map backed by a balanced (red-black) search tree.

BST Operations

Inserting and Finding Students

StudentNode insert(StudentNode root, Student st) {
    if (root == null) return new StudentNode(st);
    if (st.id < root.st.id)
        root.leftChild = insert(root.leftChild, st);     // continue left
    else if (st.id > root.st.id)
        root.rightChild = insert(root.rightChild, st);   // continue right
    return root;                                         // duplicate IDs are skipped
}
Student search(StudentNode root, int id) {
    if (root == null) return null;                // no such record
    if (id == root.st.id) return root.st; // found
    if (id < root.st.id)
        return search(root.leftChild, id);         // continue left
    return search(root.rightChild, id);            // continue right
}

search returns the student object, not a boolean, so a hit also gives the name and the rest of the record. With an int key, == and < work directly.

Balance and AVL Trees

Why Balance Matters

  • A BST's shape depends on the insertion order: insert 1, 2, 3, 4 in order and it becomes a linked list.
  • Arranged differently, the same keys fit in a shorter tree (two options on the right).
1 2 3 4 2 1 3 4 3 1 2 4h = 3h = 2h = 2
Self-balancing search trees repair themselves after each insertion and deletion with small local changes (rotations, recoloring), guaranteeing h = O(log n).

Balance and AVL Trees

A Rebalancing Example

On the left, the branches 9 → 14 → 12 and 76 → 54 → 72 → 67 have grown long. Rearranging those subtrees (new subtree roots in orange) fits the same 11 keys into a shorter tree:

50 17 9 14 12 23 19 76 54 72 67 50 17 12 9 14 23 19 72 54 67 76height 4height 3 · all |bf| ≤ 1
  • Rearranging does not change the inorder sequence: 9 12 14 17 19 23 50 54 67 72 76.
  • Rebuilding the tree each time would cost O(n); AVL and red-black trees work only along the update path, in O(log n).

Balance and AVL Trees

AVL Trees

  • Adelson-Velsky and Landis (1962): the first balanced BST.
  • Balance factor: bf(x) = h(left) − h(right). An empty subtree has height −1.
  • AVL condition: bf ∈ {−1, 0, +1} at every node.
  • Consequence: h < 1.44 log2(n + 2), so every operation is O(log n).
  • Each node stores its height; after an update, bf is checked on the way back up to the root.
How to read itThe number at a node's upper right is its bf: positive means left-heavy, negative right-heavy. We rotate at the first (lowest) node with |bf| = 2.
300 200 100 250 40−1 500 302 202 101 50 400AVL ✓not AVL: |bf| = 2 at 20 and 30

Balance and AVL Trees

Why Is the Height of an AVL Tree O(log n)?

Question: what is the fewest nodes an AVL tree of height h can have? In the sparsest one, the two subtrees of every internal node differ in height by exactly 1.

10N(0) = 1

h = 0: a single node. N(0) = 1.

2110N(0) = 1N(1) = 2

h = 1: a root and one child. N(1) = 2.

31211040N(0) = 1N(1) = 2N(2) = 4

h = 2: root (orange) + sparsest tree of height 1 (left) + sparsest tree of height 0 (right). N(2) = 1 + N(1) + N(0) = 1 + 2 + 1 = 4.

51312110407160N(0) = 1N(1) = 2N(2) = 4N(3) = 7

h = 3: root (orange) + sparsest tree of height 2 (left) + sparsest tree of height 1 (right). N(3) = 1 + N(2) + N(1) = 1 + 4 + 2 = 7.

815131211040716011110190120N(0) = 1N(1) = 2N(2) = 4N(3) = 7N(4) = 12

h = 4: root (orange) + sparsest tree of height 3 (left) + sparsest tree of height 2 (right). N(4) = 1 + N(3) + N(2) = 1 + 7 + 4 = 12. The counts grow like Fibonacci numbers: N(h) = F(h+3) − 1.

N(h) = 1 + N(h−1) + N(h−2) > 2·N(h−2) → N(h) > 2h/2 → h < 2 log2 n. Fibonacci gives a tighter bound: h < 1.44 log2(n + 2).

Balance and AVL Trees

Rotation

T3T1T2 y x rotateRight(y) T1T2T3 x y same inorder sequence on both sides: T1 · x · T2 · y · T3

Right rotation (y left-heavy): x moves up, y becomes x's right child, and x's right subtree T2 moves to y's left.

T1T2T3 x y rotateLeft(x) T3T1T2 y x same inorder sequence on both sides: T1 · x · T2 · y · T3

Left rotation (x right-heavy): the mirror image. Only 3 links change (thick lines) → O(1).

A rotation preserves the BST property (same inorder sequence) but changes subtree heights: one side loses a level, the other gains one.

Balance and AVL Trees

Single Rotations: LL and RR

302201100

LL: 10 went to the left of the left child (20). bf(30) = 2 ✗ and bf(20) = 1: both nodes lean the same way (left).

200100300

Rotate right at 30: 20 moves up and 30 becomes its right child (orange link). All bf = 0.

10−220−1300

RR: 30 went to the right of the right child (20). bf(10) = −2 ✗ and bf(20) = −1: both lean right.

200100300

Rotate left at 10: 20 moves up and 10 becomes its left child. All bf = 0.

The number at a node's upper right is its bf. LL takes a single right rotation, RR a single left rotation; the orange link is the one the rotation changes.

Balance and AVL Trees

Double Rotations: LR and RL

30210−1200

LR: 20 went to the right of the left child (10). bf(30) = 2 but bf(10) = −1: opposite leans (a triangle). A single right rotation still leaves a triangle.

302201100

Rotation 1: rotate left at 10 → 20 moves up; the triangle becomes a straight line (LL). bf(30) is still 2.

200100300

Rotation 2: rotate right at 30 → the middle key, 20, becomes the root; all bf = 0.

10−2301200

RL: 20 went to the left of the right child (30). bf(10) = −2, bf(30) = 1: a triangle.

10−220−1300

Rotation 1: rotate right at 30 → a straight line (RR). bf(10) is still −2.

200100300

Rotation 2: rotate left at 10 → 20 is the root; all bf = 0.

LR: rotate left at the child, then right at the unbalanced node. RL: right at the child, then left.

Balance and AVL Trees

AVL Rotation and Rebalancing Code

int h(AvlNode d) {
    return d == null ? -1 : d.height;
}
int bf(AvlNode d) {
    return h(d.left) - h(d.right);
}
void update(AvlNode d) {
    d.height = 1 +
        Math.max(h(d.left), h(d.right));
}
AvlNode rotateRight(AvlNode y) {
    AvlNode x = y.left;
    y.left = x.right;  // T2 → y.left
    x.right = y;       // y → x.right
    update(y);         // lower node first
    update(x);
    return x;          // new root
}
AvlNode rebalance(AvlNode d) {
    update(d);
    if (bf(d) > 1) {       // left-heavy
        if (bf(d.left) < 0)   // LR
            d.left = rotateLeft(d.left);
        return rotateRight(d); // LL
    }
    if (bf(d) < -1) {      // right-heavy
        if (bf(d.right) > 0)  // RL
            d.right = rotateRight(d.right);
        return rotateLeft(d);  // RR
    }
    return d;
}
// rotateLeft: mirror image (left ↔ right)
// insert/delete as in a BST; on return:
//   return rebalance(root);

As the recursion unwinds, rebalance runs at each node on the path; a deletion may rotate at several nodes (at most O(log n)).

Balance and AVL Trees · Step through the code

Code Trace: rotateRight(40) — the LL Case

AvlNode rotateRight(AvlNode y) {
    AvlNode x = y.left;
    y.left = x.right;  // T2 → y.left
    x.right = y;       // y → x.right
    update(y);         // lower node first
    update(x);
    return x;          // new root
}
void update(AvlNode d) {
    d.height = 1 +
        Math.max(h(d.left), h(d.right));
}
402h=3201h=2101h=150h=0300h=0500h=0

After inserting 5, rebalance(40) finds bf = 2; its left child 20 has bf = 1 → LL. Call rotateRight(y).

40h=3y20h=2x10h=15h=030h=050h=0

x = y.left: 20 is the child that will move up. No link has changed yet.

40h=3y20h=2x10h=15h=030h=050h=0

y.left = x.right: 40's left link now points to T2 (30). 20 is detached; only x still reaches it.

40h=3y20h=2x10h=15h=030h=050h=0

x.right = y: x.right: 30 → 40. 40 is now 20's right child (the orange arrow points up).

40h=1y20h=2x10h=15h=030h=050h=0

update(y): 40.height = 1 + max(0, 0) = 1 (was 3). The lower node goes first, since x's height depends on it.

40h=1y20h=2x10h=15h=030h=050h=0

update(x): 20.height = 1 + max(1, 1) = 2 (unchanged, but must be recomputed).

200h=2x101h=150h=0400h=1y300h=0500h=0

return x: 20 is the subtree's new root (same links, redrawn). All bf ∈ {−1, 0, 1}; inorder sequence unchanged. The method changed 2 links; the caller sets the 3rd (from the parent) → O(1).

Balance and AVL Trees · Step through the code

Code Trace: rotateLeft(20) — the RR Case

AvlNode rotateLeft(AvlNode x) {
    AvlNode y = x.right;
    x.right = y.left;  // T2 → x.right
    y.left = x;        // x → y.left
    update(x);         // lower node first
    update(y);
    return y;          // new root
}
void update(AvlNode d) {
    d.height = 1 +
        Math.max(h(d.left), h(d.right));
}
20−2h=3100h=040−1h=2300h=050−1h=1600h=0

After inserting 60, rebalance(20) finds bf = −2; its right child 40 has bf = −1 → RR. Call rotateLeft(x).

20h=3x10h=040h=2y30h=050h=160h=0

y = x.right: 40 is the child that will move up. No link has changed yet.

20h=3x10h=040h=2y30h=050h=160h=0

x.right = y.left: 20's right link now points to T2 (30). 40 is detached; only y still reaches it.

20h=3x10h=040h=2y30h=050h=160h=0

y.left = x: y.left: 30 → 20. 20 is now 40's left child (the orange arrow points up).

20h=1x10h=040h=2y30h=050h=160h=0

update(x): 20.height = 1 + max(0, 0) = 1 (was 3). The lower node goes first, since y's height depends on it.

20h=1x10h=040h=2y30h=050h=160h=0

update(y): 40.height = 1 + max(1, 1) = 2 (unchanged, but must be recomputed).

400h=2y200h=1x100h=0300h=050−1h=1600h=0

return y: 40 is the subtree's new root (same links, redrawn). All bf ∈ {−1, 0, 1}; inorder sequence unchanged. The method changed 2 links; the caller sets the 3rd (from the parent) → O(1).

Balance and AVL Trees · Step through the code

Code Trace: rebalance(50) — the LR Case

AvlNode rebalance(AvlNode d) {
    update(d);
    if (bf(d) > 1) {       // left-heavy
        if (bf(d.left) < 0)   // LR
            d.left = rotateLeft(d.left);
        return rotateRight(d); // LL
    }
    if (bf(d) < -1) {      // right-heavy
        if (bf(d.right) > 0)  // RL
            d.right = rotateRight(d.right);
        return rotateLeft(d);  // RR
    }
    return d;
}
501h=220−1h=1100h=0301h=1d250h=0600h=0

After inserting 25, the unwinding recursion calls rebalance(30). update: height 0 → 1, bf = 1.

501h=220−1h=1100h=0301h=1d250h=0600h=0

bf = 1: neither > 1 nor < −1 → no rotation; return d to the caller.

502h=220−1h=2d100h=0301h=1250h=0600h=0

After inserting 25, the unwinding recursion calls rebalance(20). update: height 1 → 2, bf = −1.

502h=220−1h=2d100h=0301h=1250h=0600h=0

bf = −1: neither > 1 nor < −1 → no rotation; return d to the caller.

502h=3d20−1h=2100h=0301h=1250h=0600h=0

rebalance(50): update → height 3, bf = h(20) − h(60) = 2 − 0 = 2.

502h=3d20−1h=2100h=0301h=1250h=0600h=0

bf(d) = 2 > 1 → left-heavy. Which rotation? Look at how the left child leans.

502h=3d20−1h=2100h=0301h=1250h=0600h=0

bf(d.left) = bf(20) = -1 < 0: the left child leans right → LR (a triangle). A single right rotation is not enough.

502h=3d302h=2200h=1100h=0250h=0600h=0

d.left = rotateLeft(20): 30 moves up and 25 moves to 20's right. The triangle is now a straight line (LL); bf(50) is still 2.

300h=2200h=1100h=0250h=050−1h=1600h=0

return rotateRight(50): 30 becomes the root, 50 drops to its right, and 30's old right (null) becomes 50's left.

300h=2200h=1100h=0250h=050−1h=1600h=0

rebalance(50) returns the new subtree root 30; insert hands it to its caller (here root). Height drops from 3 to 2.

Balance and AVL Trees · Step through the code

Tracing AVL Insertion: 10, 20, 30

100

Empty tree: insert(null, 10) → d == null, a new node is returned; root = 10, bf = 0.

null100d

insert(root, 20) descends as in a BST: 20 > 10 → right.

10−1200

d == null → new leaf 20 is returned and becomes 10's right child. The bf values on the path have changed.

10−1200

Unwinding, each ancestor runs return rebalance(d): bf(10) = −1 → all |bf| ≤ 1, no rotation.

null10−1200d

insert(root, 30) descends as in a BST: 30 > 10 → right; 30 > 20 → right.

10−220−1300

d == null → new leaf 30 is returned and becomes 20's right child. The bf values on the path have changed.

10−2d20−1300

rebalance(10): bf = −2 ✗ (lowest unbalanced ancestor). Right child 20 has bf = −1 → RR case.

200100300

rotateLeft at 10: 20 moves up and 10 becomes its left child. All bf ∈ {−1, 0, 1}.

AvlNode insert(AvlNode d, int k) {
    if (d == null) return new AvlNode(k);
    if (k < d.key)
        d.left = insert(d.left, k);
    else if (k > d.key)
        d.right = insert(d.right, k);
    else return d;          // no duplicates
    return rebalance(d);
}

Balance and AVL Trees · Step through the code

Tracing AVL Insertion (continued): 40, 50, 25

null200100300d

insert(root, 40) descends as in a BST: 40 > 20 → right; 40 > 30 → right.

20−110030−1400

d == null → new leaf 40 is returned and becomes 30's right child. The bf values on the path have changed.

20−110030−1400

Unwinding, each ancestor runs return rebalance(d): bf(30) = −1, bf(20) = −1 → all |bf| ≤ 1, no rotation.

null20−110030−1400d

insert(root, 50) descends as in a BST: 50 > 20 → right; 50 > 30 → right; 50 > 40 → right.

20−210030−240−1500

d == null → new leaf 50 is returned and becomes 40's right child. The bf values on the path have changed.

20−210030−2d40−1500

rebalance(30): bf = −2 ✗ (lowest unbalanced ancestor). Right child 40 has bf = −1 → RR case.

20−1100400300500

rotateLeft at 30: 40 moves up and 30 becomes its left child. All bf ∈ {−1, 0, 1}.

null20−1100400300d500

insert(root, 25) descends as in a BST: 25 > 20 → right; 25 < 40 → left; 25 < 30 → left.

20−2100401301250500

d == null → new leaf 25 is returned and becomes 30's left child. The bf values on the path have changed.

20−2d100401301250500

rebalance(20): bf = −2 ✗ (lowest unbalanced ancestor). Right child 40 has bf = 1 → RL case.

20−2d10030−125040−1500

Rotation 1: rotateRight at 40 → the triangle becomes a straight line (right-right). bf(20) is still −2.

30020010025040−1500

Rotation 2: rotateLeft at 20: 30 moves up and 25 moves to 20's right. The tree is AVL again. All bf ∈ {−1, 0, 1}.

AvlNode insert(AvlNode d, int k) {
    if (d == null) return new AvlNode(k);
    if (k < d.key)
        d.left = insert(d.left, k);
    else if (k > d.key)
        d.right = insert(d.right, k);
    else return d;          // no duplicates
    return rebalance(d);
}

Red-Black Trees

Red-Black Trees

  1. Every node is red or black.
  2. The root is black.
  3. All leaves (NIL, the empty children) count as black.
  4. A red node's children are black: no two reds in a row.
  5. Every path from a node down to its NIL leaves has the same number of black nodes (the black height).
13 8 1 6 11 17 15 25 22 27

Example: every path from the root to a NIL has 2 black nodes (counting the root, not the NIL)

Red-Black Trees

Why Do the Rules Guarantee Balance?

1052015302540shortest: 2longest: 4 = 2·2
  • Shortest path: can be all black → b nodes (10–5 in the figure: b = 2).
  • Longest path: reds cannot be adjacent, so at most one red per black → at most 2b nodes (10–20–30–40).
  • Consequence: longest path ≤ 2 × shortest path, which gives h ≤ 2·log2(n + 1).
  • Cost: search, insertion, deletion in O(log n); at most 2 rotations per insertion, 3 per deletion; the rest is recoloring.
A red-black tree is balanced more loosely than an AVL tree (it may be a bit taller), but updates are cheaper. Java's TreeMap/TreeSet and C++'s std::map use it.

Red-Black Trees

Inserting into a Red-Black Tree

Insert the new node as in a BST and color it red (leaving the black height intact). If its parent is red too, rule 4 breaks; look at the uncle (the parent's sibling):

Red uncleRecolor: parent and uncle black, grandparent red. The problem may move up → continue from the grandparent.
Black uncle, triangleIf node, parent, and grandparent zigzag, rotate at the parent → the line case.
Black uncle, lineRotate at the grandparent; the old parent becomes black and the old grandparent red. Done.
  • If the parent is black, nothing needs doing. At the end, the root is always colored black.
  • A NIL uncle counts as black.
  • Deletion is similar but has more cases (the "double black" fix).
g p x ugrandparentparentunclenew node

Red-Black Trees

Fixing Up After Insertion

void fixUp(Node x) {      // x: new, red
    while (x != root && isRed(x.parent)) {
        Node p = x.parent, g = p.parent;
        Node u = (p == g.left) ? g.right : g.left;
        if (isRed(u)) {                // red uncle
            p.color = u.color = BLACK;
            g.color = RED;
            x = g;                     // move up to g
        } else {                       // black uncle
            if (isTriangle(x, p, g)) { // zigzag
                rotate(p, x);          // make it a line
                Node t = x; x = p; p = t;
            }
            rotate(g, p);              // line
            p.color = BLACK; g.color = RED;
        }
    }
    root.color = BLACK;
}
// Insertion: add a leaf as in a BST,
// link its parent, color it red:
//   x = bstInsert(key);
//   x.color = RED; fixUp(x);

// NIL (null) counts as black
boolean isRed(Node n) {
    return n != null
        && n.color == RED;
}
// is x–p–g a zigzag (triangle)?
boolean isTriangle(Node x, Node p,
                   Node g) {
    return (p == g.left)
        != (x == p.left);
}
// rotate(a, b): b is a child of a;
// moves b into a's place (rotate
// right if b is a left child, else
// left; parent links included)

Red-Black Trees · Step through the code

Code Trace: fixUp — Inserting 10, 20, 30, 15

void fixUp(Node x) {      // x: new, red
    while (x != root && isRed(x.parent)) {
        Node p = x.parent, g = p.parent;
        Node u = (p == g.left) ? g.right : g.left;
        if (isRed(u)) {                // red uncle
            p.color = u.color = BLACK;
            g.color = RED;
            x = g;                     // move up to g
        } else {                       // black uncle
            if (isTriangle(x, p, g)) { // zigzag
                rotate(p, x);          // make it a line
                Node t = x; x = p; p = t;
            }
            rotate(g, p);              // line
            p.color = BLACK; g.color = RED;
        }
    }
    root.color = BLACK;
}
10x

10 is inserted as in a BST (as the root) and colored red; then fixUp(x).

10x

x is the root → the loop condition is false; the loop is skipped.

10x

root.color = BLACK: the root is always black (rule 2).

1020x

20 is inserted as in a BST (10's right child) and colored red; then fixUp(x).

1020x

x.parent (10) is black → no red-red pair; the loop ends. Line 18: the root is already black.

102030x

30 is inserted as in a BST (20's right child) and colored red; then fixUp(x).

102030x

x.parent (20) is red too: two reds in a row ✗ (rule 4) → enter the loop.

NILu10g20p30x

p = parent (20), g = grandparent (10), u = uncle = NIL (empty, counts as black).

NILu10g20p30x

The uncle is black → a rotation is needed. x–p–g is a straight line: isTriangle() is false; jump to line 14.

20p10g30x

rotate(g, p): left rotation at grandparent 10 → p (20) moves up, g becomes its child.

20p10g30x

Colors: p black, g red. No red-red pair remains; the black height is preserved.

20p10g30x

x.parent (20) is black → no red-red pair; the loop ends. Line 18: the root is already black.

201015x30

15 is inserted as in a BST (10's right child) and colored red; then fixUp(x).

201015x30

x.parent (10) is red too: two reds in a row ✗ (rule 4) → enter the loop.

20g10p15x30u

p = parent (10), g = grandparent (20), u = uncle = 30.

20g10p15x30u

The uncle is red → just recolor: p and u black, g red. Every path keeps the same number of blacks.

20x101530

x = g (20): g is now red, and its parent may be red too → the loop continues from there.

20x101530

x is the root → the loop condition is false; the loop is skipped.

20x101530

root.color = BLACK: the root is always black (rule 2). The black height grows by 1.

Red-Black Trees · Step through the code

Code Trace: fixUp — Inserting 12 and 5

void fixUp(Node x) {      // x: new, red
    while (x != root && isRed(x.parent)) {
        Node p = x.parent, g = p.parent;
        Node u = (p == g.left) ? g.right : g.left;
        if (isRed(u)) {                // red uncle
            p.color = u.color = BLACK;
            g.color = RED;
            x = g;                     // move up to g
        } else {                       // black uncle
            if (isTriangle(x, p, g)) { // zigzag
                rotate(p, x);          // make it a line
                Node t = x; x = p; p = t;
            }
            rotate(g, p);              // line
            p.color = BLACK; g.color = RED;
        }
    }
    root.color = BLACK;
}
20101512x30

12 is inserted as in a BST (15's left child) and colored red; then fixUp(x).

20101512x30

x.parent (15) is red too: two reds in a row ✗ (rule 4) → enter the loop.

NILu2010g15p12x30

p = parent (15), g = grandparent (10), u = uncle = NIL (empty, counts as black).

NILu2010g15p12x30

The uncle is black → a rotation is needed. x–p–g is a triangle (zigzag): x and p are children on opposite sides.

NILu2010g12x15p30

rotate(p, x): a right rotation at 15 → x goes up, p goes down. The triangle is now a straight line (still two reds in a row).

NILu2010g12p15x30

x and p swap roles: now x = 15 (the lower one) and p = 12.

2012p10g15x30

rotate(g, p): left rotation at grandparent 10 → p (12) moves up, g becomes its child.

2012p10g15x30

Colors: p black, g red. No red-red pair remains; the black height is preserved.

2012p10g15x30

x.parent (12) is black → no red-red pair; the loop ends. Line 18: the root is already black.

2012105x1530

5 is inserted as in a BST (10's left child) and colored red; then fixUp(x).

2012105x1530

x.parent (10) is red too: two reds in a row ✗ (rule 4) → enter the loop.

2012g10p5x15u30

p = parent (10), g = grandparent (12), u = uncle = 15.

2012g10p5x15u30

The uncle is red → just recolor: p and u black, g red. Every path keeps the same number of blacks.

2012x1051530

x = g (12): g is now red, and its parent may be red too → the loop continues from there.

2012x1051530

x.parent (20) is black → no red-red pair; the loop ends. Line 18: the root is already black.

Balanced Search Trees

Deletion in AVL and Red-Black Trees

Common first stepAn ordinary BST deletion. A node with two children is replaced by its successor or predecessor, so the actual removal is a leaf or one-child case.
Why the extra work?Removing a node can break the AVL height condition or the red-black black-height rule.
AVL fixUpdate heights from the deleted node up to the root. At every node with |bf| = 2, apply an LL, RR, LR, or RL rotation. Several ancestors may need fixing.
Red-black fixDeleting a red node is easy. If a black goes missing, recolor or rotate based on the sibling's color and children; the missing black may travel up to the root.
StructureUpward passRotationsTotal
AVLO(log n)up to O(log n)O(log n)
Red-blackO(log n)at most 3O(log n)

Insertion and deletion keep the BST order; rebalancing changes only the shape of the tree and its bookkeeping fields.

Red-Black Trees

AVL or Red-Black?

PropertyAVLRed-black
Balance criterion|bf| ≤ 1 at every nodecolor rules, equal black height
Maximum height≈ 1.44 log2 n2 log2(n + 1)
SearchO(log n), slightly faster (shorter tree)O(log n)
Rotations per insertionat most 2 (one single or one double)at most 2
Rotations per deletionup to O(log n)at most 3
Extra data per nodeheight (or bf)1 color bit
Best forsearch-heavy workloadsfrequent updates, libraries

Splay Trees

Splay Trees

  • Sleator and Tarjan (1985): a self-adjusting BST.
  • Every access (search, insertion, deletion) rotates the node to the root: the splay operation.
  • Nodes store no balance information (no height, no color).
  • One operation can take O(n), but any m operations take O(m log n): O(log n) amortized.
LocalityFrequently accessed keys stay near the root, so the tree acts like a cache.
SimplicityNo extra fields; one splay routine serves every operation.
DrawbackEven a search changes the tree, and a single operation has no guarantee.

Splay Trees

Splay Steps: Zig, Zig-Zig, Zig-Zag

x: the node being splayed, p: its parent, g: its grandparent. Repeat until x is the root.

20 10 5 15 30 x = 10; parent p = 20 is root rotateRight(p) 10 5 20 15 30 x is root; 15 moved left of p

Zig: x's parent p is the root → a single rotation at p. (This can only be the last step.)

30 20 10 x, p, g in a line (left-left) rotate(g) 20 10 30 grandparent g = 30 first rotate(p) 10 20 30 then parent p = 20

Zig-zig: x and p are children on the same side → rotate at g first, then at p. (Not the same as an AVL single rotation!)

30 10 20 x right, p left child (zigzag) rotateLeft(p) 30 20 10 parent p = 10 first rotateRight(g) 20 10 30 then grandparent g = 30

Zig-zag: x and p are on opposite sides → rotate at p, then at g (exactly an AVL double rotation).

Splay Trees · Step through the code

Code Trace: splay(50), then splay(10)

void splay(Node x) {
    while (x != root) {
        Node p = x.parent, g = p.parent;
        if (g == null)                 // zig
            rotate(p, x);
        else if (sameSide(x, p, g)) {  // zig-zig
            rotate(g, p);   // grandparent first
            rotate(p, x);
        } else {                       // zig-zag
            rotate(p, x);   // parent first
            rotate(g, x);
        }
    }
}
80402010306050x7090

splay(50): first an ordinary BST search, 80 → 40 → 60 → 50; the node found is x. (sameSide: are x and p children on the same side? rotate: the single rotation from the red-black code.)

8040g20103060p50x7090

x is not the root. p = 60, g = 40.

8040g20103050x60p7090

x and p are on opposite sides → zig-zag. The parent first: rotate right at 60.

8050x40g20103060p7090

Then the grandparent: rotate left at 40 → x becomes the parent of both p and g.

80p50x40201030607090

x is not the root. p = 80, g = null (p is the root).

50x4020103080p607090

g == null → zig: a right rotation at the root, 80; x becomes the root.

50x4020103080607090

x == root → the loop ends. 50 is now the root; frequently accessed keys stay near the root.

50402010x3080607090

splay(10): first an ordinary BST search, 50 → 40 → 20 → 10; the node found becomes x.

5040g20p10x3080607090

x is not the root. p = 20, g = 40.

5020p10x40g3080607090

x and p are on the same side → zig-zig. The grandparent first: rotate right at 40 (p moves up).

5010x20p40g3080607090

Then the parent: rotate right at 20 → x has risen two levels.

50p10x20403080607090

x is not the root. p = 50, g = null (p is the root).

10x50p20403080607090

g == null → zig: a right rotation at the root, 50; x becomes the root.

10x5020403080607090

x == root → the loop ends. 10 is the root; 50, accessed just before, is still near it (depth 1). One operation may take O(n); amortized, O(log n).

Multiway Search Trees

B-Trees

  • A balanced multiway search tree whose nodes hold several keys and many children.
  • In a B-tree of order m, a node has at most m children and m − 1 keys; each non-root internal node has at least ⌈m/2⌉ children.
  • Keys within a node are sorted, and all leaves are at the same depth.
  • The height stays O(log n); insertions and deletions split or merge nodes.
20405102530355060finding 30: 20 < 30 < 40 → middle child
Why? One node fills one disk block. Wide and shallow = few disk reads. The basis of database and file-system indexes.

A B-tree of order 4 (a 2-3-4 tree) is equivalent to a red-black tree: merge each black node with its red children and you get a 2-3-4 tree. Insertion and splitting step by step: B-tree simulator.

Multiway Search Trees

Search and Insertion in a B-Tree

51025303550602040root: 20 < 30 < 40 → go to the middle child

1 · Search within the node. To find 30, compare it with the root's keys in order; 20 < 30 < 40 → descend to the middle child, which covers that gap.

51025303550602040leaf: 25, 30 → found (2 nodes read)

30 is found in the leaf. Each level reads just one node (one disk block) → the cost is proportional to the height: O(logm n).

51025303335overflow!50602040m = 4: at most 3 keys per node

2 · Insert into the leaf. 33 follows the same path and goes into the leaf in sorted position. The leaf now holds 4 keys, over its capacity (m − 1 = 3).

510253035506020334025 30 | 33 ↑ | 35

3 · Split the overflow. The middle key (33) moves up to the parent; the rest split into two nodes (25 30 and 35). If the parent overflows too, splitting continues upward; if the root splits, a new root appears and the height grows by one.

When the root splitsA new root appears and the height grows by just one. All leaves remain at the same depth.
DeletionAn underfull node borrows a key from a sibling or merges with it; balance and fill limits are preserved.

Multiway Search Trees · Step by step

B-Tree Insertion and Splitting (m = 3)

Keys inserted in order: 10, 20, 30, 40, 50, 60, 70 · order m = 3 → at most 2 keys and 3 children per node.

10

Insert 10. It goes into the root leaf in sorted order; 1 key (≤ 2), no split.

1020

Insert 20. It goes into the root leaf in sorted order; 2 keys (≤ 2), no split.

102030overflow!

Insert 30 → node [10, 20, 30]: 3 keys, over capacity (m − 1 = 2) → split.

103020

The root splits: the median 20 moves up as a new root; 10 and 30 become separate nodes. The height grows by one; all leaves stay at the same depth.

10304020

Insert 40. Go right from the root and add it to the leaf in sorted order; 2 keys (≤ 2), no split.

10304050overflow!20

Insert 50 → node [30, 40, 50]: 3 keys, over capacity (m − 1 = 2) → split.

1030502040

The median, 40, moves up to the parent; the rest split into two nodes, 30 and 50. The parent has room → the split stops here.

103050602040

Insert 60. Go right from the root and add it to the leaf in sorted order; 2 keys (≤ 2), no split.

1030506070overflow!2040

Insert 70 → node [50, 60, 70]: 3 keys, over capacity (m − 1 = 2) → split.

10305070204060overflow!

The median, 60, moves up to the parent and the node splits into 50 | 70. But the parent [20, 40, 60] now has 3 keys → the split propagates upward.

10305070206040

The root splits: the median 40 moves up as a new root; 20 and 60 become separate nodes. The height grows by one; all leaves stay at the same depth.

10305070206040

Result: 7 keys on 3 levels; every internal node has 2 children; all leaves at the same depth. A B-tree grows upward: its height increases only when the root splits.

Multiway Search Trees

B+ Trees

2040510202530405060leaves form a linked list: a range query reads left to right
Data in the leavesAll keys (and records) live in the leaves; internal nodes hold only routing keys.
Linked leavesThe leaves are linked together, so range queries such as "between 20 and 50" are fast.
Wider internal nodesInternal nodes carry no records, so more keys fit in a block → a shallower tree.

Most relational database indexes are B+ trees.

Multiway Search Trees · Step by step

Range Query in a B+ Tree: 10 to 45

2040510202530405060query: 10 ≤ key ≤ 45result: —

Start at the root: 10 < 20 → the leaf holding the lower bound 10 is under the leftmost child. (Just like a search: O(log n) levels.)

2040510202530405060query: 10 ≤ key ≤ 45result: 10

First leaf, [5 10]: skip 5 < 10 and take 10. At the end of the leaf → follow the next-leaf link (without climbing back up).

2040510202530405060query: 10 ≤ key ≤ 45result: 10 20 25 30

Second leaf: 20, 25, and 30 are all in range → take them. Follow the link again.

2040510202530405060query: 10 ≤ key ≤ 45result: 10 20 25 30 40

Third leaf: take 40; 50 > 45 → stop. Result: 10 20 25 30 40.

2040510202530405060query: 10 ≤ key ≤ 45result: 10 20 25 30 40

Cost: one descent, O(log n), plus the leaves read. A B-tree would have to climb up and down through internal nodes again and again; the B+ tree's leaf chain gives O(log n + k) (k: number of results).

Summary

Summary: Operation Costs in Search Trees

StructureHeightSearchInsertDeleteHow it stays balanced
BSTlog n … nO(h)O(h)O(h)it doesn't; worst case O(n)
AVL≤ 1.44 log nO(log n)O(log n)O(log n)bf ∈ {−1, 0, 1}; LL, RR, LR, RL rotations
Red-black≤ 2 log(n+1)O(log n)O(log n)O(log n)recoloring + at most 2–3 rotations
SplayunboundedO(log n)*O(log n)*O(log n)*accessed node moves to the root by zig / zig-zig / zig-zag
B-tree / B+≈ logm nO(log n)O(log n)O(log n)overflowing nodes split; all leaves at the same depth

* Amortized; one operation can take O(n). In every structure, inorder traversal is O(n) and yields the keys sorted; min/max are O(h).

BST propertyleft < root < right, for whole subtrees.
Deletionleaf → remove; one child → bypass; two children → successor/predecessor.
RotationO(1); preserves the inorder sequence and changes heights.

Summary

Interactive Simulators

Binary Search Tree SimulatorSearch, insertion, the three deletion cases, and traversals.
BST Traversal SimulatorInsertion, search, deletion; preorder, inorder, postorder, and level-order traversals.
AVL Tree SimulatorBalance factors and the LL / RR / LR / RL rotations.
Red-Black Tree SimulatorInsertion with recoloring and rotations.
Splay Tree SimulatorBringing the accessed node to the root with zig, zig-zig, and zig-zag.
B-Tree SimulatorHow nodes split as keys are added and the tree grows in balance.
B+ Tree SimulatorA multiway tree that keeps its data in linked leaves.

Summary

Common Mistakes

Dropping the return valuedelete(root, 40); breaks the tree when the root is deleted. Write root = delete(root, 40); (likewise for insert).
Checking only the childrenThe BST property covers whole subtrees, so a check must pass the allowed (min, max) range down (isBST).
Comparing objects with ==That compares references. Order keys with compareTo and test equality with equals.
Wrong height-update orderAfter an AVL rotation, update the node that moved down first, then the new root; the reverse gives wrong bf values.
"A BST is always O(log n)"Wrong: sorted input gives h = n − 1. O(log n) is guaranteed only for balanced trees (AVL, red-black).
Forgetting null childrenAn empty subtree has height −1, and in a red-black tree NIL counts as black; hence helpers like h(null) and isRed(null).

Summary

Sample Problems

  1. Insert 50, 30, 70, 20, 40, 60, 80 in order into an empty BST. What is the height? What does an inorder traversal print?
  2. If the same keys arrive in the order 20, 30, 40, 50, 60, 70, 80, what are the height and the search cost?
  3. Delete the root 50 from the tree in problem 1 using the inorder successor. What does the tree look like?
  4. Insert 3, 2, 1 into an empty AVL tree. Which case arises, and which rotation fixes it?
  5. Insert 10, 30, 20 into an empty AVL tree. Which case arises, and what is the resulting tree?
  6. In a red-black tree, the longest root-to-NIL path is at most how many times the shortest? Why?
  7. In what order are the rotations done in a splay tree's zig-zig step?
Show answers
  1. h = 2 (perfectly balanced); inorder: 20 30 40 50 60 70 80.
  2. A chain to the right: h = 6; search is O(n) (7 comparisons in the worst case).
  3. The successor 60 is copied into the root; the 60 left of 70 is deleted: 60(30(20, 40), 70(—, 80)).
  4. bf = 2 at 3 and bf = 1 at its left child 2 → LL; a right rotation at 3 → 2(1, 3).
  5. bf = −2 at 10 and bf = +1 at its right child 30 → RL; right at 30, then left at 10 → 20(10, 30).
  6. Twice: every path has the same number of blacks, and reds cannot be adjacent.
  7. First at the grandparent (g), then at the parent (p).

Summary

Exercises

  1. Write floor(root, x), which returns the largest key ≤ x (or null if there is none).
  2. Write a method that finds the k-th smallest key in a BST. What is its time complexity?
  3. In what order should 1, 2, …, 7 be inserted into an empty BST to get a perfect tree of height 2?
  4. Insert 1, 2, …, 7 in order into an empty AVL tree. What is the final tree? How many rotations are done?
  5. What is the minimum number of nodes in an AVL tree of height 5?
  6. Why does range(root, lo, hi) take O(h + s) rather than O(n)?
Answers / hints
  1. Mirror successor: if x >= root.data, candidate = root; root = root.right;, else go left. O(h).
  2. An inorder traversal with a counter: the k-th node visited is the answer → O(h + k).
  3. The middle first, then the middles of the halves: e.g., 4, 2, 6, 1, 3, 5, 7.
  4. 4(2(1, 3), 6(5, 7)); inserting 3, 5, 6, and 7 each triggers one left rotation → 4 rotations.
  5. N(5) = 1 + N(4) + N(3) = 1 + 12 + 7 = 20.
  6. Left subtrees of nodes below lo and right subtrees of nodes above hi are never visited (pruning).

Summary

Further Reading

  • Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
  • Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, 4th ed., 2022.
  • Sedgewick and Wayne, Algorithms, 4th ed., 2011.

The End

Chapter 10: Search Trees
Next deck: Chapter 11: Priority Queues and Heaps · Study questions · Simulators: Binary Search Tree, BST Traversal, Red-Black Tree, AVL Tree, Splay Tree, B-Tree, B+ Tree · Glossary
1 / 1 Sercan KÜLCÜ. All rights reserved.