What a tree is, the vocabulary every later topic uses, and how a general tree is stored in memory.
Syllabus: Definition and terminology of trees.
1.1At a glance
A tree is a hierarchy: one item at the top (the root), and every other item hangs below exactly one parent.
Problem it solves
Stores data that is naturally nested (folders in folders, chapters in a book) and lets you reach an item by following one path down instead of scanning everything.
The rule
Exactly one root, exactly one parent for every other node, no cycles. So n nodes always have n − 1 edges.
Think of it as
A family tree, a company org chart, or the folder tree in your file explorer.
Use it when
Data has a parent-child relationship, or you need fast search, sorted order or priority.
Not the right tool when
Items have many-to-many links (roads between cities): that is a graph.
Edgesn − 1Pathsexactly one between any two nodesHeightbetween log n and n − 1
Used inFile systemsHTML DOMCompilersDatabase indexesOrg charts
A tree is a finite set of one or more nodes such that (i) there is one specially designated node called the root, and (ii) the remaining nodes are partitioned into n ≥ 0 disjoint sets T1, T2, …, Tn, where each set is itself a tree. T1 … Tn are called the subtrees of the root.
The definition is recursive: a tree is a root plus smaller trees. That is why almost every tree algorithm is written with recursion. A tree is a non-linear, hierarchical data structure: one element can have many successors, but only one predecessor.
A tree with n nodes has exactly n − 1 edges, no cycles, and exactly one path between any two nodes.
Every node except the root has exactly one parent, and each parent link is one edge. So n nodes give n − 1 edges. If you add one more edge you create a cycle; if you remove one you split the tree in two.
Structure
Tree?
Reason
A→B, A→C, C→D
Yes
One root, every other node has one parent
A→B, A→C, B→D, C→D
No
D has two parents, so there are two paths from A to D. This is a graph.
A→B, B→C, C→A
No
Contains a cycle, and there is no root
A→B and separately C→D
No
Two roots. This is a forest (a set of disjoint trees).
Tree vs. graph, linear vs. non-linear
Arrays, stacks, queues and linked lists are linear: every element has at most one next element, so the data sits on a single line. A tree is non-linear: one node can lead to several nodes, so the data spreads out in levels. A tree is a special kind of graph.
Tree
Graph
Root
Exactly one root
No root
Parents
Each node (except the root) has exactly one parent
A node can be connected to any number of nodes
Cycles
Never
Allowed
Edges
Exactly n − 1
Any number, from 0 to n(n − 1)/2 (undirected)
Path between two nodes
Exactly one
Zero, one or many
Structure
Hierarchical (levels)
Network
1.3Terminology
Tap any node below. The panel shows every term for that node, so you can check your understanding on the same tree the exam will give you.
Convention used in these notes: level of the root = 0, and height is counted in edges (a leaf has height 0, an empty tree −1). Some textbooks start levels at 1 and count height in nodes. Either is accepted if you state your convention at the top of the answer.
Term
Definition
In the tree above
Node
The basic element of a tree. It stores a data item together with links (branches) to its children.
13 nodes: A to M
Root
The unique node at the top of the tree that has no parent. Every other node can be reached from it by exactly one path.
A
Edge (branch)
The link that connects a parent node to one of its children. A tree with n nodes has exactly n − 1 edges.
12 edges
Parent
A node that has one or more nodes directly below it. If an edge goes from X down to Y, then X is the parent (predecessor) of Y.
F is the parent of K and L
Child
A node that is directly below another node and connected to it by one edge. If X is the parent of Y, then Y is a child (successor) of X.
K and L are children of F
Siblings
Two or more nodes that have the same parent.
H, I, J (children of D)
Leaf (terminal / external node)
A node with no children, that is, a node whose degree is 0.
E, K, L, G, M, I, J
Internal node (non-terminal)
A node that has at least one child. The root is internal unless it is the only node.
A, B, C, D, F, H
Degree of a node
The number of children of that node (the number of subtrees hanging from it).
deg(D) = 3, deg(C) = 1, deg(E) = 0
Degree of a tree
The maximum degree of any node in the tree.
3
Level
The generation number of a node. The root is at level 0, and if a node is at level l, its children are at level l + 1.
level(F) = 2
Depth of a node
The number of edges on the path from the root to that node. It equals the node's level.
depth(K) = 3
Height of a node
The number of edges on the longest downward path from that node to a leaf. Every leaf has height 0.
height(B) = 2
Height of the tree (depth of the tree)
The height of the root, which is the same as the maximum level of any node. An empty tree has height −1.
3
Path
A sequence of nodes n1, n2, …, nk such that each ni is the parent of ni+1.
A → B → F → K
Path length
The number of edges on a path, which is one less than the number of nodes on it.
A → B → F → K has length 3
Ancestor
Any node on the path from the root to a given node, excluding the node itself. The parent, grandparent and so on up to the root.
Ancestors of M: H, D, A
Descendant
Any node that can be reached by going downward from a given node: its children, their children, and so on.
Descendants of B: E, F, K, L
Subtree
A node together with all of its descendants. Each child of a node is the root of one subtree of that node.
Subtree rooted at F: F, K, L
Forest
A set of n ≥ 0 disjoint trees. Removing the root of a tree leaves a forest of its subtrees.
Remove A → 3 trees rooted at B, C, D
Common mistake: Mixing up depth and height. Depth is measured down from the root (the root has depth 0). Height is measured up from the leaves (every leaf has height 0). The two are equal only for the tree as a whole.
1.4Representing a general tree in memory
A general tree allows any number of children, so a node cannot have a fixed number of pointer fields without waste. There are two standard ways to store it.
(a) Fixed k child pointers per node
Give each node k pointer fields, where k is the degree of the tree. With n nodes there are nk pointer fields, but only n − 1 are used (one per edge). So n(k − 1) + 1 fields hold NULL. For k = 3 and n = 13 that is 27 wasted fields out of 39.
(b) Left-child right-sibling (LCRS)
Every node keeps just two pointers: left → its first child and right → its next sibling. Any general tree becomes a binary tree this way. This is the reason we study binary trees in such detail: they can represent every tree.
struct gnode {
char data;
struct gnode *firstChild; /* left pointer */
struct gnode *nextSibling; /* right pointer */
};
Conversion rule: keep the edge from each parent to its left-most child, delete the parent's edges to all other children, and link the siblings left to right. Then rotate the picture 45° clockwise.
1.5How to read the pictures
The same drawing rules are used on every page, so learn them once here.
Binary trees
A circle is a node and the text inside is its key. A line is an edge: the parent is above, a left child is drawn lower-left and a right child lower-right. Nodes are placed left to right in in-order, so a BST reads sorted from left to right.
Multi-key nodes (m-way, B, B+)
A box with several keys is one node. Pointers leave from the gaps: the left edge is P0, the gap between key 1 and key 2 is P1, and so on. A small dot under a gap is an empty (NULL) pointer.
B+ trees
Grey boxes are index nodes (signposts only), white boxes are leaves (the real data), and the dashed arrows are the next-leaf links.
Small labels
A small number beside a node (+1, 0, −1) is its balance factor. Triangles T1–T4 are whole subtrees. Heaps show the array under the tree. Huffman trees write 0 on left edges and 1 on right edges.
Colours in every visualizer
current nodepath taken so farnew or movedchild, successor or siblingunbalanced, overflow or being deletedvisited or found
1.6Real-life applications
File systems
Every folder is a node and its files and sub-folders are its children. The drive (C:\ or /) is the root.
Why a tree: each file lives in exactly one folder, so the paths never loop.
Web pages (HTML DOM)
The browser turns every page into a tree of tags: <html> is the root, <body> its child, and so on down to each paragraph.
Why a tree: every tag is nested inside exactly one parent tag.
Compilers
Source code becomes a syntax tree; an expression like (a + b) * c becomes an expression tree.
Why a tree: operator precedence is stored in the shape itself.
Organisation charts
A company, a book, or a menu: each item sits under exactly one parent.
Why a tree: the hierarchy is the data.
Indexes, heaps, tries and Huffman codes are trees too, but each one adds a rule of its own. They are drawn on the family map, not repeated here.
1.7The family of trees
Every tree in this unit is a general tree with one extra rule added. Read the map top to bottom: each arrow names the rule that turns one tree into the next. Tap a box to open that part.
A BST is an m-way search tree with m = 2, and an AVL tree plays the same role for binary trees that a B-tree plays for m-way trees: the balanced version. Compare all trees side by side.
1.8Interview and viva questions
Q1Is every tree a graph? Is every graph a tree?
Every tree is a graph: a connected graph with no cycles. Not every graph is a tree: a graph may have cycles, may be disconnected, and has no root.
Q2What are the smallest and largest possible heights of a tree with n nodes?
Largest: n − 1, when the nodes form a chain. Smallest: 1 for any n ≥ 2 if a node may have any number of children (the root with n − 1 children). For a binary tree the smallest height is ⌈log2(n + 1)⌉ − 1.
Q3How do you store a tree whose nodes can have any number of children?
Use the left-child right-sibling form (two pointers per node: first child and next sibling), or give each node a dynamic list or array of children. The first turns any tree into a binary tree.
Q4Why are almost all tree algorithms written with recursion?
Because the definition is recursive: a tree is a root plus smaller trees. Solving the problem for the root usually means solving the same problem on each subtree and combining the answers.
Common mistakes in this part
Mixing up depth (counted down from the root) and height (counted up from the deepest leaf).
Calling a structure with a cycle, or a node with two parents, a tree. Check: n nodes must have exactly n − 1 edges.
Confusing the degree of a node (its children) with the degree of the tree (the maximum over all nodes).
Using level 1 for the root in one answer and level 0 in the next. Pick one convention and state it.
Key points to remember
A tree is a non-linear, hierarchical structure: one root, every other node has exactly one parent, no cycles.
n nodes ⇒ n − 1 edges, and exactly one path between any two nodes.
Degree counts children; depth/level counts edges down from the root; height counts edges up from the deepest leaf.
State your convention: root at level 0, height in edges (leaf = 0).
A general tree is stored as a binary tree using left-child, right-sibling.
Unit IV · Part 2
Binary Tree
Trees where every node has at most two children. Covers the seven types, formulas, array and linked storage, recursive and non-recursive traversals, rebuilding a tree from traversals, and insert, search and delete.
A binary tree is a tree in which every node has at most two children, called the left child and the right child.
Problem it solves
Gives every node the same small, fixed shape (data and two pointers), so every tree algorithm becomes a short recursive function. It is the base of BST, AVL, heap, Huffman and expression trees.
The rule
At most 2 children; left and right are different positions; the tree may be empty.
Think of it as
A knockout tournament bracket, or a chart of yes/no questions.
Use it when
Data splits two ways (an operator and its two operands, a yes/no decision), or as the base for search trees and heaps.
Not the right tool when
You must find keys by value quickly: a plain binary tree has no order, so search checks every node. Use a BST.
InsertO(n) level orderSearchO(n)DeleteO(n)TraversalO(n)SpaceO(n), n + 1 NULL links
Used inExpression treesHuffman codingHeapsDecision treesGame graphics (BSP)
Comes fromGeneral tree limited to 2 childrenLeads toBST (+ order), Heap (+ heap order)
2.2Definition
Definition
A binary tree is a finite set of nodes that is either empty, or consists of a root and two disjoint binary trees called the left subtree and the right subtree.
There are two differences from a general tree. A binary tree can be empty, and the order of children matters: a left child and a right child are different positions even when a node has only one child.
Because every node has the same fixed shape, a binary tree is easy to store, and every algorithm on it is naturally recursive: do the work at the node, then solve the same problem on the left and right subtrees. Any general tree can be turned into a binary tree with the left-child right-sibling method, so a general tree can be stored and processed with the same binary-tree code (its pre-order equals the pre-order of the binary form, and its post-order equals the in-order of the binary form).
2.3Types of binary tree
Each type below has four parts: the definition to write in the exam, a description of what it looks like and where it is used, the formulas it satisfies, and a test to decide whether a given tree is of that type. A C function for each test is at the end of this section.
1. Full binary tree also: strict, proper, 2-tree
Definition
A binary tree in which every node has either 0 or 2 children.
Description
No node has exactly one child. Levels need not be filled and leaves can be at different depths. An expression tree with only binary operators is always full: every operator has two operands and every operand is a leaf.
Formulas
With i internal nodes and L leaves: L = i + 1, n = 2i + 1 = 2L − 1. So n is always odd. For height h: 2h + 1 ≤ n ≤ 2h+1 − 1.
How to check
Visit every node. If any node has exactly one child, it is not full. Quick count test: n must equal 2L − 1.
2. Complete binary tree
Definition
A binary tree in which every level except possibly the last is completely filled, and all nodes of the last level are as far left as possible.
Description
If you number the nodes level by level (0, 1, 2, …), a complete tree uses positions 0 to n − 1 with no gap. That is why it can be stored in an array without waste, and why heaps are complete trees. At most one node has a single child, and that child is a left child.
Formulas
h = ⌊log2 n⌋ · 2h ≤ n ≤ 2h+1 − 1 · leaves = ⌈n/2⌉ · internal nodes = ⌊n/2⌋ · node i (0-based) has a left child only if 2i + 1 < n.
How to check
Index test: give the root position 0 and each child position 2i + 1 or 2i + 2. The tree is complete if every position is less than n. Level-order test: walk the tree with a queue; once you meet the first missing child, every node after that must be a leaf.
3. Perfect binary tree
Definition
A binary tree in which every internal node has exactly two children and all leaves are at the same level.
Description
Every level is completely full. It has the largest possible number of nodes for its height. Every perfect tree is also full, complete and balanced.
Formulas
n = 2h+1 − 1 · leaves = 2h · internal = 2h − 1 · nodes on level l = 2l · h = log2(n + 1) − 1.
How to check
Find the height h and count the nodes n. The tree is perfect exactly when n = 2h+1 − 1. (Equivalently: it is full and all leaves have the same depth.)
4. Skewed binary tree left-skewed or right-skewed
Definition
A binary tree in which every node except the last has only a left child (left-skewed) or only a right child (right-skewed).
Description
The tree is a straight line leaning to one side. A BST becomes skewed when keys are inserted in sorted order, which makes every operation O(n).
Formulas
h = n − 1 (the maximum possible height) · exactly 1 leaf · array storage needs 2n − 1 slots for a right-skewed tree of n nodes (2n−1 for left-skewed), although only n are used.
How to check
Walk down the left links from the root; if any node on the way has a right child, it is not left-skewed. Mirror the test for right-skewed.
5. Degenerate (pathological) binary tree
Definition
A binary tree in which every internal node has exactly one child, on either side.
Description
It behaves like a linked list. A skewed tree is the special case where all the single children are on the same side.
Formulas
h = n − 1 · 1 leaf · n1 = n − 1 nodes with one child · number of different degenerate shapes with n nodes = 2n−1.
How to check
No node may have two children. Equivalently, the height equals n − 1.
6. Balanced (height-balanced) binary tree
Definition
A binary tree in which, for every node, the heights of the left and right subtrees differ by at most 1.
Description
It keeps the height close to log2 n, so search stays fast. AVL trees (Part 4) are balanced BSTs. Complete and perfect trees are always balanced, but a balanced tree need not be complete.
Formulas
|h(left) − h(right)| ≤ 1 at every node · h = O(log n); more precisely h < 1.44 log2(n + 2).
How to check
Compute heights bottom-up (post-order). At each node compare the two subtree heights; if any difference exceeds 1, it is not balanced. Checking only the root is not enough.
3 internal nodes, 4 external (square) nodes
7. Extended binary tree also: 2-tree
Definition
The tree obtained by replacing every empty (NULL) subtree with a special external node, usually drawn as a square. The original nodes are called internal nodes.
Description
After extension every internal node has exactly two children, so the result is a full binary tree. It is used to analyse searching: an unsuccessful search in a BST always ends at an external node.
Formulas
External nodes E = I + 1 (I = internal nodes) · external path length Ep = Ip + 2I.
How to check
Count the NULL links of the original tree: there are always n + 1 of them, and each becomes one external node.
How the types relate
If a tree is …
then it is also …
but not necessarily …
Perfect
Full, complete, balanced
—
Complete
Balanced
Full (a node can have only a left child)
Full
—
Complete or balanced
Skewed
Degenerate
Balanced (never, for n ≥ 3)
Balanced
—
Complete
Try it Binary tree type checker
Enter the tree in array (level-order) form, using - for an empty position. Example: A B C - D means A is the root, B and C its children, B has no left child and D is B's right child.
C functions to test each type
int isFull(struct node *r) {
if (r == NULL) return 1;
if ((r->left == NULL) != (r->right == NULL)) return 0; /* exactly one child */
return isFull(r->left) && isFull(r->right);
}
/* call as isComplete(root, 0, countNodes(root)) */
int isComplete(struct node *r, int i, int n) {
if (r == NULL) return 1;
if (i >= n) return 0; /* position beyond n-1: there is a gap */
return isComplete(r->left, 2 * i + 1, n) && isComplete(r->right, 2 * i + 2, n);
}
int isPerfect(struct node *r) {
int h = height(r), n = countNodes(r);
return n == (1 << (h + 1)) - 1; /* n = 2^(h+1) - 1 */
}
int isLeftSkewed(struct node *r) {
for (; r != NULL; r = r->left)
if (r->right != NULL) return 0;
return 1;
}
int isDegenerate(struct node *r) {
if (r == NULL) return 1;
if (r->left != NULL && r->right != NULL) return 0; /* two children */
return isDegenerate(r->left) && isDegenerate(r->right);
}
/* returns the height, or -2 as soon as some node is unbalanced */
int checkBalanced(struct node *r) {
int lh, rh;
if (r == NULL) return -1;
lh = checkBalanced(r->left); if (lh == -2) return -2;
rh = checkBalanced(r->right); if (rh == -2) return -2;
if (lh - rh > 1 || rh - lh > 1) return -2;
return 1 + (lh > rh ? lh : rh);
}
These use countNodes() and height() from section 2.8. Each returns 1 for yes and 0 for no.
2.4Properties and formulas
Property
Formula
Example
Maximum nodes on level l
2l
Level 3 → 8
Maximum nodes in a tree of height h
2h+1 − 1
h = 3 → 15
Minimum nodes in a tree of height h
h + 1
Skewed tree, h = 3 → 4
Minimum height with n nodes
⌈log2(n+1)⌉ − 1
n = 20 → 4
Leaves vs. nodes with two children
n0 = n2 + 1
10 nodes of degree 2 → 11 leaves
Full tree with i internal nodes
leaves = i + 1, total = 2i + 1
i = 5 → 11 nodes
NULL links in the linked form (n nodes)
n + 1
7 nodes → 8 NULLs
Different binary tree shapes with n nodes
(2n)! / ((n+1)! · n!)
n = 3 → 5 shapes
Proof of n0 = n2 + 1 (frequently asked): Let n = n0 + n1 + n2. Every node except the root has one incoming edge, so edges = n − 1. Counting from the parent side, edges = n1 + 2n2. Therefore n0 + n1 + n2 − 1 = n1 + 2n2, which gives n0 = n2 + 1.
Try it Formula calculator
2.5Array (sequential) representation
Number the positions level by level, left to right, and store the node at position i in tree[i]. A child's position can be calculated, so no pointers are needed. Tap a cell to see the formulas working.
0-based (C arrays)
1-based
Left child of i
2i + 1
2i
Right child of i
2i + 2
2i + 1
Parent of i
⌊(i − 1) / 2⌋
⌊i / 2⌋
A tree of height h needs an array of size 2h+1 − 1. A complete tree fills the array with no gaps, which is why heaps use arrays. A skewed tree wastes almost all of it: a right-skewed tree of 4 nodes has height 3, so it needs 15 slots and uses only 4.
2.6Linked implementation
Each node is created dynamically with malloc and holds the data and two pointers. An empty subtree is simply a NULL pointer, and the whole tree is reached through one pointer, root.
struct node {
int data;
struct node *left; /* address of the left child, NULL if none */
struct node *right; /* address of the right child, NULL if none */
};
struct node *createNode(int x) {
struct node *n = (struct node *)malloc(sizeof(struct node));
n->data = x;
n->left = n->right = NULL;
return n;
}
/* build 1(2(4,5),3) by hand */
struct node *root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
Array
Linked
Memory
Fixed size, decided in advance
Grows and shrinks at run time
Wasted space
Large for skewed or sparse trees
n + 1 NULL pointers
Finding a child
Arithmetic on the index, O(1)
Follow a pointer, O(1)
Finding the parent
⌊(i−1)/2⌋, O(1)
Not possible without an extra parent pointer
Insert / delete
May require shifting or resizing
Change a few pointers
Best for
Complete trees (heaps)
General binary trees, BST, AVL
2.7Traversals
1Concept
A traversal visits every node exactly once. The three depth-first orders differ only in when the root (N) is printed relative to the left (L) and right (R) subtrees.
Traversal
Order
Recursive rule
Used for
Pre-order
N L R
print root, pre(left), pre(right)
Copying a tree, prefix expression
In-order
L N R
in(left), print root, in(right)
Sorted output of a BST, infix expression
Post-order
L R N
post(left), post(right), print root
Deleting or freeing a tree, postfix expression
Level-order
level by level
use a queue (breadth-first)
Printing by levels, finding the shallowest node
Quick method on paper: trace a line around the tree counter-clockwise, starting left of the root. Print a node when you pass its left side for pre-order, its bottom for in-order, and its right side for post-order.
Each traversal visits n nodes once: time O(n). Recursion uses stack space equal to the height: O(h), which is O(log n) for a balanced tree and O(n) for a skewed one.
2Algorithm
Algorithm PREORDER(T)
1. If T = NULL, return.
2. Visit T (print T.data).
3. PREORDER(T.left)
4. PREORDER(T.right)
Algorithm INORDER(T)
1. If T = NULL, return.
2. INORDER(T.left)
3. Visit T.
4. INORDER(T.right)
Algorithm POSTORDER(T)
1. If T = NULL, return.
2. POSTORDER(T.left)
3. POSTORDER(T.right)
4. Visit T.
Algorithm LEVELORDER(T) (uses a queue Q)
1. If T = NULL, return. Enqueue T.
2. While Q is not empty:
p = dequeue(Q); visit p
if p.left ≠ NULL: enqueue p.left
if p.right ≠ NULL: enqueue p.right
3Worked example
Worked example: all four traversals of one tree
Pre-order (N L R)
A B D E C F G
In-order (L N R)
D B E A F C G
Post-order (L R N)
D E B F G C A
Level-order
A B C D E F G
Traversal visualizer
Pick a tree and an order. The stack (or queue) and the output grow step by step, exactly as in the recursive calls.
Try it Traversal visualizer
4C code
Recursive vs. non-recursive: side by side
Recursion uses the system's call stack to remember where to come back to. The non-recursive versions do the same job with an explicit stack that we manage ourselves. Both produce exactly the same output (Program 1 below prints both, so you can compare).
Pre-order · N L R
Recursive
void preorder(struct node *r) {
if (r == NULL) return;
printf("%d ", r->data); /* N */
preorder(r->left); /* L */
preorder(r->right); /* R */
}
Non-recursive (one stack)
void preorderIter(struct node *root) {
struct node *stack[100], *cur;
int top = -1;
if (root == NULL) return;
stack[++top] = root;
while (top != -1) {
cur = stack[top--]; /* pop and visit */
printf("%d ", cur->data);
if (cur->right) stack[++top] = cur->right;
if (cur->left) stack[++top] = cur->left; /* left on top */
}
}
Idea: print a node as soon as it is popped. Push the right child before the left child, because a stack is last-in-first-out and the left subtree must come out first.
In-order · L N R
Recursive
void inorder(struct node *r) {
if (r == NULL) return;
inorder(r->left); /* L */
printf("%d ", r->data); /* N */
inorder(r->right); /* R */
}
Non-recursive (one stack)
void inorderIter(struct node *root) {
struct node *stack[100], *cur = root;
int top = -1;
while (cur != NULL || top != -1) {
while (cur != NULL) { /* go left, push path */
stack[++top] = cur;
cur = cur->left;
}
cur = stack[top--]; /* leftmost unvisited */
printf("%d ", cur->data);
cur = cur->right; /* then right subtree */
}
}
Idea: keep going left and push every node on the way. When you cannot go further left, pop, print, and then move to the popped node's right child.
Post-order · L R N
Recursive
void postorder(struct node *r) {
if (r == NULL) return;
postorder(r->left); /* L */
postorder(r->right); /* R */
printf("%d ", r->data); /* N */
}
Non-recursive (two stacks)
void postorderIter(struct node *root) {
struct node *s1[100], *s2[100], *cur;
int t1 = -1, t2 = -1;
if (root == NULL) return;
s1[++t1] = root;
while (t1 != -1) {
cur = s1[t1--];
s2[++t2] = cur; /* s2 gets N R L */
if (cur->left) s1[++t1] = cur->left;
if (cur->right) s1[++t1] = cur->right;
}
while (t2 != -1) /* reverse: L R N */
printf("%d ", s2[t2--]->data);
}
Idea: the first loop produces the order N R L (a mirror pre-order) into stack S2. Popping S2 reverses it, giving L R N.
Dry run on the tree 1(2(4, 5), 3)
Pre-order (non-recursive)
#
Action
Stack (bottom → top)
Output
1
push root 1
1
—
2
pop 1, print it, push right 3, push left 2
3 2
1
3
pop 2, print it, push right 5, push left 4
3 5 4
1 2
4
pop 4, print it
3 5
1 2 4
5
pop 5, print it
3
1 2 4 5
6
pop 3, print it
—
1 2 4 5 3
In-order (non-recursive)
#
Action
Stack (bottom → top)
Output
1
go left, push 1 2 4
1 2 4
—
2
pop 4, print it, go right to NULL
1 2
4
3
pop 2, print it, go right to 5
1
4 2
4
go left, push 5
1 5
4 2
5
pop 5, print it, go right to NULL
1
4 2 5
6
pop 1, print it, go right to 3
—
4 2 5 1
7
go left, push 3
3
4 2 5 1
8
pop 3, print it, go right to NULL
—
4 2 5 1 3
Post-order (two stacks)
#
Action
S1
S2
1
push root 1 on S1
1
—
2
pop 1 → S2, push 2, push 3 on S1
2 3
1
3
pop 3 → S2
2
1 3
4
pop 2 → S2, push 4, push 5 on S1
4 5
1 3 2
5
pop 5 → S2
4
1 3 2 5
6
pop 4 → S2
—
1 3 2 5 4
7
pop all of S2
—
Output 4 5 2 3 1
Recursive
Non-recursive
Stack used
System call stack (hidden)
Our own array stack (visible)
Code length
3–4 lines, mirrors the definition
Longer, needs push/pop logic
Time
O(n)
O(n)
Extra space
O(h) stack frames
O(h) for pre/in, O(n) for two-stack post
Risk
Stack overflow on very deep (skewed) trees
Fixed array can fill up; size it to the height
Overhead
A function call per node
No function calls, usually a little faster
Application: expression tree
Operators are internal nodes and operands are leaves. The three traversals give the prefix, infix and postfix forms. To build one from a postfix expression: scan left to right; push each operand as a new node; for an operator, pop two nodes (first pop = right child, second = left child), join them under the operator, and push the result.
Pre-order
* + a b − c d (prefix)
In-order
a + b * c − d (add brackets for infix)
Post-order
a b + c d − * (postfix)
2.8Insert (build), search and delete
1Concept
A plain binary tree has no ordering rule, so the operations work level by level to keep the tree compact (close to complete).
Insert
Do a level-order walk with a queue. The first node found with an empty left or right slot receives the new node (left is checked first).
Search
Visit nodes in any traversal order until the key is found. Without ordering, every node may have to be checked: O(n).
Delete
Find the target and the deepest, right-most node (the last node in level order). Copy the deepest node's data into the target, then remove the deepest node.
2Algorithm
Algorithm BT-INSERT(root, x) (level-order insert)
1. If root = NULL, return a new node x.
2. Enqueue root.
3. While the queue is not empty:
t = dequeue()
If t.left = NULL: t.left = new node x; stop. Else enqueue t.left.
If t.right = NULL: t.right = new node x; stop. Else enqueue t.right.
Algorithm BT-DELETE(root, x)
1. Level-order walk: remember the node T holding x and the last node D visited
(the deepest, right-most node) and D's parent P.
2. If T was not found, report "not found".
3. T.data = D.data (copy the deepest node's value into T)
4. Set P's pointer to D to NULL and free D.
3Worked example
Step by step Binary tree: insert, delete and search
One traversal alone does not fix the shape of a binary tree. Both trees below have pre-order A B and level-order A B, yet they are different trees. The in-order sequence is what tells left from right, because it places the left subtree before the root and the right subtree after it.
Which combinations give a unique tree?
Given
Unique tree?
What each sequence tells you
Pre-order + In-order
Yes
Pre-order: first key is the root. In-order: keys left of the root form the left subtree, keys right of it form the right subtree.
Post-order + In-order
Yes
Post-order: last key is the root. In-order splits as above.
Level-order + In-order
Yes
Level-order: the earliest key of a group is that group's root. In-order splits.
Pre-order + Post-order
Only for a full tree
Both give the root, and pre-order's second key is the left child. But a node with one child cannot be placed left or right.
Pre + Level, Post + Level
No
No in-order, so left and right cannot be separated (see the two trees above).
Any single traversal
No
Many shapes share the same sequence.
BST: pre-order only (or post-order only)
Yes
The in-order of a BST is always the sorted list of keys, so the second sequence is known for free.
Rule: to rebuild a general binary tree you need the in-order plus any one of pre-, post- or level-order.
Pattern 1 · Pre-order + In-order
Take the next unused key from the front of the pre-order. It is the root of the current part.
Find it in the in-order segment. Keys to its left form the left subtree, keys to its right form the right subtree.
Build the left subtree first (recursively, with step 1), then the right subtree. Pre-order is N L R, so its keys are used in exactly this order.
A segment with no keys is an empty subtree (NULL). A segment with one key is a leaf.
Pattern 2 · Post-order + In-order
Take the next unused key from the end of the post-order (read it backwards). It is the root of the current part.
Split the in-order segment around that key into left and right parts.
Build the right subtree first, then the left. Post-order read backwards is N R L, so this matches the order in which its keys come.
Pattern 3 · Level-order + In-order
For the current in-order segment, scan the level-order from the start. The first key that belongs to this segment is its root (the node nearest the top).
Split the in-order segment around that root.
Repeat for the left part and then the right part, always scanning the full level-order again. (Equivalently, the level-order of each subtree is the level-order list with the other keys removed.)
Pattern 4 · Pre-order + Post-order (full binary tree only)
The root is the first key of pre-order, which is also the last key of post-order.
The second key of pre-order is the root of the left subtree. Find it in the post-order: that key and everything before it (in the current post-order range) is the left subtree.
The remaining keys, except the root, form the right subtree. Recurse on both.
If the right part turns out empty, the node has only one child and the answer is not unique.
Why the tree must be full
Pre-order A B C and post-order C B A fit all four trees below. This is why the pattern only works for full trees.
Pattern 5 · BST from one traversal
Sort the keys. For a BST, this sorted list is the in-order.
Now use Pattern 1 (given pre-order) or Pattern 2 (given post-order).
Shortcut: insert the pre-order keys one by one into an empty BST in the given order. This gives the same tree. (It does not work with post-order; use Pattern 2 there, or insert the post-order keys in reverse.)
Run the methods on examples
Choose a method and one of its examples, then press Next. Each step places one node: the colours in the two sequences show the root, its left part and its right part, and the solution table on the right is exactly what you write in the answer.
Step by step Rebuild the tree
Always verify: after drawing your answer, write its other traversal (for example the post-order) and compare. If it does not match the question, you split a segment wrongly.
2.10Real-life applications
Compilers and calculators
An arithmetic expression is stored as an expression tree: operators inside, operands at the leaves. A post-order walk evaluates it; pre-order and post-order give prefix and postfix forms.
Why binary: every operator (+, −, *, /) has exactly two operands.
Compression (ZIP, JPEG, MP3)
A Huffman tree is a binary tree whose left/right turns are the 0/1 bits of each character's code.
Why binary: one bit = one of two choices.
Priority queues
A binary heap is a complete binary tree stored in an array; Java's PriorityQueue and C++'s priority_queue are heaps.
Why binary: a complete binary tree fits an array with no gaps.
Machine learning
A decision tree asks one yes/no question at each internal node ("age > 30?") and gives an answer at each leaf.
Why binary: each question has two outcomes.
3D games and graphics
Binary space partitioning (BSP) trees split a scene again and again into two halves to decide what to draw first; the game Doom (1993) used them.
Why binary: each split has a front side and a back side.
Network routing
A binary trie walks the bits of an IP address (0 = left, 1 = right) to find the longest matching route prefix.
Why binary: an address is a string of bits.
2.11Interview and placement questions
Questions asked in viva and interviews
Q1Full, complete and perfect binary tree: what is the difference?
Full: every node has 0 or 2 children. Complete: every level is filled except possibly the last, which is filled from the left. Perfect: every internal node has 2 children and all leaves are on one level (so it is both full and complete). A heap must be complete; an expression tree is full.
Q2How many nodes can a binary tree of height h have, and what is the minimum height for n nodes?
Between h + 1 (a chain) and 2h+1 − 1 (perfect). With n nodes the height is at least ⌈log2(n + 1)⌉ − 1 and at most n − 1.
Q3Prove that the number of leaves is one more than the number of nodes with two children.
n = n0 + n1 + n2 and edges = n − 1. Counting edges from the parents, edges = n1 + 2n2. Equating gives n0 = n2 + 1 (full proof in 2.4).
Q4How many different binary trees can be made with n nodes?
Unlabelled shapes: the Catalan number (2n)! / ((n + 1)! n!), so 1, 2, 5, 14, 42 for n = 1…5. With n distinct labels each shape can be labelled in n! ways, so n! × Catalan(n).
Q5Which pairs of traversals identify a binary tree uniquely, and why?
In-order together with pre-order, post-order or level-order. Only in-order separates the left subtree from the right one; pre + post works only for a full tree (2.9).
Q6Recursive or iterative traversal: which is better?
Both are O(n) time and O(h) space. The recursive one is shorter; the iterative one avoids function-call overhead and deep recursion on a skewed tree. Morris traversal does in-order in O(1) extra space by temporarily threading the tree.
Q7Why does level-order use a queue and not a stack?
Nodes must come out in the order they were discovered (first in, first out), level by level. A stack gives last in, first out, which is depth-first order instead.
Q8When is the array representation better than the linked one?
For a complete tree (a heap): no pointers, O(1) parent and child by index arithmetic, no gaps. For a skewed tree the array wastes almost all of its 2h+1 − 1 cells, so linked is better.
Q9How do you evaluate an expression tree?
Post-order: evaluate the left subtree, then the right subtree, then apply the operator at the node. A leaf returns its operand. Time O(n).
Q10BFS or DFS on a tree: when would you choose each?
BFS (level-order) for anything "by level": shallowest node, left or right view, level sums, minimum depth. DFS (pre, in, post) for anything built from subtree answers: height, diameter, path sums, checking a property.
Coding problems (tested C programs)
Every program below builds its tree from a level-order array (children of index i at 2i + 1 and 2i + 2, −1 = empty), exactly the array representation of 2.5. All were compiled with gcc and their real output is shown.
C2Are two trees identical?EasyRecursion on two trees
Return true when two trees have the same shape and the same key in every position.
Idea. Both empty → same; exactly one empty → different; otherwise the roots must match and both pairs of subtrees must be identical.
Time O(n) · Space O(h)
Show the C program and its output
/* Q2. Are two binary trees identical (same shape and same keys)? */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *build(int a[], int n, int i) { /* level-order array, -1 = empty */
struct node *r;
if (i >= n || a[i] == -1) return NULL;
r = newNode(a[i]);
r->left = build(a, n, 2 * i + 1);
r->right = build(a, n, 2 * i + 2);
return r;
}
int identical(struct node *a, struct node *b) {
if (a == NULL && b == NULL) return 1; /* both empty */
if (a == NULL || b == NULL) return 0; /* only one is empty */
return a->data == b->data &&
identical(a->left, b->left) &&
identical(a->right, b->right);
}
int main(void) {
int x[] = {1, 2, 3, 4}, y[] = {1, 2, 3, 4}, z[] = {1, 2, 3, -1, 4};
struct node *t1 = build(x, 4, 0), *t2 = build(y, 4, 0), *t3 = build(z, 5, 0);
printf("t1 vs t2: %s\n", identical(t1, t2) ? "identical" : "different");
printf("t1 vs t3: %s\n", identical(t1, t3) ? "identical" : "different"); /* 4 is a right child in t3 */
return 0;
}
Output
t1 vs t2: identical
t1 vs t3: different
C3Diameter of a binary treeMediumPost-order, answer built from subtrees
The diameter is the number of edges on the longest path between any two nodes. It need not pass through the root.
Idea. The longest path that bends at a node uses its left height plus its right height. One post-order pass computes every height and keeps the best sum, so it is O(n) instead of O(n²).
Time O(n) · Space O(h)
Show the C program and its output
/* Q3. Diameter: the number of edges on the longest path between any two nodes */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *build(int a[], int n, int i) { /* level-order array, -1 = empty */
struct node *r;
if (i >= n || a[i] == -1) return NULL;
r = newNode(a[i]);
r->left = build(a, n, 2 * i + 1);
r->right = build(a, n, 2 * i + 2);
return r;
}
int best = 0; /* longest path found so far (edges) */
/* returns the height of r counted in nodes (empty = 0) and updates best on the way */
int depth(struct node *r) {
int l, rt;
if (r == NULL) return 0;
l = depth(r->left);
rt = depth(r->right);
if (l + rt > best) best = l + rt; /* longest path that bends at r */
return 1 + (l > rt ? l : rt);
}
int main(void) {
/* 1(2(4(8,),5(,9)),) : the longest path 8-4-2-5-9 does not pass through the root */
int a[] = {1, 2, -1, 4, 5, -1, -1, 8, -1, -1, 9};
struct node *root = build(a, 11, 0);
depth(root);
printf("Diameter = %d edges\n", best);
return 0;
}
Output
Diameter = 4 edges
C4Lowest common ancestor (LCA)MediumRecursion, return values
Find the deepest node that has both keys a and b in its subtree (a node counts as its own ancestor).
Idea. If the current node is a or b, return it. Otherwise ask both subtrees. If both answers are non-NULL, the keys are on different sides and this node is the LCA; otherwise pass up the non-NULL answer.
Time O(n) · Space O(h)
Show the C program and its output
/* Q4. Lowest common ancestor of two keys in a binary tree (no ordering) */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *build(int a[], int n, int i) { /* level-order array, -1 = empty */
struct node *r;
if (i >= n || a[i] == -1) return NULL;
r = newNode(a[i]);
r->left = build(a, n, 2 * i + 1);
r->right = build(a, n, 2 * i + 2);
return r;
}
/* assumes both keys are present in the tree */
struct node *lca(struct node *r, int a, int b) {
struct node *L, *R;
if (r == NULL || r->data == a || r->data == b) return r;
L = lca(r->left, a, b);
R = lca(r->right, a, b);
if (L != NULL && R != NULL) return r; /* one key on each side: r is the LCA */
return L != NULL ? L : R; /* both keys are on one side */
}
int main(void) {
/* 1(2(4,5(7,8)),3(6,)) */
int a[] = {1, 2, 3, 4, 5, 6, -1, -1, -1, 7, 8};
struct node *root = build(a, 11, 0);
printf("LCA(7, 4) = %d\n", lca(root, 7, 4)->data);
printf("LCA(8, 6) = %d\n", lca(root, 8, 6)->data);
printf("LCA(5, 8) = %d\n", lca(root, 5, 8)->data);
return 0;
}
Output
LCA(7, 4) = 2
LCA(8, 6) = 1
LCA(5, 8) = 5
C5Left view and right viewEasyLevel order with a queue
Print the node you would see from the left side of each level, and from the right side.
Idea. Level-order, processing one level at a time (the queue size at the start of a level is the level width). The first node of each level is the left view, the last is the right view.
Time O(n) · Space O(width)
Show the C program and its output
/* Q5. Left view and right view: the first and last node of every level */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *build(int a[], int n, int i) { /* level-order array, -1 = empty */
struct node *r;
if (i >= n || a[i] == -1) return NULL;
r = newNode(a[i]);
r->left = build(a, n, 2 * i + 1);
r->right = build(a, n, 2 * i + 2);
return r;
}
void views(struct node *root) {
struct node *q[100], *t;
int front = 0, rear = 0, size, i, left[20], right[20], lv = 0;
if (root == NULL) return;
q[rear++] = root;
while (front < rear) {
size = rear - front; /* number of nodes on this level */
for (i = 0; i < size; i++) {
t = q[front++];
if (i == 0) left[lv] = t->data; /* first on the level */
if (i == size - 1) right[lv] = t->data; /* last on the level */
if (t->left) q[rear++] = t->left;
if (t->right) q[rear++] = t->right;
}
lv++;
}
printf("Left view: "); for (i = 0; i < lv; i++) printf("%d ", left[i]);
printf("\nRight view: "); for (i = 0; i < lv; i++) printf("%d ", right[i]);
printf("\n");
}
int main(void) {
/* 1(2(,5(7,)),3(6,)) */
int a[] = {1, 2, 3, -1, 5, 6, -1, -1, -1, 7};
views(build(a, 10, 0));
return 0;
}
Output
Left view: 1 2 5 7
Right view: 1 3 6 7
C6Root-to-leaf paths with a given sumMediumBacktracking
Print every path from the root down to a leaf whose keys add up to the target.
Idea. Carry the path and its running sum downwards. At a leaf compare the sum with the target. Because the length and sum are passed by value, returning from a call automatically removes that node from the path.
Time O(n · h) including printing · Space O(h)
Show the C program and its output
/* Q6. Print every root-to-leaf path whose keys add up to a given sum */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *build(int a[], int n, int i) { /* level-order array, -1 = empty */
struct node *r;
if (i >= n || a[i] == -1) return NULL;
r = newNode(a[i]);
r->left = build(a, n, 2 * i + 1);
r->right = build(a, n, 2 * i + 2);
return r;
}
int path[100];
void paths(struct node *r, int target, int len, int sum) {
int i;
if (r == NULL) return;
path[len++] = r->data; /* add this node to the current path */
sum += r->data;
if (r->left == NULL && r->right == NULL && sum == target) {
for (i = 0; i < len; i++) printf("%d ", path[i]);
printf("\n");
}
paths(r->left, target, len, sum); /* len and sum are copies, so returning */
paths(r->right, target, len, sum); /* automatically "removes" this node */
}
int main(void) {
/* 5(4(11(7,2),),8(13,4(5,1))) */
int a[] = {5, 4, 8, 11, -1, 13, 4, 7, 2, -1, -1, -1, -1, 5, 1};
printf("Paths with sum 22:\n");
paths(build(a, 15, 0), 22, 0, 0);
return 0;
}
Output
Paths with sum 22:
5 4 11 2
5 8 4 5
C7Zig-zag (spiral) level orderMediumTwo stacks
Print level 0 left to right, level 1 right to left, level 2 left to right, and so on.
Idea. Pop nodes of the current level from one stack and push their children onto the other. Pushing left-then-right on one level and right-then-left on the next reverses the direction each time.
Time O(n) · Space O(width)
Show the C program and its output
/* Q7. Zig-zag (spiral) level order: left to right, then right to left, alternately */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *build(int a[], int n, int i) { /* level-order array, -1 = empty */
struct node *r;
if (i >= n || a[i] == -1) return NULL;
r = newNode(a[i]);
r->left = build(a, n, 2 * i + 1);
r->right = build(a, n, 2 * i + 2);
return r;
}
void zigzag(struct node *root) {
struct node *s1[100], *s2[100], *t;
int t1 = -1, t2 = -1;
if (root == NULL) return;
s1[++t1] = root;
while (t1 >= 0 || t2 >= 0) {
while (t1 >= 0) { /* this level: left to right */
t = s1[t1--]; printf("%d ", t->data);
if (t->left) s2[++t2] = t->left; /* push left first ... */
if (t->right) s2[++t2] = t->right; /* ... so right pops first */
}
while (t2 >= 0) { /* next level: right to left */
t = s2[t2--]; printf("%d ", t->data);
if (t->right) s1[++t1] = t->right;
if (t->left) s1[++t1] = t->left;
}
}
printf("\n");
}
int main(void) {
int a[] = {1, 2, 3, 4, 5, 6, 7};
printf("Zig-zag: ");
zigzag(build(a, 7, 0));
return 0;
}
Output
Zig-zag: 1 3 2 4 5 6 7
C8Build the tree from pre-order and in-orderMediumDivide and conquer
Given the pre-order and in-order sequences, rebuild the tree (the method of 2.9 in code).
Idea. The next unused pre-order key is the root. Its position in the in-order splits the remaining keys into the left and right subtrees; build the left one first because pre-order is N L R.
Time O(n²) with a linear search (O(n) with a hash map of positions) · Space O(n)
Show the C program and its output
/* Q8. Build the binary tree from its pre-order and in-order sequences */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
int pre[] = {1, 2, 4, 7, 3, 5, 6, 8};
int in[] = {4, 7, 2, 1, 5, 3, 8, 6};
int pi = 0; /* next unused pre-order position */
struct node *buildTree(int lo, int hi) { /* build the subtree of in[lo..hi] */
struct node *r;
int k;
if (lo > hi) return NULL;
r = newNode(pre[pi++]); /* next pre-order key is the root */
for (k = lo; in[k] != r->data; k++) /* find the root in the in-order */
;
r->left = buildTree(lo, k - 1); /* keys left of it */
r->right = buildTree(k + 1, hi); /* keys right of it */
return r;
}
void postorder(struct node *r) { if (r) { postorder(r->left); postorder(r->right); printf("%d ", r->data); } }
int main(void) {
struct node *root = buildTree(0, 7);
printf("Post-order of the rebuilt tree: ");
postorder(root);
printf("\n");
return 0;
}
Output
Post-order of the rebuilt tree: 7 4 2 5 8 6 3 1
2.12Code tracer and complete program
Every function of this part in one program, first as a line-by-line tracer and then as the complete source. The traversal visualizer is in 2.7; the lab for insert, search and delete is in 2.8.
Code tracer: run Program 1 line by line
Choose any function, type your own keys and press Next. The highlighted line is the statement being executed, the call stack shows every call that is still waiting to finish, and the variables, output and trace table change with each step. Use Next key ⏭ to jump to the end of the current key. Traversals, search and height accept a tree shape such as A(B(D,E),C(,F)).
Trace Program 1, inside the computer
Program 1 · Binary tree: insert, delete, search, recursive and non-recursive traversals, count
/* Binary Tree: level-order insert, traversals, search, delete, count, height */
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
struct node *left, *right;
};
struct node *createNode(int x) {
struct node *n = (struct node *)malloc(sizeof(struct node));
n->data = x;
n->left = n->right = NULL;
return n;
}
/* simple array queue of node pointers */
struct node *q[100];
int front, rear;
void enqueue(struct node *n) { q[rear++] = n; }
struct node *dequeue(void) { return q[front++]; }
int isEmpty(void) { return front == rear; }
/* insert at the first free slot in level order (keeps the tree compact) */
struct node *insert(struct node *root, int x) {
struct node *t;
if (root == NULL) return createNode(x);
front = rear = 0;
enqueue(root);
while (!isEmpty()) {
t = dequeue();
if (t->left == NULL) { t->left = createNode(x); break; }
else enqueue(t->left);
if (t->right == NULL) { t->right = createNode(x); break; }
else enqueue(t->right);
}
return root;
}
void preorder(struct node *r) { if (r) { printf("%d ", r->data); preorder(r->left); preorder(r->right); } }
void inorder(struct node *r) { if (r) { inorder(r->left); printf("%d ", r->data); inorder(r->right); } }
void postorder(struct node *r) { if (r) { postorder(r->left); postorder(r->right); printf("%d ", r->data); } }
/* ---------- non-recursive versions (explicit stack) ---------- */
/* PRE-ORDER (N L R) */
void preorderIter(struct node *root) {
struct node *stack[100], *cur;
int top = -1;
if (root == NULL) return;
stack[++top] = root; /* start with the root */
while (top != -1) {
cur = stack[top--]; /* pop and visit */
printf("%d ", cur->data);
if (cur->right) stack[++top] = cur->right; /* push RIGHT first ... */
if (cur->left) stack[++top] = cur->left; /* ... so LEFT pops first */
}
}
/* IN-ORDER (L N R) */
void inorderIter(struct node *root) {
struct node *stack[100], *cur = root;
int top = -1;
while (cur != NULL || top != -1) {
while (cur != NULL) { /* go left as far as possible */
stack[++top] = cur;
cur = cur->left;
}
cur = stack[top--]; /* no left child left: pop */
printf("%d ", cur->data); /* visit */
cur = cur->right; /* then the right subtree */
}
}
/* POST-ORDER (L R N), two stacks */
void postorderIter(struct node *root) {
struct node *s1[100], *s2[100], *cur;
int t1 = -1, t2 = -1;
if (root == NULL) return;
s1[++t1] = root;
while (t1 != -1) {
cur = s1[t1--];
s2[++t2] = cur; /* s2 collects N R L */
if (cur->left) s1[++t1] = cur->left;
if (cur->right) s1[++t1] = cur->right;
}
while (t2 != -1) /* reversed: L R N */
printf("%d ", s2[t2--]->data);
}
void levelorder(struct node *root) {
struct node *t;
if (root == NULL) return;
front = rear = 0;
enqueue(root);
while (!isEmpty()) {
t = dequeue();
printf("%d ", t->data);
if (t->left) enqueue(t->left);
if (t->right) enqueue(t->right);
}
}
/* no ordering, so search must look everywhere: O(n) */
int search(struct node *r, int key) {
if (r == NULL) return 0;
if (r->data == key) return 1;
return search(r->left, key) || search(r->right, key);
}
/* delete: copy the deepest right-most node into the target, then remove the deepest node */
struct node *deleteNode(struct node *root, int key) {
struct node *t, *target = NULL, *last = NULL, *lastParent = NULL;
if (root == NULL) return NULL;
if (root->left == NULL && root->right == NULL) {
if (root->data == key) { free(root); return NULL; }
return root;
}
front = rear = 0;
enqueue(root);
while (!isEmpty()) {
t = dequeue();
if (t->data == key && target == NULL) target = t;
if (t->left) { lastParent = t; enqueue(t->left); }
if (t->right) { lastParent = t; enqueue(t->right); }
last = t; /* last node dequeued = deepest right-most */
}
if (target == NULL) { printf("%d not found\n", key); return root; }
target->data = last->data;
if (lastParent->right == last) lastParent->right = NULL;
else lastParent->left = NULL;
free(last);
return root;
}
int countNodes(struct node *r) { return r ? 1 + countNodes(r->left) + countNodes(r->right) : 0; }
int countLeaves(struct node *r) {
if (r == NULL) return 0;
if (r->left == NULL && r->right == NULL) return 1;
return countLeaves(r->left) + countLeaves(r->right);
}
int height(struct node *r) { /* in edges: empty = -1, leaf = 0 */
int lh, rh;
if (r == NULL) return -1;
lh = height(r->left);
rh = height(r->right);
return 1 + (lh > rh ? lh : rh);
}
int main(void) {
struct node *root = NULL;
int ch, x;
while (1) {
printf("\n1.Insert 2.Delete 3.Search 4.Traversals 5.Count/Height 6.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("Value: "); scanf("%d", &x); root = insert(root, x); break;
case 2: printf("Value: "); scanf("%d", &x); root = deleteNode(root, x); break;
case 3: printf("Value: "); scanf("%d", &x);
printf(search(root, x) ? "Found\n" : "Not found\n"); break;
case 4: printf("Recursive Pre : "); preorder(root);
printf("\nNon-recursive Pre : "); preorderIter(root);
printf("\nRecursive In : "); inorder(root);
printf("\nNon-recursive In : "); inorderIter(root);
printf("\nRecursive Post: "); postorder(root);
printf("\nNon-recursive Post: "); postorderIter(root);
printf("\nLevel-order : "); levelorder(root); printf("\n"); break;
case 5: printf("Nodes=%d Leaves=%d Height=%d\n", countNodes(root), countLeaves(root), height(root)); break;
case 6: return 0;
}
}
return 0;
}
Calling a tree "complete" when it only has every node with 0 or 2 children (that is full).
Mixing 0-based (2i+1, 2i+2) and 1-based (2i, 2i+1) array formulas in the same answer.
Expecting the in-order of an ordinary binary tree to be sorted. Only a BST gives sorted in-order.
Trying to rebuild a tree from pre-order + post-order when it is not full.
Forgetting the if (r == NULL) return; base case in recursive traversals.
In non-recursive pre-order, pushing the left child before the right child (the output comes out wrong).
Key points to remember
Binary tree: each node has at most 2 children, and left ≠ right.
Full: 0 or 2 children · Complete: filled level by level from the left · Perfect: all levels full · Skewed/degenerate: one child per node.
Key formulas: max nodes 2h+1 − 1 · n0 = n2 + 1 · n + 1 NULL links · array children at 2i+1 and 2i+2.
Pre = N L R, In = L N R, Post = L R N, Level = queue. All are O(n).
Non-recursive traversals replace the call stack with our own stack (two stacks for post-order).
To rebuild a tree you need the in-order plus pre-, post- or level-order. Pre + post works only for a full tree.
Plain binary tree: insert at the first free slot (level order), delete by copying the deepest node, search is O(n).
Unit IV · Part 3
Binary Search Tree
A binary tree with an ordering rule, so every comparison lets you skip one whole subtree.
Syllabus: BST: insert, search, delete.
3.1At a glance
A binary search tree is a binary tree with one ordering rule: every key in the left subtree is smaller than the node, every key in the right subtree is larger.
Problem it solves
Searching a plain binary tree checks every node. With the order rule, one comparison at a node tells you which whole subtree to ignore, so search, insert and delete follow one path from the root.
The rule
left subtree < node < right subtree, at every node; keys are distinct.
Think of it as
The "guess my number" game: each answer (higher / lower) throws away half of the remaining numbers.
Use it when
You need search, insert, delete and sorted order, min / max or range queries, and keys arrive in random order.
Not the right tool when
Keys may arrive sorted: the tree becomes a chain and every operation is O(n). Use an AVL or red-black tree.
SearchO(h)InsertO(h)DeleteO(h)hlog n (random) to n − 1 (sorted)Sorted outputO(n) in-order
Used inSymbol tablesOrdered sets and mapsRange queriesTree sort
A binary search tree is a binary tree that is either empty or satisfies: (i) every key in the left subtree of a node is smaller than the node's key; (ii) every key in the right subtree is larger; (iii) the left and right subtrees are themselves BSTs. Keys are distinct.
The in-order traversal of a BST always gives the keys in ascending order.
Why this rule helps: binary search on a sorted array halves the search space with each comparison, but inserting into an array means shifting elements. A BST keeps the same halving idea in a linked structure: one comparison at a node tells you which whole subtree can be ignored, and a new key is added by changing one pointer.
Properties of a BST
The minimum key is the left-most node and the maximum is the right-most node.
Every subtree of a BST is itself a BST.
Search, insert and delete each follow one path from the root, so they cost O(h), where h is the height.
The shape depends on the order of insertion: the same keys can give a height of log2 n or n − 1.
Duplicate keys are not stored in these notes (the program reports them). If duplicates are needed, a count field is kept in the node, or equal keys are always sent to one side.
A BST node has the same structure as a binary tree node (2.6): data, a left pointer and a right pointer. Only the rule about where keys may be placed is new.
Common mistake: Checking only each parent with its two children is not enough. The rule applies to the whole subtree. The in-order test catches this: 20 30 60 50 70 is not sorted.
3.3Insert (build the tree)
1Concept
Start at the root. If the tree is empty, the new key becomes the root.
Compare the new key with the node: smaller → go left, larger → go right, equal → it is already there.
Keep going until the next link is NULL. Attach the new node there as a leaf.
Existing nodes never move, so the order of insertion decides the shape. The walk down is exactly the path a later search will take.
2Algorithm
Algorithm INSERT(root, key)
1. If root = NULL, create a new node with key, set its left and right to NULL,
and return it as the root of this subtree.
2. If key < root.data, root.left = INSERT(root.left, key)
3. Else if key > root.data, root.right = INSERT(root.right, key)
4. Else the key already exists: do nothing.
5. Return root.
3Worked example
Step by step Construct a BST
Insertion order decides the shape. The same seven keys inserted as 20, 30, 40, 50, 60, 70, 80 give a right-skewed tree of height 6 instead of height 2. The keys are identical; only the order differs.
4C code
struct node *insert(struct node *root, int x) {
if (root == NULL) return createNode(x); /* empty spot found */
if (x < root->data) root->left = insert(root->left, x);
else if (x > root->data) root->right = insert(root->right, x);
return root; /* duplicates ignored */
}
3.4Search
1Concept
Search walks the same path that insert walked when the key was added.
Start at the root. If it is NULL, the key is not present.
If key = node's key, it is found.
If key < node's key, search the left subtree; otherwise search the right subtree.
Each comparison moves down one level, so search takes at most h + 1 comparisons: O(h).
2Algorithm
Algorithm SEARCH(root, key)
1. p = root
2. While p ≠ NULL and p.data ≠ key:
if key < p.data: p = p.left
else: p = p.right
3. Return p (NULL means "not found")
3Worked example
Step by step Search a BST
4C code
Recursive
struct node *search(struct node *r, int key) {
if (r == NULL || r->data == key)
return r; /* NULL = not found */
if (key < r->data)
return search(r->left, key);
return search(r->right, key);
}
Iterative
struct node *search(struct node *r, int key) {
while (r != NULL && r->data != key) {
if (key < r->data) r = r->left;
else r = r->right;
}
return r; /* NULL = not found */
}
3.5Delete: three cases
1Concept
First search for the node. What happens next depends on how many children it has.
Case
Action
1. Leaf (no child)
Set the parent's pointer to NULL and free the node.
2. One child
Link the parent directly to that child, bypassing the node, then free it.
3. Two children
Find the in-order successor (smallest key in the right subtree: go right once, then left as far as possible). Copy its key into the node, then delete the successor, which is always Case 1 or Case 2. The in-order predecessor (largest in the left subtree) works equally well.
2Algorithm
Algorithm DELETE(root, key)
1. If root = NULL, the key is not present. Return NULL.
2. If key < root.data, root.left = DELETE(root.left, key); return root.
3. If key > root.data, root.right = DELETE(root.right, key); return root.
4. (key = root.data: this is the node to delete)
a. If root.left = NULL: temp = root.right; free(root); return temp. (cases 1 and 2)
b. If root.right = NULL: temp = root.left; free(root); return temp. (case 2)
c. Otherwise (case 3): succ = leftmost node of root.right.
root.data = succ.data
root.right = DELETE(root.right, succ.data)
5. Return root.
3Worked example
Step by step Delete from a BST: pick a case
4C code
struct node *deleteNode(struct node *root, int key) {
struct node *temp;
if (root == NULL) return NULL; /* not found */
if (key < root->data) root->left = deleteNode(root->left, key);
else if (key > root->data) root->right = deleteNode(root->right, key);
else {
if (root->left == NULL) { temp = root->right; free(root); return temp; }
if (root->right == NULL) { temp = root->left; free(root); return temp; }
temp = root->right; /* two children: */
while (temp->left != NULL) temp = temp->left; /* find successor */
root->data = temp->data; /* copy it up */
root->right = deleteNode(root->right, temp->data); /* remove old copy */
}
return root;
}
3.6Minimum, maximum, successor and predecessor
1Concept
Minimum: start at the root and keep going left. The node with no left child is the minimum. Maximum: keep going right.
The in-order successor of x is the next key after x in sorted order (the smallest key greater than x). The in-order predecessor is the key just before it.
Case 1, x has a right subtree: the successor is the minimum of that right subtree (go right once, then left as far as possible). This is the node used in BST deletion (3.5).
Case 2, x has no right subtree: the successor is the nearest ancestor of x whose left subtree contains x. Walking down from the root towards x, remember every node where you turn left; the last one remembered is the successor. If you never turned left, x is the maximum and has no successor.
The predecessor is the mirror image: the maximum of the left subtree, or the nearest ancestor where you turned right.
2Algorithm
Algorithm MIN(root)
1. p = root
2. While p.left ≠ NULL: p = p.left
3. Return p (MAX is the same with "right" in place of "left")
Algorithm SUCCESSOR(root, key) (works whether or not x has a right subtree)
1. succ = NULL; p = root
2. While p ≠ NULL:
if key < p.data: succ = p; p = p.left (p is a candidate; look for a smaller one)
else: p = p.right (p.data ≤ key: too small)
3. Return succ (NULL: key is the maximum)
3Worked example
Node x
Right subtree?
Successor
How
Predecessor
30
Yes (40)
35
Case 1: min of right subtree 40(35, 45)
25
50 (root)
Yes (70)
60
Case 1: min of right subtree 70(60, 80)
45
25
No
30
Case 2: path 50 (left) → 30 (left) → 20 (right) → 25; last left turn at 30
20
45
No
50
Case 2: path 50 (left) → 30 (right) → 40 (right) → 45; last left turn at 50
Step through the candidate walk on more trees, including keys that are not in the tree:
Step by step Minimum, maximum, successor and predecessor
4C code
struct node *findMin(struct node *r) { while (r && r->left) r = r->left; return r; }
struct node *findMax(struct node *r) { while (r && r->right) r = r->right; return r; }
/* smallest key greater than key (key need not be in the tree) */
struct node *successor(struct node *root, int key) {
struct node *succ = NULL;
while (root != NULL) {
if (key < root->data) { succ = root; root = root->left; } /* candidate: go left */
else root = root->right; /* too small: go right */
}
return succ; /* NULL: no successor */
}
/* largest key smaller than key */
struct node *predecessor(struct node *root, int key) {
struct node *pred = NULL;
while (root != NULL) {
if (key > root->data) { pred = root; root = root->right; } /* candidate: go right */
else root = root->left;
}
return pred;
}
★Try it
Open the code tracer and choose findMin / findMax to watch the loops run on your own keys.
3.7Complexity and the skewed-tree problem
Operation
Best / average (balanced, h ≈ log n)
Worst (skewed, h = n − 1)
Search
O(log n)
O(n)
Insert
O(log n)
O(n)
Delete
O(log n)
O(n)
In-order traversal
O(n)
O(n)
Same keys 10, 20, 30, 40, 50, 60, 70 · search for 70 in both trees
If the keys arrive in random order, the expected search cost is about 1.39 log2 n comparisons, so a BST works well on average. The danger is sorted or nearly sorted input. A BST does nothing to keep itself balanced; AVL trees (Part 4) fix this by checking the balance after every insert and delete and rotating when needed, so the height stays O(log n).
3.8Real-life applications, advantages and comparison
Compilers: symbol tables
A compiler stores every variable name with its type and address; a BST keyed by name finds them in O(log n) on average and can list them alphabetically.
Why a BST: lookups plus sorted listing.
Ordered sets and maps
"Give me all students with a score between 60 and 75", "the next event after 10:30": min, max, successor and range queries come straight from the order.
Why a BST: a hash table cannot answer range or next-larger queries.
Tree sort
Insert all keys, then walk in-order: the output is sorted. O(n log n) on random data.
Why a BST: in-order is always sorted.
The base of real libraries
C++ std::map, Java TreeMap and the Linux kernel's rbtree are balanced BSTs (red-black trees): the BST rule plus a balancing rule.
Why balanced: plain BSTs degrade on sorted input.
Advantages
Search, insert and delete in O(h) · keys come out sorted with an in-order walk · min, max, successor and range queries are easy · grows and shrinks dynamically, no shifting like an array.
Disadvantages
No balance guarantee: sorted input gives O(n) · two pointers per key of extra memory · deletion with two children needs extra work (successor).
BST compared with a sorted array and a linked list
Operation
Sorted array
Linked list
BST (height h)
Search
O(log n) binary search
O(n)
O(h)
Insert
O(n) shifting
O(1) at the front (O(n) to keep it sorted)
O(h)
Delete
O(n) shifting
O(n) to find the node
O(h)
Sorted output
O(n)
O(n log n) unless kept sorted
O(n) in-order
Binary tree vs. binary search tree
Binary tree
Binary search tree
Ordering
None
left < node < right
Search
O(n), must check every node
O(h), one path from the root
Insert position
First free slot in level order
Fixed by the key
In-order output
Any order
Sorted
Duplicates
Allowed
Usually not allowed
3.9Interview and placement questions
Questions asked in viva and interviews
Q1Why is checking each node against its two children not enough to verify a BST?
The rule is about whole subtrees. In 50(30(20, 60), 70), node 30 has 20 < 30 < 60, yet 60 is in the left subtree of 50. Check a range for every node (C1) or check that the in-order is sorted.
Q2What are the best, average and worst heights of a BST with n keys?
Best ⌊log2 n⌋ (balanced), average about 1.39 log2 n comparisons for random insertion order, worst n − 1 (keys inserted in sorted order).
Q3Deleting a node with two children: why the in-order successor, and why does its own deletion stay easy?
The successor is the smallest key larger than the node, so putting it in the node's place keeps left < node < right. It is the leftmost node of the right subtree, so it has no left child: removing it is always case 1 or case 2.
Q4BST or hash table?
A hash table gives O(1) average lookup but no order. A (balanced) BST gives O(log n) lookup plus sorted traversal, min, max, floor, ceil, successor and range queries.
Q5How do you find the k-th smallest key? Can it be faster than O(n)?
In-order walk and stop at the k-th node: O(h + k). If every node also stores the size of its subtree, you can steer left or right and find it in O(h).
Q6Which traversal rebuilds the same BST when you insert its keys in that order?
Pre-order (and level-order): a parent is always inserted before its children. In-order would insert sorted keys and give a chain.
Q7How can duplicate keys be stored?
Either keep a count field in the node, or always send equal keys to one side (for example the right). These notes ignore duplicates.
Q8How many different BSTs can be built from n distinct keys?
The Catalan number (2n)! / ((n + 1)! n!): 1, 2, 5, 14, 42 … Each shape has exactly one valid way to place the keys.
C1Check whether a binary tree is a BSTMediumRecursion with limits
Return true only if the BST rule holds for every node and its whole subtrees.
Idea. Pass down the open interval (lo, hi) that the node's key must lie in. Going left, the upper limit becomes the node's key; going right, the lower limit does.
Time O(n) · Space O(h)
Show the C program and its output
/* Q1. Is a binary tree a valid BST? Every key must lie inside the range allowed by its ancestors */
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *build(int a[], int n, int i) { /* level-order array, -1 = empty */
struct node *r;
if (i >= n || a[i] == -1) return NULL;
r = newNode(a[i]);
r->left = build(a, n, 2 * i + 1);
r->right = build(a, n, 2 * i + 2);
return r;
}
/* all keys of r must satisfy lo < key < hi */
int isBST(struct node *r, long lo, long hi) {
if (r == NULL) return 1;
if (r->data <= lo || r->data >= hi) return 0;
return isBST(r->left, lo, r->data) && /* left side: upper limit becomes r->data */
isBST(r->right, r->data, hi); /* right side: lower limit becomes r->data */
}
int main(void) {
int good[] = {50, 30, 70, 20, 40, 60, 80};
int bad[] = {50, 30, 70, 20, 60}; /* 60 sits in the LEFT subtree of 50 */
printf("Tree 1: %s\n", isBST(build(good, 7, 0), LONG_MIN, LONG_MAX) ? "BST" : "not a BST");
printf("Tree 2: %s\n", isBST(build(bad, 5, 0), LONG_MIN, LONG_MAX) ? "BST" : "not a BST");
return 0;
}
Output
Tree 1: BST
Tree 2: not a BST
C2k-th smallest keyEasyIn-order walk
Find the k-th smallest key in a BST.
Idea. In-order visits keys in increasing order; count the nodes as they are visited and stop at the k-th.
Time O(h + k) · Space O(h)
Show the C program and its output
/* Q2. k-th smallest key in a BST: the k-th node visited by an in-order walk */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *insert(struct node *r, int x) {
if (r == NULL) {
r = malloc(sizeof *r); r->data = x; r->left = r->right = NULL;
return r;
}
if (x < r->data) r->left = insert(r->left, x);
else if (x > r->data) r->right = insert(r->right, x);
return r;
}
int seen; /* nodes visited so far in in-order */
struct node *kth(struct node *r, int k) {
struct node *x;
if (r == NULL) return NULL;
x = kth(r->left, k); /* smaller keys first */
if (x != NULL) return x;
if (++seen == k) return r; /* this node is the k-th */
return kth(r->right, k);
}
int main(void) {
int keys[] = {50, 30, 70, 20, 40, 60, 80}, i, k;
struct node *root = NULL, *x;
for (i = 0; i < 7; i++) root = insert(root, keys[i]);
for (k = 1; k <= 7; k += 3) {
seen = 0; x = kth(root, k);
printf("k = %d -> %d\n", k, x ? x->data : -1);
}
return 0;
}
Output
k = 1 -> 20
k = 4 -> 50
k = 7 -> 80
C3Lowest common ancestor in a BSTEasyUse the order
Find the LCA of two keys in a BST.
Idea. Walk down from the root: if both keys are smaller go left, if both are larger go right; the first node where they split (or that equals one of them) is the LCA. No need to search both subtrees as in a plain binary tree.
Time O(h) · Space O(1)
Show the C program and its output
/* Q3. Lowest common ancestor in a BST: walk down until the two keys split */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *insert(struct node *r, int x) {
if (r == NULL) {
r = malloc(sizeof *r); r->data = x; r->left = r->right = NULL;
return r;
}
if (x < r->data) r->left = insert(r->left, x);
else if (x > r->data) r->right = insert(r->right, x);
return r;
}
struct node *lca(struct node *r, int a, int b) {
while (r != NULL) {
if (a < r->data && b < r->data) r = r->left; /* both smaller: go left */
else if (a > r->data && b > r->data) r = r->right; /* both larger: go right */
else return r; /* they split here, or one of them is r */
}
return NULL;
}
int main(void) {
int keys[] = {50, 30, 70, 20, 40, 60, 80, 35, 45}, i;
struct node *root = NULL;
for (i = 0; i < 9; i++) root = insert(root, keys[i]);
printf("LCA(35, 45) = %d\n", lca(root, 35, 45)->data);
printf("LCA(20, 45) = %d\n", lca(root, 20, 45)->data);
printf("LCA(35, 80) = %d\n", lca(root, 35, 80)->data);
return 0;
}
Print every key between lo and hi in sorted order.
Idea. Do an in-order walk, but go left only if lo is smaller than the node and go right only if hi is larger. Whole subtrees outside the range are never visited.
Time O(h + k), k = keys printed · Space O(h)
Show the C program and its output
/* Q4. Print all keys between lo and hi in sorted order, skipping subtrees that cannot qualify */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *insert(struct node *r, int x) {
if (r == NULL) {
r = malloc(sizeof *r); r->data = x; r->left = r->right = NULL;
return r;
}
if (x < r->data) r->left = insert(r->left, x);
else if (x > r->data) r->right = insert(r->right, x);
return r;
}
int visited = 0;
void range(struct node *r, int lo, int hi) {
if (r == NULL) return;
visited++;
if (lo < r->data) range(r->left, lo, hi); /* smaller keys may qualify */
if (lo <= r->data && r->data <= hi) printf("%d ", r->data);
if (r->data < hi) range(r->right, lo, hi); /* larger keys may qualify */
}
int main(void) {
int keys[] = {50, 30, 70, 20, 40, 60, 80, 10, 25, 35, 45}, i;
struct node *root = NULL;
for (i = 0; i < 11; i++) root = insert(root, keys[i]);
printf("Keys in [28, 62]: ");
range(root, 28, 62);
printf("\nNodes visited: %d of 11\n", visited);
return 0;
}
Output
Keys in [28, 62]: 30 35 40 45 50 60
Nodes visited: 9 of 11
C5Sorted array to a height-balanced BSTEasyDivide and conquer
Build a BST of minimum height from a sorted array.
Idea. The middle element becomes the root; the left half builds the left subtree and the right half the right subtree. Height ⌈log2(n + 1)⌉ − 1.
Time O(n) · Space O(log n)
Show the C program and its output
/* Q5. Build a height-balanced BST from a sorted array: the middle element becomes the root */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *fromSorted(int a[], int lo, int hi) {
struct node *r;
int mid;
if (lo > hi) return NULL;
mid = (lo + hi) / 2;
r = malloc(sizeof *r);
r->data = a[mid];
r->left = fromSorted(a, lo, mid - 1); /* left half -> left subtree */
r->right = fromSorted(a, mid + 1, hi); /* right half -> right subtree */
return r;
}
int height(struct node *r) {
int l, rt;
if (r == NULL) return -1;
l = height(r->left); rt = height(r->right);
return 1 + (l > rt ? l : rt);
}
void preorder(struct node *r) { if (r) { printf("%d ", r->data); preorder(r->left); preorder(r->right); } }
int main(void) {
int a[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
struct node *root = fromSorted(a, 0, 9);
printf("Pre-order: "); preorder(root);
printf("\nHeight: %d (a chain would have height 9)\n", height(root));
return 0;
}
Output
Pre-order: 50 20 10 30 40 80 60 70 90 100
Height: 3 (a chain would have height 9)
C6Floor and ceil of a keyMediumWalk down, remember candidates
Floor(x) is the largest key ≤ x; ceil(x) is the smallest key ≥ x.
Idea. Walk down like a search. For floor, every node smaller than x is a candidate: remember it and go right to look for a bigger one; otherwise go left. Ceil is the mirror image.
Time O(h) · Space O(1)
Show the C program and its output
/* Q6. Floor (largest key <= x) and ceil (smallest key >= x) in a BST, without recursion */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *insert(struct node *r, int x) {
if (r == NULL) {
r = malloc(sizeof *r); r->data = x; r->left = r->right = NULL;
return r;
}
if (x < r->data) r->left = insert(r->left, x);
else if (x > r->data) r->right = insert(r->right, x);
return r;
}
int floorKey(struct node *r, int x) { /* returns -1 if no key is <= x */
int best = -1;
while (r != NULL) {
if (r->data == x) return x;
if (r->data < x) { best = r->data; r = r->right; } /* candidate; look for a bigger one */
else r = r->left;
}
return best;
}
int ceilKey(struct node *r, int x) { /* returns -1 if no key is >= x */
int best = -1;
while (r != NULL) {
if (r->data == x) return x;
if (r->data > x) { best = r->data; r = r->left; } /* candidate; look for a smaller one */
else r = r->right;
}
return best;
}
int main(void) {
int keys[] = {50, 30, 70, 20, 40, 60, 80}, q[] = {45, 60, 15, 85}, i;
struct node *root = NULL;
for (i = 0; i < 7; i++) root = insert(root, keys[i]);
for (i = 0; i < 4; i++) printf("x = %d: floor %d, ceil %d\n", q[i], floorKey(root, q[i]), ceilKey(root, q[i]));
return 0;
}
Output
x = 45: floor 40, ceil 50
x = 60: floor 60, ceil 60
x = 15: floor -1, ceil 20
x = 85: floor 80, ceil -1
3.10Lab, code tracer and complete program
Use the lab to try your own keys, the tracer to watch Program 2 execute line by line, and then read the complete program.
Try it BST lab
Code tracer: run Program 2 line by line
Choose any function, type your own keys and press Next. The highlighted line is the statement being executed, the call stack shows every call that is still waiting to finish, and the variables, output and trace table change with each step. Use Next key ⏭ to jump to the end of the current key.
Trace Program 2, inside the computer
Program 2 · BST: insert, search, delete, min/max, traversals
/* Binary Search Tree: insert, search, delete (3 cases), min, max, traversals */
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
struct node *left, *right;
};
struct node *createNode(int x) {
struct node *n = (struct node *)malloc(sizeof(struct node));
n->data = x;
n->left = n->right = NULL;
return n;
}
struct node *insert(struct node *root, int x) {
if (root == NULL) return createNode(x);
if (x < root->data) root->left = insert(root->left, x);
else if (x > root->data) root->right = insert(root->right, x);
else printf("%d already exists\n", x); /* duplicates ignored */
return root;
}
struct node *search(struct node *root, int key) { /* iterative, O(h) */
while (root != NULL && root->data != key)
root = (key < root->data) ? root->left : root->right;
return root;
}
struct node *findMin(struct node *r) { while (r && r->left) r = r->left; return r; }
struct node *findMax(struct node *r) { while (r && r->right) r = r->right; return r; }
struct node *deleteNode(struct node *root, int key) {
struct node *temp;
if (root == NULL) { printf("%d not found\n", key); return NULL; }
if (key < root->data) root->left = deleteNode(root->left, key);
else if (key > root->data) root->right = deleteNode(root->right, key);
else {
if (root->left == NULL) { /* case 1 (leaf) and case 2 (only right child) */
temp = root->right; free(root); return temp;
}
if (root->right == NULL) { /* case 2 (only left child) */
temp = root->left; free(root); return temp;
}
temp = findMin(root->right); /* case 3: in-order successor */
root->data = temp->data;
root->right = deleteNode(root->right, temp->data);
}
return root;
}
void inorder(struct node *r) { if (r) { inorder(r->left); printf("%d ", r->data); inorder(r->right); } }
void preorder(struct node *r) { if (r) { printf("%d ", r->data); preorder(r->left); preorder(r->right); } }
void postorder(struct node *r) { if (r) { postorder(r->left); postorder(r->right); printf("%d ", r->data); } }
int main(void) {
struct node *root = NULL, *p;
int ch, x, n, i;
while (1) {
printf("\n1.Insert many 2.Search 3.Delete 4.Min/Max 5.Traversals 6.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many keys? "); scanf("%d", &n);
printf("Enter %d keys: ", n);
for (i = 0; i < n; i++) { scanf("%d", &x); root = insert(root, x); }
break;
case 2: printf("Key: "); scanf("%d", &x);
printf(search(root, x) ? "Found\n" : "Not found\n"); break;
case 3: printf("Key: "); scanf("%d", &x); root = deleteNode(root, x); break;
case 4: if (root == NULL) { printf("Empty\n"); break; }
p = findMin(root); printf("Min=%d ", p->data);
p = findMax(root); printf("Max=%d\n", p->data); break;
case 5: printf("In : "); inorder(root);
printf("\nPre : "); preorder(root);
printf("\nPost : "); postorder(root); printf("\n"); break;
case 6: return 0;
}
}
return 0;
}
Sample run (keys 50 30 70 20 40 60 80, delete 30)
In : 20 30 40 50 60 70 80
Pre : 50 30 20 40 70 60 80
Post : 20 40 30 60 80 70 50
-- delete 30 (two children, successor 40) --
In : 20 40 50 60 70 80
Pre : 50 40 20 70 60 80
Min=20 Max=80
Common mistakes in this part
Checking the BST rule only between a parent and its children instead of the whole subtree.
Taking the right child as the in-order successor. It is the left-most node of the right subtree.
In C, calling insert(root->left, x) without storing the result in root->left, so the new node is lost.
Forgetting return root; at the end of recursive insert or delete.
Deleting a two-child node by removing it and leaving both subtrees disconnected.
Key points to remember
BST rule: left subtree < node < right subtree, for every node. In-order gives sorted keys.
Search and insert follow one path from the root: O(h).
Delete: leaf → remove · one child → bypass · two children → copy the in-order successor, then delete it.
Minimum = leftmost node, maximum = rightmost node.
Height is log n if balanced but n − 1 if keys arrive sorted, which is why AVL trees exist.
Unit IV · Part 5
Multiway Tree
Nodes holding several sorted keys and several children. Fewer levels means fewer disk reads, and that is the reason these trees exist.
Syllabus: Multiway trees (m-way search trees).
5.1At a glance
An m-way search tree keeps up to m − 1 sorted keys in each node and up to m children; each child holds only the keys that fall between two neighbouring keys of its parent.
Problem it solves
On a disk, reading one node costs one slow disk access. A binary tree of a million keys is about 20 levels deep, so 20 reads. Putting many keys in one node makes the tree much shorter.
The rule
≤ m children and ≤ m − 1 keys per node, keys in ascending order; child Pi holds keys between Ki and Ki+1.
Think of it as
A dictionary's thumb index: one tab per letter, then a range of words per page header, then the page.
Use it when
You want to understand how disk indexes work, or for a small index that rarely changes.
Not the right tool when
Data keeps changing: nothing keeps it balanced, nodes can stay half empty and the tree can grow lopsided. Use a B-tree.
SearchO(h) node readsInsertO(h)DeleteO(h)hlogm n (best) to n − 1 (worst)
Used inThe idea behind B-treesMulti-level indexesTries (m = alphabet size)
Comes fromBST (a BST is the case m = 2)Leads toB-tree (+ balance rules)
5.2Why go beyond two children?
When data lives on disk, reading one node costs one disk access, which is thousands of times slower than a comparison in memory. A disk block can hold many keys, so it is better to put many keys in one node and read them all in a single access.
Keys stored
Binary tree (balanced) levels
Order-100 tree levels
1,000
10
2
1,000,000
20
3
1,000,000,000
30
5
The height drops from about log2 n to logm n. Each level is one disk read, so a search reads 3 blocks instead of 20.
Real-life picture: a dictionary's thumb index. The first "node" has 26 letter tabs (many keys at once). You open the right tab, then a page header shows a range of words, then you scan one page. Three looks instead of halving the book twenty times.
Multiway tree (general)
A multiway tree (m-ary or k-ary tree) is a tree in which a node may have more than two children. When the keys in every node are kept in sorted order and each child holds only the keys that fall between two neighbouring keys of its parent, it is called an m-way search tree. The rest of this part is about m-way search trees.
5.3m-way search tree: definition
Definition
An m-way search tree (search tree of order m) is either empty or satisfies:
Each node has at most m children and at most m − 1 keys.
The keys in a node are in ascending order: K1 < K2 < … < Kk.
A node with k keys has k + 1 child pointers P0 … Pk. All keys in subtree Pi lie between Ki and Ki+1: subtree P0 holds keys < K1, and subtree Pk holds keys > Kk.
Every subtree Pi is itself an m-way search tree.
A BST is simply a 2-way search tree: 1 key and 2 children per node.
Node structure
A node of an order-4 tree: 3 keys separate 4 pointer ranges.
#define M 4 /* order */
struct mnode {
int count; /* keys in use, at most M-1 */
int key[M - 1];
struct mnode *child[M]; /* child[i]: keys between key[i-1] and key[i] */
};
How to read the m-way pictures
The line Order m = … above the tree is the order: at most m children and m − 1 keys in a node.
One short box is one node. Inside it the cells go pointer, key, pointer, key, …, pointer. P0 is the left side, then the first key, then P1, and the last cell is the right side.
Children hang left to right under the node, the same way a binary tree puts its left child on the left and its right child on the right. The line leaves from that pointer.
A dot under a pointer means there is no child on that side. The child of Pi holds only keys that lie between the two keys beside that pointer.
Example (order 3): after 35 and 60 the root [35 60] is full. 20 < 35 points to P0, which is NULL, so a new node [20] opens there; 80 > 60 points to P2, so [80] opens on the right; P1 stays a dot.
Example: an m-way search tree of order 3
Each node holds at most 2 keys and 3 pointers. Dots are NULL pointers. Check the rule at the root [35, 60]: the left subtree holds keys < 35 (10, 15, 20, 25), the middle holds keys between 35 and 60 (40, 50), and the right holds keys > 60 (70, 80, 90).
5.4Insert (build the tree)
1Concept
Start at the root and find the pointer range where the key belongs.
If the node has room (fewer than m − 1 keys) and that pointer is NULL, insert the key into this node in sorted position. Done.
Otherwise, if that pointer leads to a subtree, go down and repeat.
If the node is full and the pointer is NULL, create a new node containing only this key and hang it on that pointer.
2Algorithm
Algorithm MWAY-INSERT(root, x)
1. If root = NULL, create a node [x] as the root. Stop.
2. p = root
3. Loop:
i = position of the first key ≥ x in p (i = p.count if x is the largest)
If x = p.key[i]: duplicate, stop.
If p.count < m − 1 and p.child[i] = NULL:
insert x into p at position i; stop.
If p.child[i] = NULL: (p is full)
p.child[i] = new node [x]; stop.
p = p.child[i]
3Worked example
Step by step Construct an m-way search tree
Insertion order decides the shape. Compare the first example with "sorted input 10, 20, 30, 40, 50, 60": similar keys give either a bushy tree or a long chain on the right. An m-way search tree has no rule that keeps it balanced; the B-tree (Part 6) adds that rule.
4C code
/* fill a node up to M-1 keys; otherwise go down (creating a new child if needed) */
struct mnode *insert(struct mnode *root, int x) {
struct mnode *n = root;
int i, j;
if (root == NULL) return newNode(x);
while (1) {
i = slot(n, x);
if (i < n->count && n->key[i] == x) return root; /* duplicate */
if (n->count < M - 1 && n->child[i] == NULL) { /* room here */
for (j = n->count; j > i; j--) {
n->key[j] = n->key[j - 1];
n->child[j + 1] = n->child[j];
}
n->key[i] = x;
n->child[i + 1] = NULL;
n->count++;
return root;
}
if (n->child[i] == NULL) { n->child[i] = newNode(x); return root; }
n = n->child[i];
}
}
5.5Search
1Concept
At the current node, scan the keys left to right until you reach the first key ≥ the search key.
If it is equal, the key is found.
Otherwise follow the pointer just before that key (or the last pointer if the key is larger than all of them).
If the pointer is NULL, the key is not in the tree.
Inside a node the scan can be linear or binary search. The number of nodes visited is at most the number of levels.
2Algorithm
Algorithm MWAY-SEARCH(node, x)
1. If node = NULL, return "not found".
2. Find the smallest i such that x ≤ node.key[i] (i = count if x is larger than all keys).
3. If i < count and x = node.key[i], return node (found).
4. Otherwise return MWAY-SEARCH(node.child[i], x).
3Worked example
Step by step Search an m-way tree
4C code
slot finds the first key that is ≥ x inside one node; search uses it to pick the pointer to follow.
/* position of the first key >= x inside one node */
int slot(struct mnode *n, int x) {
int i = 0;
while (i < n->count && x > n->key[i]) i++;
return i;
}
struct mnode *search(struct mnode *root, int x) {
int i;
while (root != NULL) {
i = slot(root, x);
if (i < root->count && root->key[i] == x) return root;
root = root->child[i]; /* one pointer per gap between keys */
}
return NULL;
}
5.6Delete
1Concept
Search for the key Ki. Look at the two pointers on either side of it, Pi−1 (left) and Pi (right):
Case
Action
1. Both side pointers NULL
Remove Ki (and one NULL pointer) from the node. If the node becomes empty, delete the node and set its parent's pointer to NULL.
2. Left subtree exists
Replace Ki with the largest key in the left subtree (go to Pi−1, then keep taking the last pointer). Then delete that key from the left subtree.
3. Only the right subtree exists
Replace Ki with the smallest key in the right subtree (go to Pi, then keep taking the first pointer). Then delete that key from the right subtree.
This is the same idea as Case 3 of BST deletion: a replacement key from just below keeps every range correct.
2Algorithm
Algorithm MWAY-DELETE(node, x)
1. If node = NULL, report "not found".
2. Find i with node.key[i] = x; if x is not in this node, delete from the child whose range contains x.
3. If node.child[i] = NULL and node.child[i+1] = NULL:
remove key[i] and child[i+1]; if node is now empty, free it and return NULL.
4. Else if node.child[i] ≠ NULL:
y = largest key in subtree node.child[i]; node.key[i] = y;
MWAY-DELETE(node.child[i], y)
5. Else:
y = smallest key in subtree node.child[i+1]; node.key[i] = y;
MWAY-DELETE(node.child[i+1], y)
3Worked example
Step by step Delete from an m-way search tree
4C code
/* delete x from the subtree rooted at n; returns the (possibly NULL) new subtree root */
struct mnode *deleteKey(struct mnode *n, int x) {
int i, j, rep;
struct mnode *t;
if (n == NULL) { printf("%d not found\n", x); return NULL; }
i = slot(n, x);
if (i == n->count || n->key[i] != x) { /* not in this node: go down */
n->child[i] = deleteKey(n->child[i], x);
return n;
}
if (n->child[i] == NULL && n->child[i + 1] == NULL) { /* both side pointers NULL */
for (j = i; j < n->count - 1; j++) {
n->key[j] = n->key[j + 1];
n->child[j + 1] = n->child[j + 2];
}
n->child[n->count] = NULL;
n->count--;
if (n->count == 0) { free(n); return NULL; } /* node became empty */
return n;
}
if (n->child[i] != NULL) { /* replace by largest key on the left */
t = n->child[i];
while (t->child[t->count] != NULL) t = t->child[t->count];
rep = t->key[t->count - 1];
n->key[i] = rep;
n->child[i] = deleteKey(n->child[i], rep);
} else { /* replace by smallest key on the right */
t = n->child[i + 1];
while (t->child[0] != NULL) t = t->child[0];
rep = t->key[0];
n->key[i] = rep;
n->child[i + 1] = deleteKey(n->child[i + 1], rep);
}
return n;
}
5.7Capacity and height
Level i of an m-way tree can hold at most mi nodes (every node has up to m children), and each node holds at most m − 1 keys. Adding the levels 0 to h gives the maximum node count; multiplying by m − 1 gives the maximum key count. Taking the logarithm of that gives the minimum height.
For an m-way search tree of height h
Value
Maximum nodes
(mh+1 − 1) / (m − 1)
Maximum keys
mh+1 − 1
Example: m = 3, h = 2
13 nodes, 26 keys
Minimum height for n keys
⌈logm(n + 1)⌉ − 1
Maximum height for n keys
n − 1 (one key per level, like a skewed BST)
5.8Real-life applications, advantages and comparison
The blueprint of B-trees
Every B-tree and B+ tree in a database is an m-way search tree with extra balance rules. The search walk is identical.
Why m-way: one node = one disk block holding many keys.
Static multi-level indexes
An index over a sorted file: a top block of keys points to lower blocks of keys, which point to the records. It is an m-way tree built once.
Why m-way: two or three block reads reach any record.
Tries
A trie for lower-case words is a 26-way tree: each node chooses one of 26 letters.
Why m-way: the branching factor is the alphabet size.
Advantages
Fewer levels than a BST for the same keys · one node fits one disk block · several keys compared per disk read · natural generalisation of the BST.
Disadvantages
No balancing, so the height can grow to n − 1 · nodes may be mostly empty, wasting space · searching inside a node costs extra comparisons.
BST
m-way search tree
B-tree
Keys per node
1
1 to m − 1
⌈m/2⌉ − 1 to m − 1 (non-root)
Children per node
≤ 2
≤ m
⌈m/2⌉ to m (internal, non-root)
Balanced?
No
No
Yes, all leaves on one level
Height (worst)
n − 1
n − 1
O(log⌈m/2⌉ n)
New keys go
Into a new leaf
Into a node with room, or a new child
Into a leaf, splitting upward
Used for
In-memory data
Concept / teaching
Disk-based indexes
5.9Interview and viva questions
Questions asked in viva and interviews
Q1What does "order m" mean?
At most m children per node, and so at most m − 1 keys. Order 5 means up to 4 keys and 5 pointers.
Q2Why do multiway trees reduce disk reads?
The cost of a disk search is the number of nodes read, which is the height + 1. With m children per node the height is about logm n instead of log2 n: for n = 106, about 3 levels with m = 100 against 20 levels in a binary tree. The node size is chosen to fill one disk block.
Q3What is the maximum number of keys in an m-way tree of height h?
Every node full, every level full: (mh+1 − 1)/(m − 1) nodes × (m − 1) keys = mh+1 − 1. For m = 3, h = 2: 26 keys.
Q4How is a BST related to an m-way search tree?
A BST is a 2-way search tree: one key and two pointers per node. Search, insert and delete in an m-way tree generalise the BST versions by scanning several keys in a node.
Q5Why is a plain m-way search tree not good enough for a database?
Nothing keeps it balanced. Sorted input makes a long chain of nodes with one or two keys each, so the height can reach n − 1 and nodes waste space. The B-tree adds two rules (nodes at least half full, all leaves on one level) to fix both problems.
Q6How do you print the keys of an m-way tree in sorted order?
A generalised in-order walk: child 0, key 0, child 1, key 1, …, key k−1, child k. Walking the same order backwards prints them in descending order (C2).
Coding problems (tested C programs)
C1Nodes, keys, height, smallest and largest keyEasyRecursion over k + 1 children
For an m-way search tree, count the nodes and keys, find the height and the smallest and largest key.
Idea. The recursive functions of a binary tree carry over: loop over all count + 1 children instead of two. The smallest key is reached by always taking child 0, the largest by always taking the last child.
Time O(n) · Space O(h)
Show the C program and its output
/* Q1. Count the nodes and keys of an m-way search tree, find its height, smallest and largest key */
#include <stdio.h>
#include <stdlib.h>
#define M 3
struct mnode { int count; int key[M - 1]; struct mnode *child[M]; };
struct mnode *newNode(int x) {
int i;
struct mnode *n = malloc(sizeof *n);
n->count = 1; n->key[0] = x;
for (i = 0; i < M; i++) n->child[i] = NULL;
return n;
}
int slot(struct mnode *n, int x) { int i = 0; while (i < n->count && x > n->key[i]) i++; return i; }
struct mnode *insert(struct mnode *root, int x) { /* same rules as Program 3 */
struct mnode *n = root;
int i, j;
if (root == NULL) return newNode(x);
while (1) {
i = slot(n, x);
if (i < n->count && n->key[i] == x) return root;
if (n->count < M - 1 && n->child[i] == NULL) {
for (j = n->count; j > i; j--) { n->key[j] = n->key[j - 1]; n->child[j + 1] = n->child[j]; }
n->key[i] = x; n->child[i + 1] = NULL; n->count++;
return root;
}
if (n->child[i] == NULL) { n->child[i] = newNode(x); return root; }
n = n->child[i];
}
}
int nodes(struct mnode *n) {
int i, c = 1;
if (n == NULL) return 0;
for (i = 0; i <= n->count; i++) c += nodes(n->child[i]);
return c;
}
int keys(struct mnode *n) {
int i, c;
if (n == NULL) return 0;
c = n->count;
for (i = 0; i <= n->count; i++) c += keys(n->child[i]);
return c;
}
int height(struct mnode *n) { /* edges; empty tree = -1 */
int i, h, best = -1;
if (n == NULL) return -1;
for (i = 0; i <= n->count; i++) { h = height(n->child[i]); if (h > best) best = h; }
return 1 + best;
}
int minKey(struct mnode *n) { while (n->child[0]) n = n->child[0]; return n->key[0]; }
int maxKey(struct mnode *n) { while (n->child[n->count]) n = n->child[n->count]; return n->key[n->count - 1]; }
int main(void) {
int a[] = {35, 60, 20, 80, 10, 25, 40, 50, 90, 70, 15}, i;
struct mnode *root = NULL;
for (i = 0; i < 11; i++) root = insert(root, a[i]);
printf("Nodes %d, keys %d, height %d\n", nodes(root), keys(root), height(root));
printf("Smallest %d, largest %d\n", minKey(root), maxKey(root));
return 0;
}
Output
Nodes 7, keys 11, height 2
Smallest 10, largest 90
C2Keys in descending order and the k-th largestMediumReverse in-order
Print the keys from largest to smallest and report the k-th largest.
Idea. Visit the children and keys from the right: child k, key k−1, child k−1, …, key 0, child 0. Count keys as they are printed.
Time O(n) · Space O(h)
Show the C program and its output
/* Q2. Print the keys of an m-way search tree in descending order and find the k-th largest */
#include <stdio.h>
#include <stdlib.h>
#define M 3
struct mnode { int count; int key[M - 1]; struct mnode *child[M]; };
struct mnode *newNode(int x) {
int i;
struct mnode *n = malloc(sizeof *n);
n->count = 1; n->key[0] = x;
for (i = 0; i < M; i++) n->child[i] = NULL;
return n;
}
int slot(struct mnode *n, int x) { int i = 0; while (i < n->count && x > n->key[i]) i++; return i; }
struct mnode *insert(struct mnode *root, int x) { /* same rules as Program 3 */
struct mnode *n = root;
int i, j;
if (root == NULL) return newNode(x);
while (1) {
i = slot(n, x);
if (i < n->count && n->key[i] == x) return root;
if (n->count < M - 1 && n->child[i] == NULL) {
for (j = n->count; j > i; j--) { n->key[j] = n->key[j - 1]; n->child[j + 1] = n->child[j]; }
n->key[i] = x; n->child[i + 1] = NULL; n->count++;
return root;
}
if (n->child[i] == NULL) { n->child[i] = newNode(x); return root; }
n = n->child[i];
}
}
int seen, answer;
/* reverse in-order: last child first, then key[count-1], then child[count-1], ... */
void reverseInorder(struct mnode *n, int k) {
int i;
if (n == NULL) return;
for (i = n->count; i >= 0; i--) {
reverseInorder(n->child[i], k);
if (i > 0) {
printf("%d ", n->key[i - 1]);
if (++seen == k) answer = n->key[i - 1];
}
}
}
int main(void) {
int a[] = {35, 60, 20, 80, 10, 25, 40, 50, 90, 70, 15}, i;
struct mnode *root = NULL;
for (i = 0; i < 11; i++) root = insert(root, a[i]);
seen = 0; answer = -1;
printf("Descending: ");
reverseInorder(root, 3);
printf("\n3rd largest: %d\n", answer);
return 0;
}
Use the lab to try your own keys, the tracer to watch Program 3 execute line by line, and then read the complete program.
Try it m-way search tree lab
Code tracer: run Program 3 line by line
Choose any function, type your own keys and press Next. The highlighted line is the statement being executed, the call stack shows every call that is still waiting to finish, and the variables, output and trace table change with each step. Use Next key ⏭ to jump to the end of the current key. Change the order m to see how the same keys fill the nodes.
Trace Program 3, inside the computer
Program 3 · m-way search tree: search, insert, delete, in-order
/* m-way search tree (order M): node structure, search, insert, delete, in-order */
#include <stdio.h>
#include <stdlib.h>
#define M 3 /* order: at most M children, M-1 keys */
struct mnode {
int count; /* number of keys in use */
int key[M - 1]; /* key[0] < key[1] < ... */
struct mnode *child[M]; /* child[i] holds keys between key[i-1] and key[i] */
};
struct mnode *newNode(int x) {
int i;
struct mnode *n = (struct mnode *)malloc(sizeof(struct mnode));
n->count = 1;
n->key[0] = x;
for (i = 0; i < M; i++) n->child[i] = NULL;
return n;
}
/* position of the first key >= x inside one node */
int slot(struct mnode *n, int x) {
int i = 0;
while (i < n->count && x > n->key[i]) i++;
return i;
}
struct mnode *search(struct mnode *root, int x) {
int i;
while (root != NULL) {
i = slot(root, x);
if (i < root->count && root->key[i] == x) return root;
root = root->child[i]; /* one pointer per gap between keys */
}
return NULL;
}
/* fill a node up to M-1 keys; otherwise go down (creating a new child if needed) */
struct mnode *insert(struct mnode *root, int x) {
struct mnode *n = root;
int i, j;
if (root == NULL) return newNode(x);
while (1) {
i = slot(n, x);
if (i < n->count && n->key[i] == x) return root; /* duplicate */
if (n->count < M - 1 && n->child[i] == NULL) { /* room here */
for (j = n->count; j > i; j--) {
n->key[j] = n->key[j - 1];
n->child[j + 1] = n->child[j];
}
n->key[i] = x;
n->child[i + 1] = NULL;
n->count++;
return root;
}
if (n->child[i] == NULL) { n->child[i] = newNode(x); return root; }
n = n->child[i];
}
}
/* delete x from the subtree rooted at n; returns the (possibly NULL) new subtree root */
struct mnode *deleteKey(struct mnode *n, int x) {
int i, j, rep;
struct mnode *t;
if (n == NULL) { printf("%d not found\n", x); return NULL; }
i = slot(n, x);
if (i == n->count || n->key[i] != x) { /* not in this node: go down */
n->child[i] = deleteKey(n->child[i], x);
return n;
}
if (n->child[i] == NULL && n->child[i + 1] == NULL) { /* both side pointers NULL */
for (j = i; j < n->count - 1; j++) {
n->key[j] = n->key[j + 1];
n->child[j + 1] = n->child[j + 2];
}
n->child[n->count] = NULL;
n->count--;
if (n->count == 0) { free(n); return NULL; } /* node became empty */
return n;
}
if (n->child[i] != NULL) { /* replace by largest key on the left */
t = n->child[i];
while (t->child[t->count] != NULL) t = t->child[t->count];
rep = t->key[t->count - 1];
n->key[i] = rep;
n->child[i] = deleteKey(n->child[i], rep);
} else { /* replace by smallest key on the right */
t = n->child[i + 1];
while (t->child[0] != NULL) t = t->child[0];
rep = t->key[0];
n->key[i] = rep;
n->child[i + 1] = deleteKey(n->child[i + 1], rep);
}
return n;
}
void show(struct mnode *n) { /* [keys](children), _ = NULL */
int i, any = 0;
if (n == NULL) { printf("_"); return; }
printf("[");
for (i = 0; i < n->count; i++) printf(i ? ",%d" : "%d", n->key[i]);
printf("]");
for (i = 0; i <= n->count; i++) if (n->child[i]) any = 1;
if (any) {
printf("(");
for (i = 0; i <= n->count; i++) { if (i) printf(" "); show(n->child[i]); }
printf(")");
}
}
void inorder(struct mnode *n) { /* prints keys in sorted order */
int i;
if (n == NULL) return;
for (i = 0; i < n->count; i++) {
inorder(n->child[i]);
printf("%d ", n->key[i]);
}
inorder(n->child[n->count]);
}
int main(void) {
struct mnode *root = NULL;
int ch, n, i, x;
while (1) {
printf("\n1.Insert many 2.Search 3.Delete 4.Show 5.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &n); printf("Keys: ");
for (i = 0; i < n; i++) { scanf("%d", &x); root = insert(root, x); } break;
case 2: printf("Key: "); scanf("%d", &x); printf(search(root, x) ? "Found\n" : "Not found\n"); break;
case 3: printf("Key: "); scanf("%d", &x); root = deleteKey(root, x); break;
case 4: printf("Tree: "); show(root); printf("\nIn-order: "); inorder(root); printf("\n"); break;
case 5: return 0;
}
}
return 0;
}
_ means a NULL pointer. The program follows exactly the same rules as the visualizer.
Common mistakes in this part
Inserting a key into a node that has room even though the pointer for its range already leads to a subtree.
Saying "order m" means m keys. It means at most m children and m − 1 keys.
Assuming an m-way search tree stays balanced. Only the B-tree rules guarantee that.
Key points to remember
An m-way search tree node has up to m children and m − 1 sorted keys.
Keys in child Pi lie between Ki and Ki+1; a BST is a 2-way search tree.
Fat nodes mean fewer levels, and each level is one disk read.
Insert fills a node up to m − 1 keys, then opens a new child. Nothing keeps it balanced.
Delete: remove directly if both side pointers are NULL; otherwise replace by the largest key on the left (or smallest on the right).
Max keys in an m-way tree of height h: mh+1 − 1.
Unit IV · Part 6
B-Tree
A B-tree is a balanced m-way search tree: it grows and shrinks at the root, so every leaf stays on the same level and every search stays a few disk reads.
Syllabus: B-Trees. (The B+ tree is Part 7, because exams ask it alongside B-trees.)
6.1At a glance
A B-tree of order m is an m-way search tree that stays balanced: every node except the root is at least half full, and all leaves are always on the same level.
Problem it solves
A plain m-way tree can become lopsided and half empty. A B-tree guarantees a height of O(log⌈m/2⌉ n) even while keys are inserted and deleted, so any key is only a few disk reads away.
The rule
At most m children (m − 1 keys); every non-root node has at least ⌈m/2⌉ − 1 keys; all leaves on one level. Insert splits full nodes, delete borrows or merges; the tree grows and shrinks only at the root.
Think of it as
A cabinet of drawers: when a drawer overflows you split it in two and write a new label on the drawer front above.
Use it when
Large sorted data lives on disk or SSD and changes often: database and file-system indexes.
Not the right tool when
Data is small and in memory (a balanced BST or hash table is simpler), or you mostly scan ranges (a B+ tree is faster).
An m-way search tree has fat nodes (good for disks) but can become lopsided (bad). A B-tree adds two balance rules: every node except the root must be at least half full, and all leaves must be on the same level. It was introduced by Bayer and McCreight (1972) for disk-based indexes.
Definition
A B-tree of order m is an m-way search tree that is either empty or satisfies:
Every node has at most m children and at most m − 1 keys.
Every internal node except the root has at least ⌈m/2⌉ children, and every node except the root has at least ⌈m/2⌉ − 1 keys.
The root has at least 2 children unless it is a leaf (it can hold as few as 1 key).
All leaves are on the same level.
A non-leaf node with k keys has exactly k + 1 children.
Order m
Children (internal, non-root)
Keys (any non-root node)
Also called
3
2 – 3
1 – 2
2-3 tree
4
2 – 4
1 – 3
2-3-4 tree
5
3 – 5
2 – 4
6
3 – 6
2 – 5
How to read the B-tree pictures
Order m = … above the picture is the order. The node is a short box. Its children hang underneath from left to right, the same way a binary tree puts its left child on the left and its right child on the right.
A node with no lines below it is a leaf, and every leaf is on the bottom level. Empty pointers are not drawn.
In the step-by-step visualizer a red box has too many keys (overflow) or too few (underflow), a violet key is the median that moves up or the key being borrowed, and green keys are new or have just moved.
Order means maximum children, not keys. Some books define order as the minimum degree t (Cormen), where a node has between t − 1 and 2t − 1 keys. If a question says "B-tree of order 5", use m = 5: at most 4 keys per node.
6.3Insert (build the tree): always into a leaf, split on overflow
1Concept
Walk down from the root, at each node taking the pointer for the key's range, until you reach a leaf. New keys are never placed directly in an internal node.
Insert the key in sorted order inside that leaf.
If the leaf now has m keys (overflow), split it: the median key moves up into the parent, the keys to its left form one node and the keys to its right form another.
If the parent overflows because of the key it received, split the parent the same way. Repeat upward.
If the root splits, the median becomes a new root. This is the only way the height grows, which is why all leaves stay on one level.
2Algorithm
Algorithm B-TREE-INSERT(root, key)
1. If the tree is empty, create a root node containing key. Stop.
2. Starting at the root, move down: in each node find the child whose range
contains key, until a leaf L is reached.
3. Insert key into L in sorted order.
4. While the current node has m keys (overflow):
a. Take the median key K (position ⌈m/2⌉).
b. Split the node into Left (keys before K) and Right (keys after K).
c. If the node is the root: make a new root containing only K,
with children Left and Right. Stop (height grows by 1).
d. Otherwise insert K into the parent (with Right as its new child)
and move up to the parent.
For even m the overflowing node has an even number of keys, so there are two middle keys. These notes send the left-middle key up (position ⌈m/2⌉). Some books use the right-middle; either is correct if you use it consistently.
3Worked example
Step by step Construct a B-tree
"Order 5 · C N G A H E K Q M F W L T Z D P R X Y S" is the standard textbook sequence: leaves hold up to 4 keys and the fifth key triggers a split. In the order-4 examples the overflowing node has 4 keys, and the left-middle key goes up.
4C code
The recursion inserts into a leaf first; each caller then checks the child it came from and splits it if it has M keys. insert handles the root separately, because splitting the root is the only way the tree grows taller.
/* split overflowing child c = p->child[i] (it holds M keys) around its median */
void splitChild(struct bnode *p, int i) {
struct bnode *c = p->child[i], *r = newNode(c->leaf);
int mid = (M + 1) / 2 - 1, j; /* median index = ceil(M/2) - 1 */
r->n = c->n - mid - 1;
for (j = 0; j < r->n; j++) r->key[j] = c->key[mid + 1 + j];
if (!c->leaf) for (j = 0; j <= r->n; j++) r->child[j] = c->child[mid + 1 + j];
for (j = p->n; j > i; j--) { p->key[j] = p->key[j - 1]; p->child[j + 1] = p->child[j]; }
p->key[i] = c->key[mid]; /* median moves up */
p->child[i + 1] = r;
p->n++;
c->n = mid;
}
/* insert into subtree t; the caller splits t if it overflows */
void insertRec(struct bnode *t, int x) {
int i = slot(t, x), j;
if (i < t->n && t->key[i] == x) return; /* duplicate */
if (t->leaf) {
for (j = t->n; j > i; j--) t->key[j] = t->key[j - 1];
t->key[i] = x; t->n++;
return;
}
insertRec(t->child[i], x);
if (t->child[i]->n == M) splitChild(t, i);
}
void insert(int x) {
struct bnode *s;
if (root == NULL) { root = newNode(1); root->key[0] = x; root->n = 1; return; }
insertRec(root, x);
if (root->n == M) { /* root overflow: tree grows at the top */
s = newNode(0); s->child[0] = root; root = s;
splitChild(s, 0);
}
}
6.4Search
1Concept
Exactly the m-way search: scan the keys in the node, stop if found, otherwise follow the pointer for the key's range. Because all leaves are on one level, a search reads at most h + 1 nodes, and h is O(log⌈m/2⌉ n).
2Algorithm
Algorithm B-TREE-SEARCH(node, x)
1. i = 0
2. While i < node.n and x > node.key[i]: i = i + 1
3. If i < node.n and x = node.key[i]: return (node, i) found
4. If node is a leaf: return NULL not found
5. Return B-TREE-SEARCH(node.child[i], x)
3Worked example
Step by step Search a B-tree
4C code
int slot(struct bnode *t, int x) { int i = 0; while (i < t->n && x > t->key[i]) i++; return i; }
struct bnode *search(struct bnode *t, int x) {
int i;
while (t) {
i = slot(t, x);
if (i < t->n && t->key[i] == x) return t;
if (t->leaf) return NULL;
t = t->child[i];
}
return NULL;
}
6.5Delete: borrow first, merge if you must
1Concept
Let min = ⌈m/2⌉ − 1, the smallest number of keys a non-root node may hold.
Situation
Action
Key is in an internal node
Replace it with its in-order successor (left-most key of the right subtree) or predecessor. Both are always in a leaf, so the problem becomes deleting from a leaf.
Leaf still has ≥ min keys
Just remove the key. Done.
Underflow, sibling has spare keys
Borrow (redistribute): the separating key in the parent comes down into the node, and the sibling's nearest key goes up to replace it.
Underflow, no sibling can spare
Merge: the node + the separating key from the parent + the sibling become one node. The parent loses a key and may underflow in turn, so repeat upward.
Root becomes empty
Its only child becomes the new root. The tree gets one level shorter.
2Algorithm
Algorithm B-TREE-DELETE(x)
1. Search for x. If x is in an internal node, replace it with its in-order
successor (or predecessor), which is in a leaf, and delete that key instead.
2. Remove the key from the leaf L.
3. While L is not the root and L has fewer than ⌈m/2⌉ − 1 keys:
a. If a neighbouring sibling S has more than the minimum:
move the separator from the parent down into L,
move S's nearest key up into the parent (borrow). Stop.
b. Else merge L, the separator and S into one node;
remove the separator from the parent; L = parent (merge, repeat).
4. If the root has no keys left, make its only child the new root.
3Worked example
Each case below starts from the same order-5 tree (root [M]; minimum 2 keys per non-root node), so you can compare them.
Case 1 Key in a leaf with spare keys: delete H
Leaf [H K L] keeps 2 keys after removal. Just remove it.
Case 2 Key in an internal node: delete G
G separates [E F] and [H K L]. Replace it with its in-order successor H (first key of the right subtree), then delete H from the leaf, which still has 2 keys.
Case 3 Underflow, borrow from the right sibling: delete E
[E F] becomes [F], below the minimum. The left sibling [A C] has only 2 keys, but the right sibling [H K L] has 3. The separator G comes down, H goes up to the parent.
Case 4 Underflow, borrow from the left sibling: delete E (after B was inserted)
Now the left sibling [A B C] has a spare key. The separator D comes down into the leaf and C goes up.
Case 5 Underflow, no sibling can lend: merge (and the root shrinks): delete N
[N P] becomes [P]; its only sibling [R S] has just 2 keys. Merge [P] + separator Q + [R S] = [P Q R S]. The parent [Q T] drops to [T], which underflows; its sibling [D G] cannot lend either, so they merge with M from the root. The root becomes empty and the tree gets one level shorter.
More deletion examples, step by step
Step by step Delete from a B-tree
4C code
deleteRec removes the key (using the successor for an internal key); on the way back each caller checks the child it came from and calls fix if that child has fewer than MIN keys. fix tries the left sibling, then the right sibling, and merges only if neither can lend.
void removeAt(struct bnode *t, int i) { /* remove key i and child i+1 */
int j;
for (j = i; j < t->n - 1; j++) { t->key[j] = t->key[j + 1]; t->child[j + 1] = t->child[j + 2]; }
t->n--;
}
/* child p->child[i] has MIN-1 keys: borrow from a sibling or merge */
void fix(struct bnode *p, int i) {
struct bnode *c = p->child[i], *L, *R;
int j;
L = (i > 0) ? p->child[i - 1] : NULL;
R = (i < p->n) ? p->child[i + 1] : NULL;
if (L && L->n > MIN) { /* borrow from left */
for (j = c->n; j > 0; j--) c->key[j] = c->key[j - 1];
if (!c->leaf) for (j = c->n + 1; j > 0; j--) c->child[j] = c->child[j - 1];
c->key[0] = p->key[i - 1];
if (!c->leaf) c->child[0] = L->child[L->n];
p->key[i - 1] = L->key[L->n - 1];
L->n--; c->n++;
} else if (R && R->n > MIN) { /* borrow from right */
c->key[c->n] = p->key[i];
if (!c->leaf) c->child[c->n + 1] = R->child[0];
c->n++;
p->key[i] = R->key[0];
for (j = 0; j < R->n - 1; j++) R->key[j] = R->key[j + 1];
if (!R->leaf) for (j = 0; j < R->n; j++) R->child[j] = R->child[j + 1];
R->n--;
} else { /* merge with a sibling */
if (L) { c = L; R = p->child[i]; i--; } /* merge child[i] and child[i+1] */
c->key[c->n] = p->key[i];
for (j = 0; j < R->n; j++) c->key[c->n + 1 + j] = R->key[j];
if (!c->leaf) for (j = 0; j <= R->n; j++) c->child[c->n + 1 + j] = R->child[j];
c->n += R->n + 1;
free(R);
removeAt(p, i);
}
}
void deleteRec(struct bnode *t, int x) {
int i = slot(t, x);
struct bnode *s;
if (i < t->n && t->key[i] == x) {
if (t->leaf) { for (; i < t->n - 1; i++) t->key[i] = t->key[i + 1]; t->n--; return; }
s = t->child[i + 1]; /* internal: use the in-order successor */
while (!s->leaf) s = s->child[0];
t->key[i] = s->key[0];
x = s->key[0];
i = i + 1; /* now delete the successor from the right subtree */
} else if (t->leaf) { printf("%d not found\n", x); return; }
deleteRec(t->child[i], x);
if (t->child[i]->n < MIN) fix(t, i);
}
void deleteKey(int x) {
struct bnode *old;
if (root == NULL) return;
deleteRec(root, x);
if (root->n == 0) { /* root emptied: tree shrinks */
old = root;
root = root->leaf ? NULL : root->child[0];
free(old);
}
}
6.6Height and complexity
A B-tree of order m with n keys has height h ≤ log⌈m/2⌉((n + 1) / 2).
The root has at least 2 children and every other internal node at least ⌈m/2⌉, so the number of keys grows exponentially with the height. For m = 200 and n = 2 million keys, h ≤ log100(1,000,000) = 3. Any key is at most 4 disk reads away.
For a B-tree of order m and height h
Value
Maximum keys (every node full)
mh+1 − 1
Minimum keys (every node at the minimum)
2⌈m/2⌉h − 1
Example: m = 5, h = 2
between 17 and 124 keys
Operation
Nodes read / written
CPU time
Search
O(logm n)
O(m · logm n)
Insert
O(logm n)
O(m · logm n)
Delete
O(logm n)
O(m · logm n)
6.7Real-life applications and advantages
Databases
SQLite stores every index as a B-tree; MongoDB's WiredTiger engine keeps collections and indexes in B-trees. A lookup among millions of rows reads only 3 or 4 pages.
Why a B-tree: one node = one disk page, and the height stays tiny.
File systems
Btrfs (the "B-tree file system") keeps all its metadata in B-trees; Apple's HFS+ keeps its catalogue of files in a B-tree; NTFS indexes directories with B+ trees.
Why a B-tree: millions of files, few disk reads per lookup.
Key-value stores and caches on disk
Embedded stores such as LMDB and Berkeley DB keep sorted keys in B-tree variants, so lookups, inserts and ordered scans stay fast.
Why a B-tree: sorted order plus cheap updates.
Advantages
Always balanced · height O(log n) with a large base, so very few disk reads · nodes at least half full, so little wasted space · insert and delete stay local (a few nodes change).
Disadvantages
More complex code (split, borrow, merge) · searching inside a node costs extra comparisons · range queries need an in-order walk (fixed by the B+ tree).
6.8Interview and placement questions
Questions asked in viva and interviews
Q1Why do databases use B-trees instead of AVL or red-black trees?
The cost on disk is the number of pages read. An AVL tree of 109 keys is about 30 levels deep, so 30 reads. A B-tree node fills one page and holds hundreds of keys, so the same data is 3 to 4 levels deep. Keys inside a node are compared in memory, which is almost free by comparison.
Q2Estimate the order and height of a B-tree index: 4 KB pages, 8-byte keys, 8-byte pointers, 100 million keys.
A node with m pointers and m − 1 keys needs about 16m bytes, so m ≈ 4096 / 16 = 256. Levels needed: 2563 ≈ 16.7 million is not enough, 2564 ≈ 4.3 billion is, so at most 4 levels even with full nodes, and still only about 4 or 5 levels when nodes are half full. The top one or two levels usually stay in memory, so a lookup costs about 2 disk reads.
Q3Why are all leaves of a B-tree always on the same level?
The tree never grows or shrinks at the bottom. Height increases only when the root splits (a new root above everything) and decreases only when the root becomes empty, and both change every root-to-leaf path by exactly one.
Q4What are the fewest and most keys in a B-tree of order m and height h?
Most: every node full, mh+1 − 1. Fewest: root with 1 key and every other node at the minimum, 2⌈m/2⌉h − 1. For m = 5, h = 2: between 17 and 124 keys.
Q5On underflow, why try borrowing before merging?
A borrow changes three nodes and always stops there. A merge removes a key from the parent, which can make the parent underflow too and repeat up to the root. Borrowing keeps the tree fuller and the work local.
Q6Why must the minimum be ⌈m/2⌉ − 1 keys, not more?
When a full node gets one more key it has m keys; splitting around the median gives two nodes of about (m − 1)/2 keys each. The minimum must be small enough that both halves of a split are legal, which gives ⌈m/2⌉ − 1.
Q7What is the time complexity of B-tree operations?
O(logm n) nodes are read or written; inside each node a scan costs O(m) (or O(log m) with binary search). So CPU time O(m logm n), disk accesses O(logm n).
Q8Some books say "B-tree of minimum degree t". How does that relate to order m?
Cormen's minimum degree t means every non-root node has between t − 1 and 2t − 1 keys, so it is a B-tree of order m = 2t (always even). "Order m" in these notes means at most m children. Always state which definition you use.
Coding problems (tested C programs)
C1Check whether a tree is a valid B-treeMediumRecursion with limits
Given a multiway tree, check every B-tree rule for order M: key counts, sorted keys, key ranges, children count, and leaves on one level.
Idea. Recurse with the open range (lo, hi) that the node's keys must fall in (like validating a BST). Record the depth of the first leaf and compare every other leaf with it.
Time O(n) · Space O(h)
Show the C program and its output
/* Q1. Check that a multiway tree obeys every B-tree rule for order M */
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define M 5
#define MIN ((M + 1) / 2 - 1) /* ceil(M/2) - 1 = 2 for M = 5 */
struct bnode { int n; int key[M]; struct bnode *child[M + 1]; int leaf; };
struct bnode *mk(int n, const int *k) { /* helper: a node with n keys and no children yet */
int i;
struct bnode *t = calloc(1, sizeof *t);
t->n = n; t->leaf = 1;
for (i = 0; i < n; i++) t->key[i] = k[i];
return t;
}
void link(struct bnode *p, int i, struct bnode *c) { p->child[i] = c; p->leaf = 0; }
int leafDepth; /* depth of the first leaf found */
/* keys of t must lie strictly between lo and hi */
int check(struct bnode *t, int isRoot, int depth, long lo, long hi) {
int i;
if (t->n > M - 1) { printf(" node with %d keys: more than M-1\n", t->n); return 0; }
if (!isRoot && t->n < MIN) { printf(" node with %d key(s): fewer than MIN = %d\n", t->n, MIN); return 0; }
for (i = 0; i < t->n; i++) {
if (t->key[i] <= lo || t->key[i] >= hi || (i && t->key[i - 1] >= t->key[i])) {
printf(" key %d is out of order\n", t->key[i]); return 0;
}
}
if (t->leaf) {
if (leafDepth == -1) leafDepth = depth;
else if (depth != leafDepth) { printf(" leaves at depth %d and %d\n", leafDepth, depth); return 0; }
return 1;
}
for (i = 0; i <= t->n; i++) { /* an internal node needs exactly n+1 children */
if (t->child[i] == NULL) { printf(" child %d is missing\n", i); return 0; }
if (!check(t->child[i], 0, depth + 1, i ? t->key[i - 1] : lo, i < t->n ? t->key[i] : hi)) return 0;
}
return 1;
}
int isBTree(struct bnode *root) { leafDepth = -1; return check(root, 1, 0, LONG_MIN, LONG_MAX); }
int main(void) {
int r[] = {40}, a[] = {10, 20}, b[] = {50, 60, 70}, c[] = {5};
int d[] = {45, 50}, e[] = {55, 65}, f[] = {58, 60}, g[] = {70, 80};
struct bnode *root = mk(1, r);
link(root, 0, mk(2, a)); link(root, 1, mk(3, b));
printf("Tree 1: %s\n", isBTree(root) ? "valid B-tree of order 5" : "invalid");
root->child[0] = mk(1, c); /* a leaf with only 1 key */
printf("Tree 2:\n"); printf("%s\n", isBTree(root) ? " valid" : " -> invalid");
root->child[0] = mk(2, a); /* repair the left leaf ... */
root->child[1] = mk(2, e); /* ... and hang a deeper subtree right */
link(root->child[1], 0, mk(2, d)); link(root->child[1], 1, mk(2, f)); link(root->child[1], 2, mk(2, g));
printf("Tree 3:\n"); printf("%s\n", isBTree(root) ? " valid" : " -> invalid");
return 0;
}
Output
Tree 1: valid B-tree of order 5
Tree 2:
node with 1 key(s): fewer than MIN = 2
-> invalid
Tree 3:
leaves at depth 1 and 2
-> invalid
C2Smallest, largest and k-th smallest keyEasyGeneralised in-order
Find the minimum, the maximum and the k-th smallest key of a B-tree.
Idea. Minimum: keep taking child 0 down to a leaf; maximum: keep taking the last child. For the k-th smallest do the generalised in-order walk (child 0, key 0, child 1, …) and count keys.
Time O(h) for min and max, O(h + k) for k-th · Space O(h)
Show the C program and its output
/* Q2. Smallest, largest and k-th smallest key of a B-tree (in-order walk) */
#include <stdio.h>
#include <stdlib.h>
#define M 3
struct bnode { int n; int key[M]; struct bnode *child[M + 1]; int leaf; };
struct bnode *root = NULL;
struct bnode *newNode(int leaf) { struct bnode *t = calloc(1, sizeof *t); t->leaf = leaf; return t; }
int slot(struct bnode *t, int x) { int i = 0; while (i < t->n && x > t->key[i]) i++; return i; }
void splitChild(struct bnode *p, int i) { /* as in Program 4 */
struct bnode *c = p->child[i], *r = newNode(c->leaf);
int mid = (M + 1) / 2 - 1, j;
r->n = c->n - mid - 1;
for (j = 0; j < r->n; j++) r->key[j] = c->key[mid + 1 + j];
if (!c->leaf) for (j = 0; j <= r->n; j++) r->child[j] = c->child[mid + 1 + j];
for (j = p->n; j > i; j--) { p->key[j] = p->key[j - 1]; p->child[j + 1] = p->child[j]; }
p->key[i] = c->key[mid]; p->child[i + 1] = r; p->n++; c->n = mid;
}
void insertRec(struct bnode *t, int x) {
int i = slot(t, x), j;
if (i < t->n && t->key[i] == x) return;
if (t->leaf) { for (j = t->n; j > i; j--) t->key[j] = t->key[j - 1]; t->key[i] = x; t->n++; return; }
insertRec(t->child[i], x);
if (t->child[i]->n == M) splitChild(t, i);
}
void insert(int x) {
struct bnode *s;
if (root == NULL) { root = newNode(1); root->key[0] = x; root->n = 1; return; }
insertRec(root, x);
if (root->n == M) { s = newNode(0); s->child[0] = root; root = s; splitChild(s, 0); }
}
int minKey(struct bnode *t) { while (!t->leaf) t = t->child[0]; return t->key[0]; }
int maxKey(struct bnode *t) { while (!t->leaf) t = t->child[t->n]; return t->key[t->n - 1]; }
int seen;
int kth(struct bnode *t, int k) { /* returns the key, or -1 if not reached yet */
int i, v;
for (i = 0; i <= t->n; i++) {
if (!t->leaf) { v = kth(t->child[i], k); if (v != -1) return v; }
if (i < t->n && ++seen == k) return t->key[i];
}
return -1;
}
int main(void) {
int a[] = {10, 20, 5, 6, 12, 30, 7, 17}, i;
for (i = 0; i < 8; i++) insert(a[i]);
printf("Smallest %d, largest %d\n", minKey(root), maxKey(root));
for (i = 1; i <= 8; i += 3) { seen = 0; printf("k = %d -> %d\n", i, kth(root, i)); }
return 0;
}
Output
Smallest 5, largest 30
k = 1 -> 5
k = 4 -> 10
k = 7 -> 20
6.9B-tree lab, code tracer and complete program
Use the lab to try your own keys and orders, the tracer to watch Program 4 execute line by line, and then read the complete program.
Try it B-tree lab
Change the order to see the same keys rearranged. Delete a key that sits next to a sibling with only the minimum number of keys to force a merge.
Code tracer: run Program 4 line by line
Choose any function, type your own keys and press Next. The highlighted line is the statement being executed, the call stack shows every call that is still waiting to finish, and the variables, output and trace table change with each step. Use Next key ⏭ to jump to the end of the current key. Try order 3 with the default keys, then order 5 with the letters C N G A H E K Q M F W L T Z D P R X Y S.
Trace Program 4, inside the computer
Program 4 · B-tree of order M: insert with split, search, delete with borrow and merge
/* B-Tree of order M: search, insert (split at median), delete (borrow / merge), traversal */
#include <stdio.h>
#include <stdlib.h>
#define M 3 /* order: max M children, max M-1 keys */
#define MIN ((M + 1) / 2 - 1) /* min keys in a non-root node = ceil(M/2) - 1 */
struct bnode {
int n; /* keys in use */
int key[M]; /* one extra slot to hold a temporary overflow */
struct bnode *child[M + 1];
int leaf;
};
struct bnode *root = NULL;
struct bnode *newNode(int leaf) {
int i;
struct bnode *t = (struct bnode *)malloc(sizeof(struct bnode));
t->n = 0; t->leaf = leaf;
for (i = 0; i <= M; i++) t->child[i] = NULL;
return t;
}
int slot(struct bnode *t, int x) { int i = 0; while (i < t->n && x > t->key[i]) i++; return i; }
struct bnode *search(struct bnode *t, int x) {
int i;
while (t) {
i = slot(t, x);
if (i < t->n && t->key[i] == x) return t;
if (t->leaf) return NULL;
t = t->child[i];
}
return NULL;
}
/* split overflowing child c = p->child[i] (it holds M keys) around its median */
void splitChild(struct bnode *p, int i) {
struct bnode *c = p->child[i], *r = newNode(c->leaf);
int mid = (M + 1) / 2 - 1, j; /* median index = ceil(M/2) - 1 */
r->n = c->n - mid - 1;
for (j = 0; j < r->n; j++) r->key[j] = c->key[mid + 1 + j];
if (!c->leaf) for (j = 0; j <= r->n; j++) r->child[j] = c->child[mid + 1 + j];
for (j = p->n; j > i; j--) { p->key[j] = p->key[j - 1]; p->child[j + 1] = p->child[j]; }
p->key[i] = c->key[mid]; /* median moves up */
p->child[i + 1] = r;
p->n++;
c->n = mid;
}
/* insert into subtree t; the caller splits t if it overflows */
void insertRec(struct bnode *t, int x) {
int i = slot(t, x), j;
if (i < t->n && t->key[i] == x) return; /* duplicate */
if (t->leaf) {
for (j = t->n; j > i; j--) t->key[j] = t->key[j - 1];
t->key[i] = x; t->n++;
return;
}
insertRec(t->child[i], x);
if (t->child[i]->n == M) splitChild(t, i);
}
void insert(int x) {
struct bnode *s;
if (root == NULL) { root = newNode(1); root->key[0] = x; root->n = 1; return; }
insertRec(root, x);
if (root->n == M) { /* root overflow: tree grows at the top */
s = newNode(0); s->child[0] = root; root = s;
splitChild(s, 0);
}
}
/* ---------- delete ---------- */
void removeAt(struct bnode *t, int i) { /* remove key i and child i+1 */
int j;
for (j = i; j < t->n - 1; j++) { t->key[j] = t->key[j + 1]; t->child[j + 1] = t->child[j + 2]; }
t->n--;
}
/* child p->child[i] has MIN-1 keys: borrow from a sibling or merge */
void fix(struct bnode *p, int i) {
struct bnode *c = p->child[i], *L, *R;
int j;
L = (i > 0) ? p->child[i - 1] : NULL;
R = (i < p->n) ? p->child[i + 1] : NULL;
if (L && L->n > MIN) { /* borrow from left */
for (j = c->n; j > 0; j--) c->key[j] = c->key[j - 1];
if (!c->leaf) for (j = c->n + 1; j > 0; j--) c->child[j] = c->child[j - 1];
c->key[0] = p->key[i - 1];
if (!c->leaf) c->child[0] = L->child[L->n];
p->key[i - 1] = L->key[L->n - 1];
L->n--; c->n++;
} else if (R && R->n > MIN) { /* borrow from right */
c->key[c->n] = p->key[i];
if (!c->leaf) c->child[c->n + 1] = R->child[0];
c->n++;
p->key[i] = R->key[0];
for (j = 0; j < R->n - 1; j++) R->key[j] = R->key[j + 1];
if (!R->leaf) for (j = 0; j < R->n; j++) R->child[j] = R->child[j + 1];
R->n--;
} else { /* merge with a sibling */
if (L) { c = L; R = p->child[i]; i--; } /* merge child[i] and child[i+1] */
c->key[c->n] = p->key[i];
for (j = 0; j < R->n; j++) c->key[c->n + 1 + j] = R->key[j];
if (!c->leaf) for (j = 0; j <= R->n; j++) c->child[c->n + 1 + j] = R->child[j];
c->n += R->n + 1;
free(R);
removeAt(p, i);
}
}
void deleteRec(struct bnode *t, int x) {
int i = slot(t, x);
struct bnode *s;
if (i < t->n && t->key[i] == x) {
if (t->leaf) { for (; i < t->n - 1; i++) t->key[i] = t->key[i + 1]; t->n--; return; }
s = t->child[i + 1]; /* internal: use the in-order successor */
while (!s->leaf) s = s->child[0];
t->key[i] = s->key[0];
x = s->key[0];
i = i + 1; /* now delete the successor from the right subtree */
} else if (t->leaf) { printf("%d not found\n", x); return; }
deleteRec(t->child[i], x);
if (t->child[i]->n < MIN) fix(t, i);
}
void deleteKey(int x) {
struct bnode *old;
if (root == NULL) return;
deleteRec(root, x);
if (root->n == 0) { /* root emptied: tree shrinks */
old = root;
root = root->leaf ? NULL : root->child[0];
free(old);
}
}
void inorder(struct bnode *t) {
int i;
if (!t) return;
for (i = 0; i < t->n; i++) { if (!t->leaf) inorder(t->child[i]); printf("%d ", t->key[i]); }
if (!t->leaf) inorder(t->child[t->n]);
}
void show(struct bnode *t, int level) { /* one line per node, indented by level */
int i;
if (!t) return;
printf("%*s[", level * 4, "");
for (i = 0; i < t->n; i++) printf(i ? " %d" : "%d", t->key[i]);
printf("]\n");
if (!t->leaf) for (i = 0; i <= t->n; i++) show(t->child[i], level + 1);
}
int main(void) {
int ch, x, n, i;
while (1) {
printf("\n1.Insert many 2.Search 3.Delete 4.Show 5.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &n); printf("Keys: ");
for (i = 0; i < n; i++) { scanf("%d", &x); insert(x); } break;
case 2: printf("Key: "); scanf("%d", &x); printf(search(root, x) ? "Found\n" : "Not found\n"); break;
case 3: printf("Key: "); scanf("%d", &x); deleteKey(x); break;
case 4: show(root, 0); printf("In-order: "); inorder(root); printf("\n"); break;
case 5: return 0;
}
}
return 0;
}
Sample run (M = 3; insert 10 20 5 6 12 30 7 17, then delete 6)
Change #define M 3 to any order. The program follows exactly the same rules as the visualizer above.
Common mistakes in this part
Inserting a new key into an internal node. New keys always go into a leaf.
Choosing the wrong median for an even order. State which middle key you send up and use it every time.
Borrowing a key straight from the sibling. Borrowing always goes through the parent: parent key down, sibling key up.
Forgetting that a merge can make the parent underflow, and that an empty root disappears (height −1).
Key points to remember
B-tree of order m: max m children and m − 1 keys; min ⌈m/2⌉ children (internal, non-root) and ⌈m/2⌉ − 1 keys (non-root).
All leaves on one level, always.
Insert into a leaf; on overflow split and push the median up. Only a root split increases the height.
Delete: from a leaf directly, or replace an internal key by its successor. On underflow borrow from a sibling if it can spare a key, otherwise merge.
Height is O(log⌈m/2⌉ n), so a lookup needs only a few disk reads.
Unit IV · Part 7
B+ Tree
A B-tree that keeps every data key in the leaves and links the leaves into a sorted list. One descent finds a key; a walk along the leaves answers a range. This is the index inside almost every database.
Syllabus: asked along with B-trees (definition, insertion, deletion, B-tree vs B+ tree).
7.1At a glance
A B+ tree keeps every data key in the leaves, uses the upper levels only as signposts, and links the leaves into one sorted list.
Problem it solves
Databases need both exact lookups and ranges ("salary between 30,000 and 50,000", ORDER BY). A B-tree range scan climbs up and down the tree; a B+ tree does one descent and then walks the leaf list. Index nodes hold only keys, so more fit in a page and the tree is shorter.
The rule
B-tree balance rules, plus: all keys live in leaves; index keys are copies; leaves are linked. Leaf split copies the key up, index split moves it up.
Think of it as
The index at the back of a book: the letter headings guide you, and the entries themselves are read one after another in order.
Use it when
Sorted data on disk with lookups and range scans: almost every relational database index.
Not the right tool when
Data is small and in memory, or you want a search to stop early at an internal node (a B-tree can; a B+ tree always goes to a leaf).
SearchO(log n), always to a leafRange of k keysO(log n + k)InsertO(log n)DeleteO(log n)
Used inMySQL InnoDBPostgreSQL, Oracle, SQL Server indexesSQLite tablesNTFS, XFS, JFS
A B+ tree of order m is a balanced multiway tree in which all data keys are stored in the leaves, the internal nodes store only copies of keys that guide the search, and the leaves are linked together in key order. Like a B-tree, all leaves are on the same level and every non-root node is at least half full.
Index set (internal nodes)
At most m children and m − 1 keys. Keys are signposts only: all keys in child Pi are ≥ Ki and < Ki+1.
Sequence set (leaves)
Hold every key (and the record or record pointer). Each leaf has a next pointer to the leaf on its right, forming a sorted linked list.
Properties of a B+ tree of order m
All leaves are on the same level, so the tree is always balanced.
An internal (index) node has at most m children and m − 1 keys, and at least ⌈m/2⌉ children. The root, if it is not a leaf, needs only 2 children.
A leaf holds at most m − 1 keys and at least ⌈m/2⌉ − 1 keys (unless the leaf is the root).
Every key is stored in a leaf. A key may also appear in an index node as a separator; that copy only guides the search.
The leaves are linked left to right, so the sequence set is a sorted linked list.
Every search ends at a leaf: the cost is always height + 1 node reads, O(log⌈m/2⌉ n).
How to read the B+ tree pictures
Order m = … above the picture is the order. Children hang left to right under each node, as in a binary tree.
Grey boxes are index nodes: their keys are only signposts. White boxes are leaves: together they hold every key.
A key can appear twice, once as a signpost and once in a leaf (17, 20, 25 and 31 in the figure below).
The dashed arrows are the next-leaf links: follow them to read all keys in sorted order.
At an index node, a key equal to a signpost goes to the right (keys ≥ Ki live on the right of Ki).
#define M 4
struct bpnode {
int isLeaf;
int n; /* keys in use (at most M-1) */
int key[M - 1];
struct bpnode *child[M]; /* used only in internal nodes */
struct bpnode *next; /* used only in leaves: next leaf */
};
7.3Insert (build the tree)
1Concept
Step
Rule
1. Find the leaf
Walk down the index (at each node take the pointer for the key's range; equal keys go right) and insert the key into the leaf in sorted order.
2. Leaf overflow (m keys)
Split the leaf: the left leaf keeps the first ⌈m/2⌉ keys, the rest go to a new right leaf. Copy the first key of the right leaf up into the parent (it stays in the leaf too). Link the new leaf into the leaf chain.
3. Index overflow (m keys)
Split the index node exactly as in a B-tree: the median key moves up (it is not kept below).
4. Root split
A new root is created. The height grows by one, at the top.
Leaf split → copy up. Index split → move up. This is the one rule that separates B+ insertion from B-tree insertion.
2Algorithm
Algorithm BPLUS-INSERT(root, x)
1. Find the leaf L for x (as in BPLUS-SEARCH) and insert x in sorted order.
2. If L has m keys (overflow):
Left = first ⌈m/2⌉ keys, Right = remaining keys
Right.next = L.next; Left.next = Right
COPY Right.key[0] into the parent (with Right as its new child)
3. While the parent P has m keys (overflow):
K = P.key[⌈m/2⌉ − 1] (median)
split P into keys before K and keys after K
MOVE K up into P's parent (K is not kept below)
If P was the root, K becomes a new root.
3Worked example
Step by step Construct a B+ tree
7.4Search and range query
1Concept
At an index node, find the first key greater than x and follow the pointer before it. If x equals a key, go to its right (keys ≥ Ki live on the right).
Repeat until a leaf is reached. The search always ends at a leaf, even if x was seen in an index node.
Range query lo…hi: search for lo, then walk along the leaf links collecting keys until a key > hi appears. No need to go back up the tree.
2Algorithm
Algorithm BPLUS-SEARCH(root, x)
1. p = root
2. While p is not a leaf:
i = number of keys in p that are ≤ x
p = p.child[i] (equal keys go right)
3. If x is in leaf p, return it; else "not found".
Algorithm RANGE(root, lo, hi)
1. L = the leaf reached by BPLUS-SEARCH(root, lo)
2. While L ≠ NULL:
for each key k in L: if lo ≤ k ≤ hi, output k; if k > hi, stop
L = L.next (follow the leaf link)
3Worked example
Step by step B+ tree search and range query
7.5Delete
1Concept
Situation
Action
Leaf keeps ≥ ⌈m/2⌉ − 1 keys
Remove the key from the leaf. If the same key is a signpost in an index node, it may stay: it still separates the ranges correctly.
Leaf underflows, sibling leaf can lend
Move one key across from the sibling and update the separator in the parent to the new first key of the right leaf.
Leaf underflows, sibling cannot lend
Merge the two leaves and delete the separator from the parent (nothing moves down, because leaf keys are the real data). Fix the leaf chain.
Index node underflows
Borrow through the parent or merge with the separator pulled down, exactly as in a B-tree.
2Algorithm
Algorithm BPLUS-DELETE(root, x)
1. Find the leaf L holding x and remove x.
2. If L is the root or L has ≥ ⌈m/2⌉ − 1 keys: stop.
3. If a sibling leaf S has a spare key:
move one key from S into L;
set the parent separator to the first key of the right-hand leaf. Stop.
4. Else merge L and S into one leaf (fix the next link) and
delete the separator between them from the parent.
5. If the parent underflows, fix it exactly as in a B-tree
(borrow through its parent, or merge pulling the separator down).
6. If the root has no keys left, its only child becomes the root.
3Worked example
Step by step Delete from a B+ tree
7.6B-tree vs. B+ tree
B-tree
B+ tree
Where data lives
In every node
Only in the leaves; internal nodes hold copies of keys as signposts
Duplicate keys in the tree
Never; each key appears once
A key can appear in an index node and in a leaf
Leaves
Not connected
Linked left to right like a list
Search ends
At any level (can stop early)
Always at a leaf (same cost for every key)
Range query (e.g. 20 to 60)
Needs an in-order walk over the whole tree
Find 20, then walk along the leaf list
Leaf split on insert
Median moves up
First key of the new leaf is copied up
Deletion
Harder: internal keys must be replaced
Easier: always from a leaf
Fan-out of internal nodes
Lower (nodes also carry data)
Higher (only keys), so the tree is shorter
Used in
General indexing, some file systems
Almost every database (MySQL InnoDB, PostgreSQL, Oracle) and many file systems
7.7Real-life applications
MySQL (InnoDB)
Every InnoDB table is stored as a B+ tree ordered by the primary key (the clustered index): the leaves hold the full rows. Each secondary index is another B+ tree whose leaves hold the primary key.
Why B+: lookups by key and scans in key order both stay cheap.
Other databases
The default index in PostgreSQL, Oracle and SQL Server is a B+ tree variant. SQLite stores each table as a B+ tree with rows in the leaves.
Why B+: high fan-out, so 3 or 4 levels for millions of rows.
Range queries and ORDER BY
WHERE age BETWEEN 20 AND 30: descend once to 20, then read the linked leaves until a key passes 30. ORDER BY on an indexed column simply reads the leaves in order.
Why B+: the leaf chain is already sorted.
File systems
NTFS indexes directory entries in B+ trees; XFS and JFS use B+ trees for directories and for the list of disk blocks (extents) of a file.
Why B+: huge directories and files still need only a few reads.
7.8Interview and placement questions
Questions asked in viva and interviews
Q1Why do databases prefer the B+ tree over the B-tree?
(1) Index nodes hold only keys and pointers, so more keys fit in a page and the tree is shorter. (2) Range scans and ORDER BY walk the linked leaves instead of going up and down the tree. (3) Every lookup reads the same number of pages, so performance is predictable. (4) Leaves can be read sequentially from disk.
Q2Why does a leaf split copy the key up, but an index split move it up?
Leaf keys are the data: the key must stay in the leaf, so the parent only gets a copy to act as a signpost. Index keys are only signposts: after a split the median is needed in exactly one place, the parent, so it moves.
Q3What is the time for a range query that returns k keys?
O(log n) to reach the first leaf plus O(k) to walk the leaves (about k / (keys per leaf) extra page reads).
Q4Clustered vs secondary index, in B+ tree terms?
In a clustered index the leaves hold the rows themselves, in key order (only one per table). A secondary index is another B+ tree whose leaves hold the key plus a pointer to the row (in InnoDB, the primary key), so a lookup may need a second descent in the clustered tree.
Q5After deleting a key, can it still appear in an index node?
Yes. A signpost only has to separate the ranges correctly, and the deleted key still does. It is replaced only if a borrow or merge rewrites that separator.
Q6A B+ tree has fan-out 500. How many levels does it need for 1 billion rows?
5003 = 1.25 × 108 is too small; 5004 = 6.25 × 1010 is enough, so about 4 levels (the leaves also hold several rows each, which reduces it further). With the root and the next level cached in memory, a lookup needs about 2 disk reads.
Q7Why is every B+ tree search the same length?
The data is only in the leaves and all leaves are on one level, so every search goes from the root to a leaf: height + 1 node reads, whether the key is found or not.
Coding problems (tested C programs)
C1Range query using the leaf linksMediumDescend once, then scan
Print every key between lo and hi in a B+ tree and count the nodes read.
Idea. Descend from the root to the leaf where lo belongs (equal keys go right). Then follow the next pointers, printing keys in the range and stopping at the first key greater than hi. The tree is never climbed again.
Time O(log n + k) · Space O(1)
Show the C program and its output
/* Q1. B+ tree range query: descend once to the leaf, then walk the leaf links */
#include <stdio.h>
#include <stdlib.h>
#define M 4
struct bpnode { int isLeaf, n, key[M - 1]; struct bpnode *child[M], *next; };
struct bpnode *node(int isLeaf, int n, const int *k) {
int i;
struct bpnode *t = calloc(1, sizeof *t);
t->isLeaf = isLeaf; t->n = n;
for (i = 0; i < n; i++) t->key[i] = k[i];
return t;
}
int reads = 0;
struct bpnode *findLeaf(struct bpnode *t, int x) {
int i;
while (!t->isLeaf) {
reads++;
i = 0; while (i < t->n && x >= t->key[i]) i++; /* equal keys go right */
t = t->child[i];
}
reads++;
return t;
}
void range(struct bpnode *root, int lo, int hi) {
struct bpnode *L = findLeaf(root, lo);
int i;
for (; L != NULL; L = L->next) { /* walk the sequence set */
for (i = 0; i < L->n; i++) {
if (L->key[i] > hi) return; /* past the range: stop */
if (L->key[i] >= lo) printf("%d ", L->key[i]);
}
if (L->next) reads++;
}
}
int main(void) {
/* the order-4 tree of 1 4 7 10 17 21 31 25 19 20 28 42 */
int r[] = {17}, i1[] = {7}, i2[] = {20, 25, 31};
int l1[] = {1, 4}, l2[] = {7, 10}, l3[] = {17, 19}, l4[] = {20, 21}, l5[] = {25, 28}, l6[] = {31, 42};
struct bpnode *root = node(0, 1, r), *a = node(0, 1, i1), *b = node(0, 3, i2);
struct bpnode *L[6];
int i;
L[0] = node(1, 2, l1); L[1] = node(1, 2, l2); L[2] = node(1, 2, l3);
L[3] = node(1, 2, l4); L[4] = node(1, 2, l5); L[5] = node(1, 2, l6);
for (i = 0; i < 5; i++) L[i]->next = L[i + 1]; /* link the leaves */
root->child[0] = a; root->child[1] = b;
a->child[0] = L[0]; a->child[1] = L[1];
b->child[0] = L[2]; b->child[1] = L[3]; b->child[2] = L[4]; b->child[3] = L[5];
printf("Keys in [5, 21]: ");
range(root, 5, 21);
printf("\nNodes read: %d\n", reads);
return 0;
}
Output
Keys in [5, 21]: 7 10 17 19 20 21
Nodes read: 7
7.9Lab, algorithm tracer and complete program
Try your own keys in the lab, follow the algorithms step by step in the tracer, and then read the complete C program (insert, search, range query and a level-by-level print).
Try it B+ tree lab
For a range query type two keys (low and high) and press Range.
Algorithm tracer: B+ tree insert, search, range and delete
This tracer highlights the algorithm step being carried out while the tree changes.
Trace B+ tree algorithms, step by step
Program 6 · B+ tree of order M: insert, search, range query, level print
/* B+ tree of order M: insert (leaf split copies up, index split moves up), search, range query */
#include <stdio.h>
#include <stdlib.h>
#define M 4 /* order: at most M children, M-1 keys */
struct bp {
int leaf, n;
int key[M]; /* one spare slot holds a key during an overflow */
struct bp *child[M + 1];
struct bp *next; /* leaves only: the leaf to the right */
};
struct bp *root = NULL;
struct bp *newNode(int leaf) { struct bp *t = calloc(1, sizeof *t); t->leaf = leaf; return t; }
/* insert x below t; if t splits, return 1 and give back the key and node for the parent */
int insertRec(struct bp *t, int x, int *upKey, struct bp **upNode) {
int i, j, mid, k;
struct bp *r, *nr;
if (t->leaf) {
for (i = 0; i < t->n && t->key[i] < x; i++) ;
if (i < t->n && t->key[i] == x) return 0; /* duplicate */
for (j = t->n; j > i; j--) t->key[j] = t->key[j - 1];
t->key[i] = x; t->n++;
if (t->n < M) return 0; /* no overflow */
mid = (M + 1) / 2; /* left leaf keeps ceil(M/2) keys */
r = newNode(1);
for (j = mid; j < t->n; j++) r->key[r->n++] = t->key[j];
t->n = mid;
r->next = t->next; t->next = r; /* link the new leaf in */
*upKey = r->key[0]; /* COPY the first key up */
*upNode = r;
return 1;
}
for (i = 0; i < t->n && x >= t->key[i]; i++) ; /* equal keys go right */
if (!insertRec(t->child[i], x, &k, &nr)) return 0;
for (j = t->n; j > i; j--) { t->key[j] = t->key[j - 1]; t->child[j + 1] = t->child[j]; }
t->key[i] = k; t->child[i + 1] = nr; t->n++;
if (t->n < M) return 0;
mid = (M + 1) / 2 - 1; /* median of the index node */
r = newNode(0);
for (j = mid + 1; j < t->n; j++) { r->key[r->n] = t->key[j]; r->child[r->n] = t->child[j]; r->n++; }
r->child[r->n] = t->child[t->n];
*upKey = t->key[mid]; /* MOVE the median up */
*upNode = r;
t->n = mid;
return 1;
}
void insert(int x) {
int k;
struct bp *nr, *s;
if (root == NULL) { root = newNode(1); root->key[0] = x; root->n = 1; return; }
if (insertRec(root, x, &k, &nr)) { /* the root split: grow at the top */
s = newNode(0);
s->key[0] = k; s->child[0] = root; s->child[1] = nr; s->n = 1;
root = s;
}
}
struct bp *findLeaf(int x) {
struct bp *t = root;
int i;
while (!t->leaf) {
for (i = 0; i < t->n && x >= t->key[i]; i++) ;
t = t->child[i];
}
return t;
}
int search(int x) {
struct bp *L;
int i;
if (root == NULL) return 0;
L = findLeaf(x); /* every search ends in a leaf */
for (i = 0; i < L->n; i++) if (L->key[i] == x) return 1;
return 0;
}
void range(int lo, int hi) {
struct bp *L;
int i;
if (root == NULL) return;
for (L = findLeaf(lo); L != NULL; L = L->next) /* one descent, then walk right */
for (i = 0; i < L->n; i++) {
if (L->key[i] > hi) return;
if (L->key[i] >= lo) printf("%d ", L->key[i]);
}
}
void show(void) { /* level by level, then the leaf chain */
struct bp *q[200], *t;
int f = 0, b = 0, i, lv = 0, cnt;
if (root == NULL) { printf("empty\n"); return; }
q[b++] = root;
while (f < b) {
cnt = b - f;
printf("Level %d: ", lv++);
while (cnt--) {
t = q[f++];
printf("[");
for (i = 0; i < t->n; i++) printf(i ? " %d" : "%d", t->key[i]);
printf("] ");
if (!t->leaf) for (i = 0; i <= t->n; i++) q[b++] = t->child[i];
}
printf("\n");
}
printf("Leaf chain: ");
for (t = findLeaf(-2147483647); t != NULL; t = t->next) {
printf("[");
for (i = 0; i < t->n; i++) printf(i ? " %d" : "%d", t->key[i]);
printf(t->next ? "] -> " : "]\n");
}
}
int main(void) {
int ch, k, i, x, y;
while (1) {
printf("\n1.Insert many 2.Search 3.Range 4.Show 5.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &k); printf("Keys: ");
for (i = 0; i < k; i++) { scanf("%d", &x); insert(x); } break;
case 2: printf("Key: "); scanf("%d", &x); printf(search(x) ? "Found in a leaf\n" : "Not found\n"); break;
case 3: printf("Low and high: "); scanf("%d %d", &x, &y); printf("Keys: "); range(x, y); printf("\n"); break;
case 4: show(); break;
case 5: return 0;
}
}
return 0;
}
Sample run
1.Insert many 2.Search 3.Range 4.Show 5.Exit
Choice: 1
How many? 12
Keys: 1 4 7 10 17 21 31 25 19 20 28 42
1.Insert many 2.Search 3.Range 4.Show 5.Exit
Choice: 4
Level 0: [17]
Level 1: [7] [20 25 31]
Level 2: [1 4] [7 10] [17 19] [20 21] [25 28] [31 42]
Leaf chain: [1 4] -> [7 10] -> [17 19] -> [20 21] -> [25 28] -> [31 42]
1.Insert many 2.Search 3.Range 4.Show 5.Exit
Choice: 2
Key: 20
Found in a leaf
1.Insert many 2.Search 3.Range 4.Show 5.Exit
Choice: 2
Key: 22
Not found
1.Insert many 2.Search 3.Range 4.Show 5.Exit
Choice: 3
Low and high: 5 21
Keys: 7 10 17 19 20 21
1.Insert many 2.Search 3.Range 4.Show 5.Exit
Choice: 5
Compared with the visualizer on 160 random key sequences for orders 3 to 6: the trees are identical. Deletion follows the algorithm in 7.5.
Common mistakes in this part
In a B+ tree, moving the key up on a leaf split. Leaf splits copy the key up; only index splits move it.
Stopping a B+ tree search at an index node. Every search must end at a leaf.
Key points to remember
B+ tree: all data in linked leaves, index nodes hold copies. Leaf split copies the key up; index split moves it up. Every search ends at a leaf; range queries walk the leaf chain.
Unit IV · Part 4
AVL Tree
A BST that checks its balance after every insert and delete, and repairs it with rotations, so the height never goes beyond about 1.44 log2 n.
Syllabus: AVL tree · single rotations · double rotations.
4.1At a glance
An AVL tree is a BST that re-balances itself after every insert and delete, so that at every node the heights of the two subtrees differ by at most 1.
Problem it solves
A BST built from sorted keys becomes a chain and every operation costs O(n). An AVL tree guarantees O(log n) whatever the order of the keys.
The rule
Balance factor BF = height(left) − height(right) must be −1, 0 or +1 at every node. A node that reaches ±2 is fixed by an LL, RR, LR or RL rotation.
Think of it as
A see-saw you level again the moment one side becomes two steps heavier.
Use it when
Lookups are far more frequent than updates and you need a worst-case guarantee.
Not the right tool when
Inserts and deletes dominate (a red-black tree rotates less), or data is on disk (use a B-tree).
SearchO(log n)InsertO(log n), at most 1 rotationDeleteO(log n), up to O(log n) rotationsHeight< 1.44 log2(n + 2)
Used inLookup-heavy in-memory indexesDictionaries and symbol tablesOrdered sets and maps
An AVL tree (Adelson-Velsky and Landis, 1962) is a binary search tree in which, for every node, the heights of the left and right subtrees differ by at most 1.
Balance factor BF(node) = height(left subtree) − height(right subtree). In an AVL tree every BF is −1, 0 or +1.
BF = +1 means left-heavy, −1 means right-heavy, 0 means equal. An empty subtree has height −1, so a leaf has BF = 0. In the figures, the small number beside each node is its BF.
How to calculate BF (left tree above)
Work bottom-up: find each subtree's height first (leaf = 0, empty = −1), then subtract.
Node
h(left)
h(right)
BF = h(L) − h(R)
Meaning
10, 30, 60 (leaves)
−1
−1
0
Balanced
20
0
0
0
Balanced
50
−1
0
−1
Right-heavy, allowed
40 (root)
1
1
0
Balanced
Properties of an AVL tree
It is a binary search tree: left < node < right, so its in-order traversal is sorted.
Every node has BF −1, 0 or +1, and therefore every subtree is itself an AVL tree.
Its height is less than 1.44 log2(n + 2), so search, insert and delete are all O(log n) (proved in 4.7).
Each node stores its height (or its BF), so the balance of a node can be checked in O(1) on the way back up after an update.
A perfect or complete binary search tree is always an AVL tree (its shape always satisfies the balance rule), but an AVL tree need not be complete: a BF of ±1 leaves gaps.
Node structure in C
An AVL node is a BST node plus one extra field, the height of the subtree rooted at it. The balance factor is computed from the children's heights whenever it is needed.
struct node {
int data, height; /* height in edges: leaf = 0, NULL = -1 */
struct node *left, *right;
};
int h(struct node *n) { return n ? n->height : -1; }
int max(int a, int b) { return a > b ? a : b; }
void update(struct node *n) { n->height = 1 + max(h(n->left), h(n->right)); }
int BF(struct node *n) { return n ? h(n->left) - h(n->right) : 0; }
How to check whether a tree is an AVL tree
Apply two tests. (1) Is it a BST? Its in-order traversal must be sorted. (2) Is it balanced? Compute every height bottom-up and check |BF| ≤ 1 at every node, not only at the root. A tree that fails either test is not AVL. Problems P1 and P2 practise this.
Real-life picture: a well-run tournament bracket. No side of the bracket is allowed to be more than one round deeper than the other, so every player reaches the final in about log2 n rounds.
4.3Why AVL?
Part 3 showed the problem: a BST's cost is its height, and sorted keys make a chain of height n − 1. An AVL tree pays for a rotation on the way back up so that height stays close to the shortest possible tree.
n keys
Skewed BST height (worst)
AVL tree height (worst)
Minimum possible height (complete tree)
15
14
4
3
1,000
999
13
9
1,000,000
999,999
27
19
There are two ways to keep a tree short:
Perfect balance
Keep every level full. The height is the smallest possible, but restoring this after one insertion can mean rebuilding the whole tree: O(n) per update. Too expensive.
Height balance (AVL)
Allow the two subtrees of every node to differ in height by at most 1. Balance is restored by O(1)-time rotations on the path that changed (at most one for an insertion, possibly one per level for a deletion), so each update stays O(log n). The height is at most about 44% more than perfect.
The AVL tree (Adelson-Velsky and Landis, 1962) was the first self-balancing binary search tree. It pays a small price on every insert and delete (updating heights, and sometimes rotating) to guarantee O(log n) for every later operation, whatever the order of the keys.
4.4Rotations
1Concept
A rotation rearranges three nodes (z = the unbalanced node, y = its taller child, x = y's taller child) and their subtrees T1–T4. It keeps the BST order (the in-order sequence of the nodes and subtrees does not change: in the LL picture below it is T1 10 T2 20 T3 30 T4 before and after) and, after an insertion, reduces the height of that part of the tree by one (after a deletion where y has BF 0 the height stays the same; see 4.6).
Name the case by the path from the unbalanced node down to where the extra height is: two steps Left = LL, Left then Right = LR, and so on.
Single rotations
LL case: the new node went into the left subtree of the left child. Fix: one right rotation at z (clockwise).
RR case: the new node went into the right subtree of the right child. Fix: one left rotation at z (anticlockwise).
Double rotations
If the extra height is on the inside (a zig-zag), one rotation only moves the problem to the other side. First rotate at the child to make a straight line, then rotate at z.
LR case: the new node went into the right subtree of the left child. Fix: left rotation at y (anticlockwise), then right rotation at z (clockwise).
RL case: the new node went into the left subtree of the right child. Fix: right rotation at y (clockwise), then left rotation at z (anticlockwise).
All four cases end in the same shape: the middle key on top with the other two as its children. The worked example below shows each case with three keys.
2Algorithm
Which rotation? (identification table)
BF of unbalanced node z
BF of its taller child y
Case
Rotation
+2
+1 (or 0, delete only)
LL
Single: right at z
−2
−1 (or 0, delete only)
RR
Single: left at z
+2
−1
LR
Double: left at y, right at z
−2
+1
RL
Double: right at y, left at z
Memory aid: same signs (+2/+1, −2/−1) mean a straight line and one rotation. Opposite signs (+2/−1, −2/+1) mean a zig-zag and two rotations.
Algorithm RIGHT-ROTATE(z) (fixes LL)
1. y = z.left; T3 = y.right
2. y.right = z; z.left = T3
3. Update height(z), then height(y) (z is now below y)
4. Return y (the caller links y where z was)
Algorithm LEFT-ROTATE(z) (fixes RR, mirror image)
1. y = z.right; T2 = y.left
2. y.left = z; z.right = T2
3. Update height(z), then height(y)
4. Return y
LR: z.left = LEFT-ROTATE(z.left); then RIGHT-ROTATE(z)
RL: z.right = RIGHT-ROTATE(z.right); then LEFT-ROTATE(z)
3Worked example
Step by step Rotation visualizer: every step of every case
4C code
What changes in memory: right rotation at z
A rotation changes only three pointers, so it takes O(1) time. For a right rotation at z with left child y:
#
Pointer
Before
After
1
z→left
y
T3 (y's old right subtree)
2
y→right
T3
z
3
parent's pointer (or root)
z
y
Then recompute the heights of z first (it is now lower) and then y. A left rotation is the mirror image.
In the C code (as in Program 5) the unbalanced node is called y, its child x, and the subtree that moves is T2 (the T3 of the table above).
struct node *rightRotate(struct node *y) { /* LL */
struct node *x = y->left, *T2 = x->right;
x->right = y; /* y comes down to the right of x */
y->left = T2; /* x's old right subtree moves to y */
update(y); update(x); /* recompute heights, lower node first */
return x; /* x is the new root of this subtree */
}
struct node *leftRotate(struct node *x) { /* RR */
struct node *y = x->right, *T2 = y->left;
y->left = x;
x->right = T2;
update(x); update(y);
return y;
}
4.5Insertion
1Concept
Insert the key exactly as in a BST (it becomes a leaf).
Walk back up the path towards the root, updating heights and balance factors.
At the first node whose BF becomes ±2, identify the case (LL, RR, LR or RL) and apply the rotation.
After one rotation (single or double) the subtree is back to its old height, so no other ancestor needs fixing. An insertion needs at most one rotation.
2Algorithm
Algorithm AVL-INSERT(node, key)
1. If node = NULL, return a new node (height 0).
2. If key < node.data, node.left = AVL-INSERT(node.left, key)
Else if key > node.data, node.right = AVL-INSERT(node.right, key)
Else return node (duplicate).
3. Update node.height = 1 + max(height(left), height(right)).
4. BF = height(left) − height(right).
5. If BF = +2 and BF(node.left) ≥ 0 : return RIGHT-ROTATE(node) (LL)
If BF = +2 and BF(node.left) < 0 : node.left = LEFT-ROTATE(node.left);
return RIGHT-ROTATE(node) (LR)
If BF = −2 and BF(node.right) ≤ 0 : return LEFT-ROTATE(node) (RR)
If BF = −2 and BF(node.right) > 0 : node.right = RIGHT-ROTATE(node.right);
return LEFT-ROTATE(node) (RL)
6. Return node.
3Worked example
Step by step Construct an AVL tree
Keys that are words (letters, months) are compared alphabetically.
4C code
insert is the BST insert with one change: on the way back up every node passes through balance(), which recomputes its height and applies the rotation if its BF is ±2. The rotations are the ones shown in 4.4.
/* called on every node on the way back up */
struct node *balance(struct node *n) {
update(n);
if (BF(n) > 1) {
if (BF(n->left) < 0) { /* LR */
printf(" LR at %d\n", n->data);
n->left = leftRotate(n->left);
} else printf(" LL at %d\n", n->data);
return rightRotate(n);
}
if (BF(n) < -1) {
if (BF(n->right) > 0) { /* RL */
printf(" RL at %d\n", n->data);
n->right = rightRotate(n->right);
} else printf(" RR at %d\n", n->data);
return leftRotate(n);
}
return n;
}
struct node *insert(struct node *n, int x) {
if (n == NULL) return createNode(x);
if (x < n->data) n->left = insert(n->left, x);
else if (x > n->data) n->right = insert(n->right, x);
else return n; /* duplicate */
return balance(n);
}
★Try it
Press ⟨/⟩ Trace this example in the C code under any example above to watch insert, balance and the rotations run line by line. More construction problems are in 4.9.
4.6Deletion
1Concept
Delete as in a BST (the two-children case uses the in-order successor).
Walk up from the parent of the node that was physically removed, updating heights.
At every node with BF ±2, look at the child on the taller side (the sibling of the side that got shorter) and use the same identification table. When that child's BF is 0, a single rotation is used (this case never happens during insertion).
A rotation after deletion can make the subtree shorter, which can unbalance a higher ancestor. Keep checking all the way to the root. A deletion may need up to O(log n) rotations.
Many textbooks name the deletion cases R0, R1, R−1 (deleted from the right side) and L0, L1, L−1 (deleted from the left side). The number is the BF of the sibling node on the other side.
Case
Situation after deleting from the right
Rotation
Height of subtree after fix
R0
Sibling (left child) has BF 0
Single right (like LL)
Unchanged: stop
R1
Sibling has BF +1
Single right (LL)
Decreases by 1: keep checking up
R−1
Sibling has BF −1
Double: left, then right (LR)
Decreases by 1: keep checking up
L0, L1, L−1
Mirror images (deleted from the left)
Left / RL
Same as above
2Algorithm
Algorithm AVL-DELETE(node, key)
1. Delete key as in a BST (two children → copy the in-order successor
and delete it from the right subtree).
2. On the way back up, at every node on the path:
update height; BF = height(left) − height(right)
If BF = +2: if BF(left) ≥ 0: RIGHT-ROTATE (R0 / R1, like LL)
else: LEFT-ROTATE(left), RIGHT-ROTATE (R−1, like LR)
If BF = −2: if BF(right) ≤ 0: LEFT-ROTATE (L0 / L−1, like RR)
else: RIGHT-ROTATE(right), LEFT-ROTATE (L1, like RL)
3. Continue up to the root (do not stop after the first rotation).
3Worked example
R0 Delete 70 from 50(30(20, 40), 70)
BF(50) becomes +2; its left child 30 has BF 0. One right rotation at 50; 30 becomes the root.
R1 Delete 70 from 50(30(20, ), 70)
BF(50) becomes +2; its left child 30 has BF +1. One right rotation at 50.
R−1 Delete 70 from 50(30( , 40), 70)
BF(50) becomes +2; its left child 30 has BF −1 (zig-zag). Left rotation at 30, then right rotation at 50; 40 becomes the root.
Two rotations One deletion, two fixes: delete 25
25 has two children, so its successor 30 replaces it. Node 30 (the successor copied into 25's place) loses its right child, so BF(30) = +2 and its left child 10 has BF +1 (LL: right rotation at 30). That subtree becomes one level shorter, which makes the root 35 right-heavy: BF −2, right child 65 has BF −1 (RR: left rotation at 35). This is why deletion must check every ancestor.
More deletion examples, step by step
Step by step Delete from an AVL tree
4C code
Deletion is the BST delete followed by the same balance() call on every node of the path. Because the recursion returns through every ancestor, the check automatically continues up to the root.
struct node *deleteNode(struct node *n, int x) {
struct node *t;
if (n == NULL) { printf("%d not found\n", x); return NULL; }
if (x < n->data) n->left = deleteNode(n->left, x);
else if (x > n->data) n->right = deleteNode(n->right, x);
else {
if (n->left == NULL || n->right == NULL) {
t = n->left ? n->left : n->right;
free(n);
return t; /* NULL for a leaf */
}
t = n->right; /* in-order successor */
while (t->left) t = t->left;
n->data = t->data;
n->right = deleteNode(n->right, t->data);
}
return balance(n);
}
4.7Search, height bound and complexity
Search
Searching an AVL tree is exactly BST search: compare with the node, go left or right, stop at the key or at NULL. Nothing changes in the tree, so no rotation is ever needed. The difference is the guarantee: the path is never longer than the height, and the height is always O(log n).
Why the height is O(log n)
Let N(h) be the smallest number of nodes an AVL tree of height h can have. To use as few nodes as possible, the root has one subtree of height h − 1 and the other of height h − 2 (the most unequal pair that is still allowed), and each of those is itself as sparse as possible:
N(h) = N(h − 1) + N(h − 2) + 1, with N(0) = 1 and N(1) = 2.
So N = 1, 2, 4, 7, 12, 20, 33, … This is the Fibonacci pattern: N(h) = F(h + 3) − 1. A quick bound: N(h) > 2 N(h − 2), so the node count at least doubles every two levels, giving N(h) ≥ 2h/2 and h ≤ 2 log2 n. Using the Fibonacci numbers exactly gives the tighter bound:
Height of an AVL tree with n nodes < 1.44 log2(n + 2), so it is O(log n).
Height h
0
1
2
3
4
5
6
Minimum nodes N(h)
1
2
4
7
12
20
33
Maximum nodes 2h+1 − 1
1
3
7
15
31
63
127
Operation
AVL (every case)
BST (worst case)
Search
O(log n)
O(n)
Insert
O(log n), at most 1 rotation
O(n)
Delete
O(log n), up to O(log n) rotations
O(n)
Rotation itself
O(1)
—
4.8Real-life applications, advantages and comparison
Lookup-heavy in-memory indexes
Tables that are built once and then searched millions of times (a dictionary, a routing or configuration table, a symbol table after compilation starts) gain most from the lowest possible height.
Why AVL: it is the most tightly balanced BST, so searches are shortest.
Ordered sets and maps
Any program that needs a sorted collection with guaranteed O(log n) insert, delete and search, plus min, max, floor, ceil and range queries.
Why AVL: BST features with a worst-case guarantee.
Teaching self-balancing
Rotations, balance factors and "fix the lowest unbalanced node" are the same ideas used by red-black, splay and treap trees.
Why AVL: the simplest balance rule to state and check.
Advantages
Guaranteed O(log n) search, insert and delete · the most strictly balanced BST, so lookups are the fastest · in-order is still sorted.
Disadvantages
Every node stores a height or BF · rotations make insert and delete code longer · deletion may rotate at many levels.
Try each problem on paper first, then open the solution. Every answer was produced by the same tested code that drives the animations, and every construction and deletion can be replayed step by step, or traced line by line in the C program with the ⟨/⟩ button.
How to write an AVL construction answer
Insert the key exactly as in a BST.
Write the BF of every node on the path back to the root.
Stop at the lowest node z with BF +2 or −2 (the first one you meet going up).
Look at z's taller child y. Same signs (+2/+1, −2/−1): single rotation (LL or RR). Opposite signs (+2/−1, −2/+1): double rotation (LR or RL).
Rotate, then redraw the whole tree with its new balance factors before inserting the next key.
A. Balance factor and “is it an AVL tree?”
P1Balance factor
Find the height and balance factor of every node of the tree below. Is it an AVL tree?
Show the solution
Work bottom-up. An empty subtree has height −1, a leaf has height 0.
Node
h(left)
h(right)
height
BF
10
−1
−1
0
0
20
0
−1
1
+1
40
−1
−1
0
0
30
1
0
2
+1
90
−1
−1
0
0
80
−1
0
1
−1
70
−1
1
2
−2
50 (root)
2
2
3
0
Answer.Not an AVL tree. The root is perfectly balanced (BF 0), but node 70 has BF −2. Every node must be checked, not just the root. (An RR rotation at 70 would repair it.)
P2Checking
Which of these four trees are AVL trees? Give a reason for each.
Show the solution
A tree is AVL only if both tests pass: (1) it is a BST, and (2) every node has BF −1, 0 or +1.
Tree
BST?
All |BF| ≤ 1?
AVL?
(a)
Yes
Yes, every BF is 0 (perfect tree)
Yes
(b)
No: 40 is in the left subtree of 30 but 40 > 30
Yes
No
(c)
Yes
No: BF(20) = +2 and BF(40) = +2
No
(d)
Yes
Yes: BFs are 50:+1, 30:−1, 40:+1, 60:−1, leaves 0
Yes
Answer. (a) and (d) are AVL trees. (b) is height-balanced but is not a BST, so it is not AVL. (c) is a BST but is not balanced.
B. Naming the rotation
P3Rotation type
Insert each sequence into an empty AVL tree. Which rotation is needed, and which key ends up as the root? (a) 50, 40, 30 (b) 50, 60, 70 (c) 50, 30, 40 (d) 50, 70, 60
Show the solution
Sequence
After the 3rd key
Case
Fix
New root
(a) 50, 40, 30
BF(50) = +2, BF(40) = +1
LL
Right rotation at 50
40
(b) 50, 60, 70
BF(50) = −2, BF(60) = −1
RR
Left rotation at 50
60
(c) 50, 30, 40
BF(50) = +2, BF(30) = −1
LR
Left at 30, right at 50
40
(d) 50, 70, 60
BF(50) = −2, BF(70) = +1
RL
Right at 70, left at 50
60
Answer. LL, RR, LR, RL. In every case the middle key of the three becomes the root.
P4First unbalanced node
T1 is the AVL tree built from 20, 10, 30, 5 and T2 is the AVL tree built from 20, 10, 30, 25. Find the first unbalanced node and the rotation when you insert (a) 1 into T1, (b) 7 into T1, (c) 27 into T2, (d) 40 into the tree built from 20, 10, 30, 35.
Show the solution
The unbalanced node is always an ancestor of the new key. In all four cases the root 20 also reaches BF ±2, but we fix only the lowest unbalanced node: that one rotation makes its subtree as short as it was before the insertion, so the root is balanced again.
Insert
z (BF)
y (BF)
Case
Result
(a) 1 into 20(10(5), 30)
10 (+2)
5 (+1)
LL
20(5(1, 10), 30)
(b) 7 into 20(10(5), 30)
10 (+2)
5 (−1)
LR
20(7(5, 10), 30)
(c) 27 into 20(10, 30(25))
30 (+2)
25 (−1)
LR
20(10, 27(25, 30))
(d) 40 into 20(10, 30(, 35))
30 (−2)
35 (−1)
RR
20(10, 35(30, 40))
Answer. (a) LL at 10, (b) LR at 10, (c) LR at 30, (d) RR at 30. The root stays 20 in all four.
P5Insert into a given tree
The AVL tree 40(20(10, 30), 60( , 70)) is given. Insert (a) 80 and, separately, (b) 65. For each, name the first unbalanced node and the rotation, and draw the result.
Show the solution
Insert
Path
z (BF)
y (BF)
Case
Result
(a) 80
40 → 60 → 70
60 (−2)
70 (−1)
RR: left rotation at 60
40(20(10, 30), 70(60, 80))
(b) 65
40 → 60 → 70
60 (−2)
70 (+1)
RL: right at 70, left at 60
40(20(10, 30), 65(60, 70))
Answer. Both keys go into the right subtree of 60, below 70. 80 (right of 70) makes a straight line (RR); 65 (left of 70) makes a zig-zag (RL). In both results the root 40 has BF 0, because both of its subtrees now have height 1.
C. Construction by insertion
P6Construction
Construct an AVL tree by inserting 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 one at a time into an empty tree. Name the rotation used at every step and draw the final tree. Compare the height with a plain BST built from the same keys.
Show the solution
Method: insert each key as in a BST, walk back up updating the balance factors, and at the first node whose BF becomes ±2 read the case from the signs of BF(z) and BF(y). Redraw the whole tree after every rotation.
Insert
First unbalanced node z
Taller child y
Case
Fix
3
1 (BF −2)
2 (BF −1)
RR
Left rotation at 1
5
3 (BF −2)
4 (BF −1)
RR
Left rotation at 3
6
2 (BF −2)
4 (BF −1)
RR
Left rotation at 2
7
5 (BF −2)
6 (BF −1)
RR
Left rotation at 5
9
7 (BF −2)
8 (BF −1)
RR
Left rotation at 7
10
6 (BF −2)
8 (BF −1)
RR
Left rotation at 6
1, 2, 4, 8
No node reaches BF ±2: no rotation
Answer. 6 rotations (6 RR). Final pre-order: 4 2 1 3 8 6 5 7 9 10. Height 3. Check: the in-order 1 2 3 4 5 6 7 8 9 10 is sorted and every BF is −1, 0 or +1. A plain BST from sorted keys is a straight line of height 9; the AVL tree has height 3. Only RR rotations appear because every key goes to the right.
P7Construction
Construct an AVL tree by inserting 100, 90, 80, 70, 60, 50, 40 one at a time into an empty tree. Name the rotation used at every step and draw the final tree.
Show the solution
Method: insert each key as in a BST, walk back up updating the balance factors, and at the first node whose BF becomes ±2 read the case from the signs of BF(z) and BF(y). Redraw the whole tree after every rotation.
Insert
First unbalanced node z
Taller child y
Case
Fix
80
100 (BF +2)
90 (BF +1)
LL
Right rotation at 100
60
80 (BF +2)
70 (BF +1)
LL
Right rotation at 80
50
90 (BF +2)
70 (BF +1)
LL
Right rotation at 90
40
60 (BF +2)
50 (BF +1)
LL
Right rotation at 60
100, 90, 70
No node reaches BF ±2: no rotation
Answer. 4 rotations (4 LL). Final pre-order: 70 50 40 60 90 80 100. Height 2. Check: the in-order 40 50 60 70 80 90 100 is sorted and every BF is −1, 0 or +1. Keys in decreasing order produce only LL rotations (the mirror of P6) and a perfect tree.
P8Construction
Construct an AVL tree by inserting 21, 26, 30, 9, 4, 14, 28, 18, 15, 10, 2, 3, 7 one at a time into an empty tree. Name the rotation used at every step and draw the final tree.
Show the solution
Method: insert each key as in a BST, walk back up updating the balance factors, and at the first node whose BF becomes ±2 read the case from the signs of BF(z) and BF(y). Redraw the whole tree after every rotation.
Insert
First unbalanced node z
Taller child y
Case
Fix
30
21 (BF −2)
26 (BF −1)
RR
Left rotation at 21
4
21 (BF +2)
9 (BF +1)
LL
Right rotation at 21
14
26 (BF +2)
9 (BF −1)
LR
Left at 9, then right at 26
28
26 (BF −2)
30 (BF +1)
RL
Right at 30, then left at 26
15
14 (BF −2)
18 (BF +1)
RL
Right at 18, then left at 14
10
9 (BF −2)
15 (BF +1)
RL
Right at 15, then left at 9
2
21 (BF +2)
14 (BF +1)
LL
Right rotation at 21
3
4 (BF +2)
2 (BF −1)
LR
Left at 2, then right at 4
7
9 (BF +2)
3 (BF −1)
LR
Left at 3, then right at 9
21, 26, 9, 18
No node reaches BF ±2: no rotation
Answer. 9 rotations (2 LL, 1 RR, 3 LR, 3 RL). Final pre-order: 14 4 3 2 9 7 10 21 15 18 28 26 30. Height 3. Check: the in-order 2 3 4 7 9 10 14 15 18 21 26 28 30 is sorted and every BF is −1, 0 or +1.
P9Construction
Construct an AVL tree by inserting 63, 9, 19, 27, 18, 108, 99, 81 one at a time into an empty tree. Name the rotation used at every step and draw the final tree.
Show the solution
Method: insert each key as in a BST, walk back up updating the balance factors, and at the first node whose BF becomes ±2 read the case from the signs of BF(z) and BF(y). Redraw the whole tree after every rotation.
Insert
First unbalanced node z
Taller child y
Case
Fix
19
63 (BF +2)
9 (BF −1)
LR
Left at 9, then right at 63
81
108 (BF +2)
99 (BF +1)
LL
Right rotation at 108
63, 9, 27, 18, 108, 99
No node reaches BF ±2: no rotation
Answer. 2 rotations (1 LL, 1 LR). Final pre-order: 19 9 18 63 27 99 81 108. Height 3. Check: the in-order 9 18 19 27 63 81 99 108 is sorted and every BF is −1, 0 or +1.
P10Construction
Construct an AVL tree by inserting 15, 20, 24, 10, 13, 7, 30, 36, 25 one at a time into an empty tree. Name the rotation used at every step and draw the final tree.
Show the solution
Method: insert each key as in a BST, walk back up updating the balance factors, and at the first node whose BF becomes ±2 read the case from the signs of BF(z) and BF(y). Redraw the whole tree after every rotation.
Insert
First unbalanced node z
Taller child y
Case
Fix
24
15 (BF −2)
20 (BF −1)
RR
Left rotation at 15
13
15 (BF +2)
10 (BF −1)
LR
Left at 10, then right at 15
7
20 (BF +2)
13 (BF +1)
LL
Right rotation at 20
36
24 (BF −2)
30 (BF −1)
RR
Left rotation at 24
25
20 (BF −2)
30 (BF +1)
RL
Right at 30, then left at 20
15, 20, 10, 30
No node reaches BF ±2: no rotation
Answer. 5 rotations (1 LL, 2 RR, 1 LR, 1 RL). Final pre-order: 13 10 7 24 20 15 30 25 36. Height 3. Check: the in-order 7 10 13 15 20 24 25 30 36 is sorted and every BF is −1, 0 or +1.
P11Construction
Construct an AVL tree by inserting 8, 9, 10, 2, 1, 5, 3, 6, 4, 7, 11, 12 one at a time into an empty tree. Name the rotation used at every step and draw the final tree.
Show the solution
Method: insert each key as in a BST, walk back up updating the balance factors, and at the first node whose BF becomes ±2 read the case from the signs of BF(z) and BF(y). Redraw the whole tree after every rotation.
Insert
First unbalanced node z
Taller child y
Case
Fix
10
8 (BF −2)
9 (BF −1)
RR
Left rotation at 8
1
8 (BF +2)
2 (BF +1)
LL
Right rotation at 8
5
9 (BF +2)
2 (BF −1)
LR
Left at 2, then right at 9
4
2 (BF −2)
5 (BF +1)
RL
Right at 5, then left at 2
7
8 (BF +2)
3 (BF −1)
LR
Left at 3, then right at 8
11
9 (BF −2)
10 (BF −1)
RR
Left rotation at 9
8, 9, 2, 3, 6, 12
No node reaches BF ±2: no rotation
Answer. 6 rotations (1 LL, 2 RR, 2 LR, 1 RL). Final pre-order: 5 3 2 1 4 8 6 7 10 9 11 12. Height 4. Check: the in-order 1 2 3 4 5 6 7 8 9 10 11 12 is sorted and every BF is −1, 0 or +1.
P12Construction
Construct an AVL tree by inserting H, I, J, B, A, E, C, F, D, G, K, L one at a time into an empty tree. Name the rotation used at every step and draw the final tree. Keys are compared in alphabetical order.
Show the solution
Method: insert each key as in a BST, walk back up updating the balance factors, and at the first node whose BF becomes ±2 read the case from the signs of BF(z) and BF(y). Redraw the whole tree after every rotation.
Insert
First unbalanced node z
Taller child y
Case
Fix
J
H (BF −2)
I (BF −1)
RR
Left rotation at H
A
H (BF +2)
B (BF +1)
LL
Right rotation at H
E
I (BF +2)
B (BF −1)
LR
Left at B, then right at I
D
B (BF −2)
E (BF +1)
RL
Right at E, then left at B
G
H (BF +2)
C (BF −1)
LR
Left at C, then right at H
K
I (BF −2)
J (BF −1)
RR
Left rotation at I
H, I, B, C, F, L
No node reaches BF ±2: no rotation
Answer. 6 rotations (1 LL, 2 RR, 2 LR, 1 RL). Final pre-order: E C B A D H F G J I K L. Height 4. Check: the in-order A B C D E F G H I J K L is sorted and every BF is −1, 0 or +1.
P13Construction
Construct an AVL tree by inserting JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC one at a time into an empty tree. Name the rotation used at every step and draw the final tree. Months are inserted in calendar order and compared alphabetically (APR < AUG < DEC < FEB < JAN < JUL < JUN < MAR < MAY < NOV < OCT < SEP).
Show the solution
Method: insert each key as in a BST, walk back up updating the balance factors, and at the first node whose BF becomes ±2 read the case from the signs of BF(z) and BF(y). Redraw the whole tree after every rotation.
Insert
First unbalanced node z
Taller child y
Case
Fix
AUG
FEB (BF +2)
APR (BF −1)
LR
Left at APR, then right at FEB
OCT
MAY (BF −2)
SEP (BF +1)
RL
Right at SEP, then left at MAY
NOV
JAN (BF −2)
MAR (BF −1)
RR
Left rotation at JAN
JAN, FEB, MAR, APR, MAY, JUN, JUL, SEP, DEC
No node reaches BF ±2: no rotation
Answer. 3 rotations (1 RR, 1 LR, 1 RL). Final pre-order: MAR JAN AUG APR FEB DEC JUN JUL OCT MAY NOV SEP. Height 4. Check: the in-order APR AUG DEC FEB JAN JUL JUN MAR MAY NOV OCT SEP is sorted and every BF is −1, 0 or +1.
P14Construction + traversals
Construct an AVL tree by inserting 10, 20, 30, 25, 28, 27, 5. Then write its in-order, pre-order, post-order and level-order traversals.
Show the solution
Insert
First unbalanced node z
Taller child y
Case
Fix
30
10 (BF −2)
20 (BF −1)
RR
Left rotation at 10
28
30 (BF +2)
25 (BF −1)
LR
Left at 25, then right at 30
27
20 (BF −2)
28 (BF +1)
RL
Right at 28, then left at 20
5
20 (BF +2)
10 (BF +1)
LL
Right rotation at 20
10, 20, 25
No node reaches BF ±2: no rotation
In-order
5 10 20 25 27 28 30 (always sorted)
Pre-order
25 10 5 20 28 27 30
Post-order
5 20 10 27 30 28 25
Level-order
25 10 28 5 20 27 30
Answer. All four rotation types appear (RR, LR, RL, LL). The final tree is perfect, height 2.
P15BST vs AVL
Insert 5, 10, 15, 20, 25, 30, 35 into (a) a plain BST and (b) an AVL tree. Compare the heights and the number of comparisons needed to search for 35.
Show the solution
BST
AVL
Shape
Chain 5 → 10 → … → 35
20(10(5, 15), 30(25, 35))
Height
6
2
Comparisons to find 35
7 (5, 10, 15, 20, 25, 30, 35)
3 (20, 30, 35)
Rotations
0
4 RR (while inserting 15, 25, 30, 35)
Answer. Sorted input turns a BST into a list (O(n) search). The AVL tree spends 4 cheap rotations while building and then answers every search in at most 3 comparisons (O(log n)).
D. Deletion
P16Deletion, sibling BF 0
Build the AVL tree by inserting 44, 17, 62, 32, 50, 78, 48, 54, 88. Now delete 32. Show the rebalancing.
Show the solution
32 is a leaf: remove it. Now 17 is a leaf.
Walk up: BF(17) = 0, then BF(44) = h(17) − h(62) = 0 − 2 = −2.
The taller child is 62 with BF 0. BF 0 in the child can happen only after a deletion; it is treated like RR (L0 case): one left rotation at 44.
62 becomes the root, 44 becomes its left child and 50 (62's old left subtree) moves to 44's right.
The height of the whole tree stays 3, so no further checks are needed.
Answer. One left rotation at 44 (L0). Final tree 62(44(17, 50(48, 54)), 78( , 88)); pre-order 62 44 17 50 48 54 78 88.
P17Deleting the root
Build the AVL tree from 10, 20, 30, 40, 50, 60, 70 and then delete 40, 50, 60, 30 in that order. Name any rotation.
Show the solution
Delete
What happens
Rotation
Tree after
40 (root)
Two children: replace by the in-order successor 50, delete the old 50 leaf
none
50(20(10, 30), 60( , 70))
50 (root)
Successor 60 replaces it; 60 had only a right child 70, which moves up
none
60(20(10, 30), 70)
60 (root)
Successor 70 replaces it. BF(70) = +2 and its left child 20 has BF 0
R0: right rotation at 70
20(10, 70(30, ))
30
Leaf: remove
none
20(10, 70)
Answer. Only the deletion of 60 needs a rotation (R0, a single right rotation). Final tree 20(10, 70), height 1.
P18Deletion after construction
Using the AVL tree of P6 (keys 1 to 10), delete 4, 5, 6 in that order.
Show the solution
Delete
What happens
Rotation
Pre-order after
4 (root)
Successor 5 is copied into the root; the old 5 (a leaf) is removed
none
5 2 1 3 8 6 7 9 10
5 (root)
Successor 6 replaces it; 6's right child 7 moves up
none
6 2 1 3 8 7 9 10
6 (root)
Successor 7 replaces it; now BF(8) = −2, its right child 9 has BF −1
RR: left rotation at 8
7 2 1 3 9 8 10
Answer. One rotation in total, at 8: the key was removed from its left side and the sibling 9 has BF −1, so it is case L−1, a single left rotation (like RR). Final tree 7(2(1, 3), 9(8, 10)), height 2.
P19One deletion, two rotations
The tree below is the smallest possible AVL tree of height 4 (built by inserting 80, 50, 110, 30, 70, 100, 120, 20, 40, 60, 90, 10). Delete 120 and show why two rotations are needed.
Show the solution
120 is a leaf: remove it. Now BF(110) = h(100) − h(empty) = 1 − (−1) = +2, and its left child 100 has BF +1: LL, right rotation at 110. 100 becomes the root of that subtree, whose height drops from 2 to 1.
Because that subtree became shorter, the root is now unbalanced: BF(80) = 3 − 1 = +2, and its left child 50 has BF +1: LL, right rotation at 80.
50 becomes the new root.
Answer. Two right rotations (at 110, then at 80), both of type R1 (deleted from the right, sibling BF +1). Final pre-order 50 30 20 10 40 80 70 60 100 90 110, height 3. This is why deletion must check every ancestor, while insertion stops after the first rotation.
E. Numerical problems on height and nodes
Height is counted in edges (a single node has height 0). If your textbook counts levels (root = level 1), add 1 to every height below.
P20Minimum nodes
What is the minimum number of nodes in an AVL tree of height 5?
Show the solution
The sparsest AVL tree of height h has a root, one subtree of height h − 1 and one of height h − 2 (the most unequal pair allowed), each itself as sparse as possible:
N(h) = N(h − 1) + N(h − 2) + 1, with N(0) = 1 and N(1) = 2.
h
0
1
2
3
4
5
N(h)
1
2
1+2+1 = 4
2+4+1 = 7
4+7+1 = 12
7+12+1 = 20
Answer.20 nodes. (With the level convention, "height 5" means 4 edges and the answer is 12.)
P21Maximum height
What is the maximum possible height of an AVL tree with (a) 20 nodes, (b) 100 nodes, (c) 1000 nodes? What is the largest number of comparisons a search can need in case (c)?
Show the solution
The tallest AVL tree with n nodes has the largest h such that N(h) ≤ n. Extend the table: N = 1, 2, 4, 7, 12, 20, 33, 54, 88, 143, 232, 376, 609, 986, 1596 …
n
Largest N(h) ≤ n
Maximum height
20
N(5) = 20
5
100
N(8) = 88
8
1000
N(13) = 986
13
Answer. (a) 5, (b) 8, (c) 13. A search visits at most height + 1 nodes, so at most 14 comparisons for 1000 keys (a plain BST could need 1000). The bound h < 1.44 log2(n + 2) = 1.44 × log2 1002 ≈ 14.4 agrees: 13 < 14.4.
P22Minimum and maximum nodes
Find the minimum and maximum number of nodes in an AVL tree of height 4.
Show the solution
Minimum: N(4) = N(3) + N(2) + 1 = 7 + 4 + 1 = 12. Maximum: every level full, 2h+1 − 1 = 25 − 1 = 31 (a perfect tree is also an AVL tree).
Answer. Minimum 12, maximum 31.
P23Possible heights
An AVL tree has exactly 7 nodes. What are its smallest and largest possible heights? Draw one tree for each.
Show the solution
Smallest height: as full as possible, ⌈log2(7 + 1)⌉ − 1 = 2 (a perfect tree). Largest height: the largest h with N(h) ≤ 7 is h = 3, since N(3) = 7.
Answer. Smallest height 2, largest height 3.
P24Search cost for a million keys
A dictionary stores 1,000,000 keys. How many comparisons does a search need in the worst case if the keys are kept in (a) a plain BST, (b) an AVL tree, (c) a perfectly balanced tree?
Show the solution
Structure
Worst-case height
Worst-case comparisons
(a) Plain BST (sorted input)
999,999
1,000,000
(b) AVL tree: largest h with N(h) ≤ 106
27
28
(c) Perfectly balanced: ⌈log2(106 + 1)⌉ − 1
19
20
Answer. BST up to 1,000,000; AVL at most 28; perfect tree 20. An AVL tree is never more than about 44% taller than the best possible tree.
4.10Interview and placement questions
Questions asked in viva and interviews
Q1How do you identify which rotation to apply?
Find the lowest node z with BF ±2 on the path back up. Look at its taller child y. Same signs (+2/+1 or −2/−1): straight line, single rotation (LL → right, RR → left). Opposite signs (+2/−1 or −2/+1): zig-zag, double rotation (LR, RL).
Q2Why does an insertion need at most one rotation, but a deletion may need many?
After an insertion the fixed subtree returns to its old height, so no ancestor sees any change. After a deletion the rotated subtree can end up one level shorter, which can unbalance the next ancestor, and so on up to the root.
Q3What is the maximum height of an AVL tree with n nodes?
The largest h with N(h) ≤ n, where N(h) = N(h − 1) + N(h − 2) + 1 is the fewest nodes of height h. This gives h < 1.44 log2(n + 2): at most 44% taller than a perfect tree (C2 computes it).
Q4AVL or red-black tree?
AVL is more strictly balanced, so searches are slightly faster; red-black trees allow more imbalance and need fewer rotations (at most 2 per insert, 3 per delete), so updates are cheaper. That is why general-purpose libraries (C++ std::map, Java TreeMap) use red-black trees.
Q5What does a rotation cost, and what does it not change?
It changes three pointers and two heights: O(1). It does not change the in-order sequence, so the tree is still a BST.
Q6Why store the height in each node?
So the balance factor of a node is computed in O(1) from its two children. Recomputing heights from scratch would cost O(n) at every node and destroy the O(log n) bound.
Q7Is an AVL tree always a complete tree?
No. It is height-balanced, not complete: the minimum AVL tree of height 4 has only 12 of the 31 possible nodes. Every complete BST, however, is an AVL tree.
Coding problems (tested C programs)
C1Is this binary tree an AVL tree?MediumTwo checks in O(n)
Decide whether a binary tree is an AVL tree.
Idea. It must pass two tests: the BST range test, and |BF| ≤ 1 everywhere. Compute heights bottom-up and return a special value (−2) as soon as any node is unbalanced, so the balance test is one O(n) pass, not O(n²).
Time O(n) · Space O(h)
Show the C program and its output
/* Q1. Is a binary tree an AVL tree? It must be a BST AND every node must have |BF| <= 1 */
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *build(int a[], int n, int i) { /* level-order array, -1 = empty */
struct node *r;
if (i >= n || a[i] == -1) return NULL;
r = newNode(a[i]);
r->left = build(a, n, 2 * i + 1);
r->right = build(a, n, 2 * i + 2);
return r;
}
int isBST(struct node *r, long lo, long hi) {
if (r == NULL) return 1;
if (r->data <= lo || r->data >= hi) return 0;
return isBST(r->left, lo, r->data) && isBST(r->right, r->data, hi);
}
/* height in edges, or -2 as soon as some node has |BF| > 1: one pass, O(n) */
int balancedHeight(struct node *r) {
int l, rt;
if (r == NULL) return -1;
l = balancedHeight(r->left); if (l == -2) return -2;
rt = balancedHeight(r->right); if (rt == -2) return -2;
if (l - rt > 1 || rt - l > 1) return -2;
return 1 + (l > rt ? l : rt);
}
int isAVL(struct node *r) { return isBST(r, LONG_MIN, LONG_MAX) && balancedHeight(r) != -2; }
int main(void) {
int t1[] = {40, 20, 60, 10, 30, -1, 70}; /* AVL */
int t2[] = {40, 20, 60, 10, -1, -1, -1, 5}; /* BST, but BF(20) = +2 */
int t3[] = {30, 20, 50, 10, 40}; /* balanced, but 40 is left of 30 */
printf("t1: %s\n", isAVL(build(t1, 7, 0)) ? "AVL" : "not AVL");
printf("t2: %s\n", isAVL(build(t2, 8, 0)) ? "AVL" : "not AVL");
printf("t3: %s\n", isAVL(build(t3, 5, 0)) ? "AVL" : "not AVL");
return 0;
}
Output
t1: AVL
t2: not AVL
t3: not AVL
C2Minimum nodes and maximum heightEasyRecurrence (Fibonacci-like)
Print N(h), the fewest nodes of an AVL tree of height h, and the tallest possible AVL tree for a given n.
Idea. N(0) = 1, N(1) = 2, N(h) = N(h − 1) + N(h − 2) + 1. The maximum height for n nodes is the largest h with N(h) ≤ n.
Time O(h) per value · Space O(1)
Show the C program and its output
/* Q2. Minimum nodes N(h) of an AVL tree of height h, and the maximum height for n nodes */
#include <stdio.h>
long minNodes(int h) { /* N(0) = 1, N(1) = 2, N(h) = N(h-1) + N(h-2) + 1 */
long a = 1, b = 2, c;
int i;
if (h == 0) return 1;
for (i = 2; i <= h; i++) { c = a + b + 1; a = b; b = c; }
return b;
}
int maxHeight(long n) { /* largest h with N(h) <= n */
int h = 0;
while (minNodes(h + 1) <= n) h++;
return h;
}
int main(void) {
int h;
long n[] = {7, 20, 100, 1000, 1000000};
printf("h : "); for (h = 0; h <= 8; h++) printf("%4d", h);
printf("\nN(h) : "); for (h = 0; h <= 8; h++) printf("%4ld", minNodes(h));
printf("\n");
for (h = 0; h < 5; h++) printf("n = %ld -> max height %d\n", n[h], maxHeight(n[h]));
return 0;
}
Output
h : 0 1 2 3 4 5 6 7 8
N(h) : 1 2 4 7 12 20 33 54 88
n = 7 -> max height 3
n = 20 -> max height 5
n = 100 -> max height 8
n = 1000 -> max height 13
n = 1000000 -> max height 27
C3Rebalance any BST in O(n)MediumIn-order + divide and conquer
Given a BST that may be skewed, rebuild it into a height-balanced BST without creating new nodes.
Idea. Collect the nodes in in-order (sorted) into an array, then rebuild by making the middle node the root of each range. This is how some balanced trees rebuild a subtree all at once.
Time O(n) · Space O(n)
Show the C program and its output
/* Q3. Turn any BST (even a skewed one) into a height-balanced BST in O(n) */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *insert(struct node *r, int x) {
if (r == NULL) {
r = malloc(sizeof *r); r->data = x; r->left = r->right = NULL;
return r;
}
if (x < r->data) r->left = insert(r->left, x);
else if (x > r->data) r->right = insert(r->right, x);
return r;
}
int height(struct node *r) {
int l, rt;
if (r == NULL) return -1;
l = height(r->left); rt = height(r->right);
return 1 + (l > rt ? l : rt);
}
struct node *nodes[100];
int cnt = 0;
void collect(struct node *r) { /* in-order: nodes come out sorted */
if (r == NULL) return;
collect(r->left); nodes[cnt++] = r; collect(r->right);
}
struct node *rebuild(int lo, int hi) { /* middle node becomes the root */
int mid;
if (lo > hi) return NULL;
mid = (lo + hi) / 2;
nodes[mid]->left = rebuild(lo, mid - 1);
nodes[mid]->right = rebuild(mid + 1, hi);
return nodes[mid];
}
void preorder(struct node *r) { if (r) { printf("%d ", r->data); preorder(r->left); preorder(r->right); } }
int main(void) {
struct node *root = NULL;
int i;
for (i = 1; i <= 7; i++) root = insert(root, i * 10); /* sorted input: a chain */
printf("Before: height %d\n", height(root));
collect(root);
root = rebuild(0, cnt - 1); /* reuses the same nodes */
printf("After: height %d, pre-order ", height(root));
preorder(root);
printf("\n");
return 0;
}
Use the lab to try your own keys, the tracer to watch Program 5 execute line by line, and then read the complete program. It was compiled with gcc and tested on all the examples in this part.
Try it AVL lab (balance factors shown)
Try inserting 5 (LL), then 35 and 32 (watch for LR/RL), then delete keys from the shorter side.
Code tracer: run Program 5 line by line
Choose any function, type your own keys and press Next. The highlighted line is the statement being executed, the call stack shows every call that is still waiting to finish, and the variables, output and trace table change with each step. Use Next key ⏭ to jump to the end of the current key. The output box shows what the program prints: the rotation case and the node where it happened.
Trace Program 5, inside the computer
Program 5 · AVL tree: insert and delete with all four rotations
/* AVL Tree: insert and delete with LL, RR, LR, RL rotations; prints balance factors */
#include <stdio.h>
#include <stdlib.h>
struct node {
int data, height; /* height in edges: leaf = 0, NULL = -1 */
struct node *left, *right;
};
int h(struct node *n) { return n ? n->height : -1; }
int max(int a, int b) { return a > b ? a : b; }
void update(struct node *n) { n->height = 1 + max(h(n->left), h(n->right)); }
int BF(struct node *n) { return n ? h(n->left) - h(n->right) : 0; }
struct node *createNode(int x) {
struct node *n = (struct node *)malloc(sizeof(struct node));
n->data = x; n->height = 0;
n->left = n->right = NULL;
return n;
}
struct node *rightRotate(struct node *y) { /* fixes LL */
struct node *x = y->left, *T2 = x->right;
x->right = y;
y->left = T2;
update(y); update(x); /* y first: it is now below x */
return x;
}
struct node *leftRotate(struct node *x) { /* fixes RR */
struct node *y = x->right, *T2 = y->left;
y->left = x;
x->right = T2;
update(x); update(y);
return y;
}
/* called on every node on the way back up */
struct node *balance(struct node *n) {
update(n);
if (BF(n) > 1) {
if (BF(n->left) < 0) { /* LR */
printf(" LR at %d\n", n->data);
n->left = leftRotate(n->left);
} else printf(" LL at %d\n", n->data);
return rightRotate(n);
}
if (BF(n) < -1) {
if (BF(n->right) > 0) { /* RL */
printf(" RL at %d\n", n->data);
n->right = rightRotate(n->right);
} else printf(" RR at %d\n", n->data);
return leftRotate(n);
}
return n;
}
struct node *insert(struct node *n, int x) {
if (n == NULL) return createNode(x);
if (x < n->data) n->left = insert(n->left, x);
else if (x > n->data) n->right = insert(n->right, x);
else return n; /* duplicate */
return balance(n);
}
struct node *deleteNode(struct node *n, int x) {
struct node *t;
if (n == NULL) { printf("%d not found\n", x); return NULL; }
if (x < n->data) n->left = deleteNode(n->left, x);
else if (x > n->data) n->right = deleteNode(n->right, x);
else {
if (n->left == NULL || n->right == NULL) {
t = n->left ? n->left : n->right;
free(n);
return t; /* NULL for a leaf */
}
t = n->right; /* in-order successor */
while (t->left) t = t->left;
n->data = t->data;
n->right = deleteNode(n->right, t->data);
}
return balance(n);
}
void preorder(struct node *r) { /* key(BF) */
if (r) { printf("%d(%d) ", r->data, BF(r)); preorder(r->left); preorder(r->right); }
}
void inorder(struct node *r) { if (r) { inorder(r->left); printf("%d ", r->data); inorder(r->right); } }
int main(void) {
struct node *root = NULL;
int ch, x, n, i;
while (1) {
printf("\n1.Insert many 2.Delete 3.Show 4.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &n); printf("Keys: ");
for (i = 0; i < n; i++) { scanf("%d", &x); root = insert(root, x); } break;
case 2: printf("Key: "); scanf("%d", &x); root = deleteNode(root, x); break;
case 3: printf("Pre-order key(BF): "); preorder(root);
printf("\nIn-order: "); inorder(root);
printf("\nHeight of tree: %d\n", h(root)); break;
case 4: return 0;
}
}
return 0;
}
Sample run (insert 10 20 30 40 50 25, then delete 40)
RR at 10
RR at 30
RL at 20
Pre-order key(BF): 30(0) 20(0) 10(0) 25(0) 40(-1) 50(0)
In-order: 10 20 25 30 40 50
Height of tree: 2
-- delete 40 --
Pre-order key(BF): 30(1) 20(0) 10(0) 25(0) 50(0)
Common mistakes in this part
Using BF = h(right) − h(left) in one place and h(left) − h(right) in another. These notes use left − right.
Rotating at the root instead of the lowest unbalanced node.
Fixing a zig-zag (LR or RL) with a single rotation.
In C, updating the height of the new top node before the node that moved down. Update the lower node first.
In deletion, stopping after the first rotation. Keep checking every ancestor up to the root.
Key points to remember
AVL = BST + balance rule: BF = h(left) − h(right) must be −1, 0 or +1 at every node.
After an insert or delete, walk up and find the lowest node with BF ±2.
LL → right rotation (clockwise) · RR → left rotation (anticlockwise) · LR → left (anticlockwise) then right (clockwise) · RL → right (clockwise) then left (anticlockwise).
Same signs (+2/+1, −2/−1) → single rotation; opposite signs → double rotation.
Insert needs at most one (single or double) rotation; delete may need one at every level (R0 / R1 / R−1 cases).
Height < 1.44 log2(n + 2), so search, insert and delete are O(log n).
Part 9 · Beyond the syllabus
Binary Heap
A complete binary tree kept in an array, with the largest (or smallest) key always at the root. It gives the most urgent item in O(1) and updates in O(log n), which is why every priority queue is built on it.
Beyond the syllabus: asked very often in placement tests (priority queue, top-k, heap sort, running median).
9.1At a glance
A max-heap is a complete binary tree in which every parent is ≥ its children, so the largest key sits at the root. A min-heap is the mirror: every parent ≤ its children.
Problem it solves
Items keep arriving and you keep taking out the most urgent one (highest priority, earliest time). An unsorted array makes "take the max" O(n); a sorted array makes "insert" O(n). A heap does both in O(log n).
The rule
(1) Shape: complete tree, filled level by level from the left. (2) Order: parent ≥ both children (max-heap). Siblings have no order between them.
Think of it as
A hospital emergency queue: the most serious patient is seen next, not the one who came first.
Use it when
Priority queues, scheduling by deadline, top-k items, Dijkstra and Prim, merging k sorted lists, heap sort.
Not the right tool when
You must search for an arbitrary key (O(n)) or list all keys in order. Use a BST or AVL tree.
Find maxO(1)InsertO(log n)Delete maxO(log n)Build from n keysO(n)Search any keyO(n)
Used inC++ priority_queueJava PriorityQueuePython heapqDijkstra, PrimHeap sortTimer queues
A binary heap is a complete binary tree that satisfies the heap order property: in a max-heap every node is ≥ its children (so the maximum is at the root); in a min-heap every node is ≤ its children (the minimum is at the root).
Because the tree is complete, it is stored in an array with no gaps and no pointers. Reading the tree level by level, left to right, gives the array.
For the node at index i
0-based array (C)
1-based array (many textbooks)
Left child
2i + 1
2i
Right child
2i + 2
2i + 1
Parent
⌊(i − 1) / 2⌋
⌊i / 2⌋
Leaves
⌊n/2⌋ … n − 1
⌊n/2⌋ + 1 … n
Height
⌊log2 n⌋ (always balanced, because the tree is complete)
How to read the heap pictures
The tree and the array strip under it are the same data. The small number above each cell is its index.
Amber = the key being moved, violet = the parent or child it is compared with, green = settled.
In heap sort, green cells at the right end of the array are the sorted part; they are no longer drawn in the tree.
#define MAX 100
int heap[MAX], n = 0; /* heap[0..n-1]; children of i: 2i+1, 2i+2 */
Common mistake: A heap is not sorted. [90, 70, 80, 40, 50, 60] is a max-heap, but 70 comes before 80. Only the root is certain. (The reverse holds: an array sorted in ascending order is always a min-heap.)
9.3Insert (heapify up)
1Concept
Put the new key at the next free position (end of the array). The shape stays complete.
Compare it with its parent. If it is larger (max-heap), swap them.
Repeat until the parent is larger or the key reaches the root. This is called heapify up (sift up).
The key climbs at most the height of the tree, so insert is O(log n).
2Algorithm
Algorithm HEAP-INSERT(a, n, key) (max-heap)
1. If n = MAX: report overflow and stop.
2. a[n] = key; i = n; n = n + 1
3. While i > 0 and a[(i − 1)/2] < a[i]:
swap a[i] and a[(i − 1)/2]
i = (i − 1)/2
3Worked example
Step by step Insert into a heap
4C code
void heapifyUp(int a[], int i) {
while (i > 0 && a[(i - 1) / 2] < a[i]) { /* parent smaller: swap up */
swap(&a[i], &a[(i - 1) / 2]);
i = (i - 1) / 2;
}
}
void insert(int x) {
if (n == MAX) { printf("Heap is full\n"); return; }
heap[n] = x; /* next free position */
heapifyUp(heap, n);
n++;
}
9.4Find max and search for a key
1Concept
Find max (peek): the maximum is always a[0]. O(1).
Search for any key x: the heap order does not say left or right, so both subtrees may need checking. One saving: in a max-heap, if a node is smaller than x, nothing below it can be x, so that subtree is skipped. Worst case O(n).
The minimum of a max-heap is one of the leaves (indexes ⌊n/2⌋ to n − 1), so finding it is O(n) as well.
2Algorithm
Algorithm HEAP-SEARCH(a, n, i, x) (call with i = 0)
1. If i ≥ n or a[i] < x: return NOT FOUND (nothing below can be x)
2. If a[i] = x: return i
3. r = HEAP-SEARCH(a, n, 2i + 1, x)
4. If r ≠ NOT FOUND: return r
5. Return HEAP-SEARCH(a, n, 2i + 2, x)
3Worked example
Step by step Search a heap
4C code
int findMax(void) { return heap[0]; } /* O(1) */
int search(int i, int x) { /* returns the index or -1 */
int r;
if (i >= n || heap[i] < x) return -1; /* skip this subtree */
if (heap[i] == x) return i;
r = search(2 * i + 1, x);
return r != -1 ? r : search(2 * i + 2, x);
}
9.5Delete max (heapify down)
1Concept
The key removed is always the root (the maximum). Save it.
Move the last element of the array into the root and reduce n by one. The shape stays complete.
Compare the root with its children. If the larger child is bigger, swap with it.
Repeat until no child is larger or a leaf is reached. This is heapify down (sift down).
Always swap with the larger child. Swapping with the smaller one would put a smaller key above a larger one and break the heap.
2Algorithm
Algorithm DELETE-MAX(a, n)
1. If n = 0: report underflow and stop.
2. max = a[0]; a[0] = a[n − 1]; n = n − 1; i = 0
3. Loop:
l = 2i + 1, r = 2i + 2, big = i
if l < n and a[l] > a[big]: big = l
if r < n and a[r] > a[big]: big = r
if big = i: stop
swap a[i] and a[big]; i = big
4. Return max
3Worked example
Step by step Delete the maximum
4C code
void heapifyDown(int a[], int size, int i) {
int l, r, big;
while (1) {
l = 2 * i + 1; r = 2 * i + 2; big = i;
if (l < size && a[l] > a[big]) big = l;
if (r < size && a[r] > a[big]) big = r;
if (big == i) return; /* heap order holds */
swap(&a[i], &a[big]);
i = big;
}
}
int deleteMax(void) {
int top = heap[0];
heap[0] = heap[n - 1]; /* last element to the root */
n--;
heapifyDown(heap, n, 0);
return top;
}
9.6Build a heap from an array in O(n)
1Concept
Top-down: n inserts
Insert the keys one by one with heapify up. Each insert can climb log n levels: O(n log n).
Bottom-up (Floyd's method)
Treat the array as a complete tree. Leaves are already heaps. Heapify down every internal node, from the last one (index ⌊n/2⌋ − 1) back to the root: O(n).
Why bottom-up is O(n): half the nodes are leaves (no work), a quarter sit one level up (at most 1 swap), an eighth at most 2 swaps, and so on. The sum n/4·1 + n/8·2 + n/16·3 + … stays below n.
The two methods can give different heaps. From 10, 20, 30, 40, 50, 60, 70: inserting one by one gives [70, 40, 60, 10, 30, 20, 50]; bottom-up gives [70, 50, 60, 40, 20, 10, 30]. Both are valid max-heaps, so use the method the question asks for.
2Algorithm
Algorithm BUILD-MAX-HEAP(a, n)
1. For i = ⌊n/2⌋ − 1 down to 0:
HEAPIFY-DOWN(a, n, i)
3Worked example
Step by step Bottom-up heap construction
4C code
void buildHeap(int a[], int size) {
int i;
for (i = size / 2 - 1; i >= 0; i--) /* last internal node up to the root */
heapifyDown(a, size, i);
}
9.7Heap sort
1Concept
Build a max-heap from the array (bottom-up, O(n)).
The root is the largest key. Swap it with the last element of the heap: it is now in its final place.
Shrink the heap by one and heapify down from the root.
Repeat steps 2 and 3 until one element is left. The array is now in ascending order.
Property
Heap sort
Time (best, average, worst)
O(n log n) in all cases
Extra space
O(1): sorts inside the same array
Stable?
No: equal keys can change their order
Compared with quick sort
No O(n²) worst case, but usually slower in practice (poor cache use)
2Algorithm
Algorithm HEAP-SORT(a, n)
1. BUILD-MAX-HEAP(a, n)
2. For end = n − 1 down to 1:
swap a[0] and a[end] (largest goes to the end)
HEAPIFY-DOWN(a, end, 0) (the heap is now a[0 .. end − 1])
3Worked example
Step by step Heap sort
4C code
void heapSort(int a[], int size) {
int end;
buildHeap(a, size);
for (end = size - 1; end > 0; end--) {
swap(&a[0], &a[end]); /* largest goes to the end */
heapifyDown(a, end, 0); /* restore the heap on the rest */
}
}
9.8Complexity and comparison
Structure
Insert
Find max
Delete max
Search any key
Unsorted array
O(1)
O(n)
O(n)
O(n)
Sorted array
O(n)
O(1)
O(1)
O(log n)
Balanced BST (AVL, red-black)
O(log n)
O(log n)
O(log n)
O(log n)
Binary heap
O(log n)
O(1)
O(log n)
O(n)
A balanced BST can do everything a heap does, but the heap is simpler, needs no pointers, is built in O(n) and keeps its data in one array, so it is faster in practice when only the max (or min) is needed.
9.9Real-life applications
Priority queues in every language
C++ std::priority_queue, Java PriorityQueue and Python heapq are binary heaps stored in an array.
Why a heap: O(1) peek, O(log n) push and pop, no pointers.
Shortest paths and routes
Dijkstra's algorithm repeatedly takes the unvisited place with the smallest distance; Prim's algorithm takes the cheapest edge. Both keep the candidates in a min-heap.
Why a heap: "give me the smallest" thousands of times.
Timers and event queues
Simulators and network libraries keep future events ordered by time and always run the earliest one next; libevent, for example, keeps its timers in a min-heap.
Why a heap: the next event is always at the root.
Top-k and trending lists
"Top 10 products out of millions": keep a min-heap of size 10 and replace its root whenever a bigger item arrives.
Why a heap: O(n log k) time, O(k) memory.
Merging sorted files
External sorting and database merge steps combine k sorted runs by keeping the front item of each run in a min-heap.
Why a heap: the smallest of k fronts in O(log k).
Huffman compression
Building a Huffman tree means "take the two smallest weights" again and again, which a min-heap does (see Huffman tree).
Why a heap: n − 1 merges in O(n log n).
9.10Interview and placement questions
Questions asked in viva and interviews
Q1Why is bottom-up build-heap O(n) and not O(n log n)?
Heapify down costs the height of the node, not the height of the tree. About n/2 nodes are leaves (cost 0), n/4 have height 1, n/8 height 2, and so on. The total Σ h · n / 2h+1 is below n, so building is O(n).
Q2Where is the smallest key in a max-heap, and how fast can you find it?
It is one of the leaves, which are indexes ⌊n/2⌋ to n − 1 (0-based). All of them must be checked: O(n).
Q3Why is a heap stored in an array while a BST usually uses pointers?
A heap is always a complete tree, so the array has no gaps and the parent and children are found by arithmetic. A BST can have any shape; a skewed BST in an array would waste up to 2n cells.
Q4Is heap sort stable? Is it in place?
In place: yes, O(1) extra space. Stable: no. Swapping the root with the last element can jump one equal key over another: sorting (5a, 5b, 3) gives 3, 5b, 5a.
Q5What is the height of a heap with n keys?
⌊log2 n⌋, because the tree is complete. So a heap of one million keys has height 19, and insert or delete does at most 19 swaps.
Q6How do you change the priority of a key already in the heap?
Change the value, then heapify up if it became larger (max-heap) or down if it became smaller: O(log n). To find the key quickly, keep a separate array from key to index (this is the decrease-key step of Dijkstra's algorithm).
Q7Find the k-th largest element of an unsorted array. Which heap do you use?
A min-heap of size k: push elements; when the size goes above k, pop the minimum. At the end the root is the k-th largest. O(n log k) time, O(k) space. (Building a max-heap and deleting k times is O(n + k log n).)
Q8Heap or BST for a priority queue?
A heap: O(1) peek, O(log n) insert and delete, O(n) build, one compact array. Choose a balanced BST only if you also need search, deletion of arbitrary keys, or sorted listing.
Q9Is the array [100, 19, 36, 17, 3, 25, 1, 2, 7] a max-heap?
Yes. Check each internal node i (0 to 3) against 2i + 1 and 2i + 2: 100 ≥ 19, 36; 19 ≥ 17, 3; 36 ≥ 25, 1; 17 ≥ 2, 7.
Coding problems (tested C programs)
C1k largest elements of an arrayMediumMin-heap of size k
Print the k largest values of a large array without sorting all of it.
Idea. Keep a min-heap of the best k seen so far. Its root is the smallest of them; a new value enters only if it beats the root (replace the root and heapify down).
Time O(n log k) · Space O(k)
Show the C program and its output
/* Q1. The k largest elements of a large array: keep a MIN-heap of size k */
#include <stdio.h>
int h[100], n = 0; /* min-heap: smallest of the kept k is at h[0] */
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
void push(int x) {
int i = n++;
h[i] = x;
while (i > 0 && h[(i - 1) / 2] > h[i]) { swap(&h[i], &h[(i - 1) / 2]); i = (i - 1) / 2; }
}
void replaceTop(int x) { /* pop the minimum and push x in one step */
int i = 0, c;
h[0] = x;
while ((c = 2 * i + 1) < n) {
if (c + 1 < n && h[c + 1] < h[c]) c++;
if (h[i] <= h[c]) break;
swap(&h[i], &h[c]); i = c;
}
}
int main(void) {
int a[] = {12, 5, 87, 34, 66, 1, 90, 43, 21, 78, 9, 55}, len = 12, k = 4, i;
for (i = 0; i < len; i++) {
if (n < k) push(a[i]);
else if (a[i] > h[0]) replaceTop(a[i]); /* bigger than the smallest kept one */
}
printf("%d largest: ", k);
for (i = 0; i < n; i++) printf("%d ", h[i]);
printf("(the %d-th largest is %d, at the root)\n", k, h[0]);
return 0;
}
Output
4 largest: 66 78 87 90 (the 4-th largest is 66, at the root)
C2Is this array a max-heap?EasyIndex arithmetic
Check the heap order of an array stored as a complete tree.
Idea. Only internal nodes have children: for i = 0 to ⌊n/2⌋ − 1 check a[i] ≥ a[2i + 1] and, if it exists, a[i] ≥ a[2i + 2].
Time O(n) · Space O(1)
Show the C program and its output
/* Q2. Is an array a max-heap? Every parent must be >= its children */
#include <stdio.h>
int isMaxHeap(int a[], int n) {
int i;
for (i = 0; i <= (n - 2) / 2; i++) { /* only internal nodes have children */
if (2 * i + 1 < n && a[i] < a[2 * i + 1]) return 0;
if (2 * i + 2 < n && a[i] < a[2 * i + 2]) return 0;
}
return 1;
}
int main(void) {
int a[] = {90, 70, 80, 40, 50, 60}, b[] = {90, 70, 80, 75, 50, 60};
printf("[90 70 80 40 50 60]: %s\n", isMaxHeap(a, 6) ? "max-heap" : "not a heap");
printf("[90 70 80 75 50 60]: %s\n", isMaxHeap(b, 6) ? "max-heap" : "not a heap (75 > its parent 70)");
return 0;
}
Output
[90 70 80 40 50 60]: max-heap
[90 70 80 75 50 60]: not a heap (75 > its parent 70)
C3Running median of a streamHardTwo heaps
After each new number, print the median of all numbers so far.
Idea. A max-heap holds the smaller half and a min-heap the larger half. Keep their sizes equal, or the max-heap one bigger. The median is the max-heap's root, or the average of both roots.
Time O(log n) per number · Space O(n)
Show the C program and its output
/* Q3. Running median of a stream: a max-heap for the lower half, a min-heap for the upper half */
#include <stdio.h>
int lo[100], nlo = 0; /* max-heap: the smaller half */
int hi[100], nhi = 0; /* min-heap: the larger half */
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
/* one push/pop pair serves both heaps: sign = +1 for max-heap, -1 for min-heap */
void push(int h[], int *n, int x, int sign) {
int i = (*n)++;
h[i] = x;
while (i > 0 && sign * h[(i - 1) / 2] < sign * h[i]) { swap(&h[i], &h[(i - 1) / 2]); i = (i - 1) / 2; }
}
int pop(int h[], int *n, int sign) {
int top = h[0], i = 0, c;
h[0] = h[--(*n)];
while ((c = 2 * i + 1) < *n) {
if (c + 1 < *n && sign * h[c + 1] > sign * h[c]) c++;
if (sign * h[i] >= sign * h[c]) break;
swap(&h[i], &h[c]); i = c;
}
return top;
}
void add(int x) {
if (nlo == 0 || x <= lo[0]) push(lo, &nlo, x, 1); else push(hi, &nhi, x, -1);
if (nlo > nhi + 1) push(hi, &nhi, pop(lo, &nlo, 1), -1); /* keep the sizes */
else if (nhi > nlo) push(lo, &nlo, pop(hi, &nhi, -1), 1); /* within one */
}
double median(void) { return nlo > nhi ? lo[0] : (lo[0] + hi[0]) / 2.0; }
int main(void) {
int s[] = {5, 15, 1, 3, 8, 7, 9, 10}, i;
for (i = 0; i < 8; i++) { add(s[i]); printf("after %2d: median %.1f\n", s[i], median()); }
return 0;
}
Output
after 5: median 5.0
after 15: median 10.0
after 1: median 5.0
after 3: median 4.0
after 8: median 5.0
after 7: median 6.0
after 9: median 7.0
after 10: median 7.5
9.11Lab, algorithm tracer and complete program
Insert and delete your own keys in the lab, follow the algorithms line by line in the tracer, and then read the complete program.
Try it Heap lab (tree and array together)
Algorithm tracer: heap insert and delete max
Trace Max-heap algorithms, step by step
Program 7 · Max-heap: insert, delete max, build heap in O(n), heap sort
/* Binary max-heap in an array: insert, delete max, build heap (bottom-up), heap sort */
#include <stdio.h>
#define MAX 100
int heap[MAX], n = 0; /* heap[0..n-1]; children of i: 2i+1, 2i+2 */
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
/* move a[i] up while it is larger than its parent */
void heapifyUp(int a[], int i) {
while (i > 0 && a[(i - 1) / 2] < a[i]) {
swap(&a[i], &a[(i - 1) / 2]);
i = (i - 1) / 2;
}
}
/* move a[i] down while a child is larger (only the first size elements count) */
void heapifyDown(int a[], int size, int i) {
int l, r, big;
while (1) {
l = 2 * i + 1; r = 2 * i + 2; big = i;
if (l < size && a[l] > a[big]) big = l;
if (r < size && a[r] > a[big]) big = r;
if (big == i) return;
swap(&a[i], &a[big]);
i = big;
}
}
void insert(int x) {
if (n == MAX) { printf("Heap is full\n"); return; }
heap[n] = x; /* next free position keeps the tree complete */
heapifyUp(heap, n);
n++;
}
int deleteMax(void) {
int top = heap[0];
heap[0] = heap[n - 1]; /* last element moves to the root */
n--;
heapifyDown(heap, n, 0);
return top;
}
/* Floyd's method: heapify every internal node from the last one up to the root: O(n) */
void buildHeap(int a[], int size) {
int i;
for (i = size / 2 - 1; i >= 0; i--) heapifyDown(a, size, i);
}
void heapSort(int a[], int size) {
int end;
buildHeap(a, size);
for (end = size - 1; end > 0; end--) {
swap(&a[0], &a[end]); /* largest goes to the end */
heapifyDown(a, end, 0); /* restore the heap on the rest */
}
}
void show(int a[], int size) { int i; for (i = 0; i < size; i++) printf("%d ", a[i]); printf("\n"); }
int main(void) {
int ch, x, k, i, a[MAX];
while (1) {
printf("\n1.Insert many 2.Delete max 3.Display 4.Build heap from array 5.Heap sort 6.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &k); printf("Keys: ");
for (i = 0; i < k; i++) { scanf("%d", &x); insert(x); } break;
case 2: if (n == 0) printf("Heap is empty\n"); else printf("Deleted %d\n", deleteMax()); break;
case 3: printf("Heap array: "); show(heap, n); break;
case 4: case 5:
printf("How many? "); scanf("%d", &k); printf("Array: ");
for (i = 0; i < k; i++) scanf("%d", &a[i]);
if (ch == 4) { buildHeap(a, k); printf("As a max-heap: "); }
else { heapSort(a, k); printf("Sorted: "); }
show(a, k); break;
case 6: return 0;
}
}
return 0;
}
Sample run
1.Insert many 2.Delete max 3.Display 4.Build heap from array 5.Heap sort 6.Exit
Choice: 1
How many? 6
Keys: 40 20 60 10 50 30
1.Insert many 2.Delete max 3.Display 4.Build heap from array 5.Heap sort 6.Exit
Choice: 3
Heap array: 60 50 40 10 20 30
1.Insert many 2.Delete max 3.Display 4.Build heap from array 5.Heap sort 6.Exit
Choice: 2
Deleted 60
1.Insert many 2.Delete max 3.Display 4.Build heap from array 5.Heap sort 6.Exit
Choice: 3
Heap array: 50 30 40 10 20
1.Insert many 2.Delete max 3.Display 4.Build heap from array 5.Heap sort 6.Exit
Choice: 4
How many? 7
Array: 10 20 30 40 50 60 70
As a max-heap: 70 50 60 40 20 10 30
1.Insert many 2.Delete max 3.Display 4.Build heap from array 5.Heap sort 6.Exit
Choice: 5
How many? 8
Array: 35 12 99 47 5 61 23 80
Sorted: 5 12 23 35 47 61 80 99
1.Insert many 2.Delete max 3.Display 4.Build heap from array 5.Heap sort 6.Exit
Choice: 6
Common mistakes in this part
Thinking a heap is sorted. Only the root is certain.
Swapping with the smaller child during heapify down.
Inserting the new key at the root instead of at the end of the array.
Running bottom-up build from the root downwards. It must go from index ⌊n/2⌋ − 1 back to 0.
Mixing 0-based (2i + 1) and 1-based (2i) formulas in one answer.
Key points to remember
Heap = complete tree + heap order. Array storage: children 2i + 1 and 2i + 2, parent ⌊(i − 1)/2⌋.
Insert: add at the end, heapify up. Delete max: last element to the root, heapify down with the larger child. Both O(log n).
Bottom-up build is O(n); n inserts is O(n log n).
Heap sort: O(n log n) always, in place, not stable.
Priority queues, Dijkstra, Prim, top-k and Huffman all use heaps.
Part 8 · Beyond the syllabus
Threaded Binary Tree
A binary tree that reuses its empty (NULL) links as pointers to the in-order predecessor and successor. With these threads, in-order traversal needs no stack and no recursion.
Beyond the syllabus: appears in many university syllabi and in viva questions on traversal; Morris traversal is its interview form.
8.1At a glance
In a threaded binary tree, a NULL left link points to the node's in-order predecessor and a NULL right link points to its in-order successor. A flag in each node tells a real child from a thread.
Problem it solves
A linked binary tree with n nodes has n + 1 NULL links, more than half of all links. And in-order traversal needs a stack (or recursion) of size O(h). Threads use the wasted links so the walk needs O(1) extra memory.
The rule
Empty left link → in-order predecessor. Empty right link → in-order successor. lthread / rthread = 1 means "this link is a thread".
Think of it as
Every page of a book printing "next page is …", so you never go back to the contents page to continue reading.
Use it when
In-order walks and successor / predecessor queries are frequent, stack memory is tight, and the tree changes rarely.
Not the right tool when
The tree is updated all the time (every insert and delete must repair threads) or it must stay balanced (use an AVL or red-black tree).
In-order walkO(n) time, O(1) spaceSuccessorO(1) via a thread, else O(h)SearchO(h)InsertO(h)DeleteO(h)
Used inStack-free traversalMorris traversalMemory-limited systemsSuccessor queries
Comes fromBST + threads in the empty linksSame ideaB+ tree leaf links: neighbours linked for an in-order walk
8.2Definition and types
Definition
A threaded binary tree is a binary tree in which every NULL left pointer is replaced by a pointer to the node's in-order predecessor, and every NULL right pointer by a pointer to its in-order successor. These extra pointers are called threads. Two flag bits in each node tell whether a pointer is a real child link or a thread.
Why: a linked binary tree with n nodes has 2n links but only n − 1 edges, so n + 1 links are NULL (2.4). Threads put them to use.
Type
Threads kept
Single (one-way), right-threaded
Only NULL right links become threads to the in-order successor
Single (one-way), left-threaded
Only NULL left links become threads to the in-order predecessor
Double (two-way), in-threaded
Both: left threads to the predecessor, right threads to the successor (used on this page)
With a header node
The two dangling threads point to a dummy header node instead of NULL, so no link is ever NULL
How to read the threaded pictures
Solid lines are real child links. Dashed purple arrows are threads.
A thread from the left side of a node goes to its predecessor; from the right side, to its successor.
The first node in in-order has a NULL left thread and the last has a NULL right thread (labelled NULL).
Threads are not drawn as edges of the tree: the tree shape is still made only by the solid lines.
struct tnode {
int data;
struct tnode *left, *right;
int lthread; /* 1: left is a thread to the predecessor, 0: real child */
int rthread; /* 1: right is a thread to the successor, 0: real child */
};
8.3Insert (build the tree)
1Concept
Walk down as in a BST. Stop when the link in the chosen direction is a thread (not a child): the new key goes there.
The new node is a leaf, so both its links are threads.
If it becomes the left child of parent P: new.left = P.left (P's old predecessor thread), new.right = P. Then P.left = new and P.lthread = 0.
If it becomes the right child of P: new.right = P.right (P's old successor thread), new.left = P. Then P.right = new and P.rthread = 0.
The new node takes over one thread from its parent and gets the parent itself as its other thread.
2Algorithm
Algorithm TBT-INSERT(root, x)
1. par = NULL, p = root
2. While p ≠ NULL:
if x = p.data: duplicate, stop
par = p
if x < p.data: if p.lthread = 1, break; else p = p.left
else: if p.rthread = 1, break; else p = p.right
3. n = new node(x); n.lthread = n.rthread = 1
4. If par = NULL: n.left = n.right = NULL; root = n
5. Else if x < par.data: n.left = par.left; n.right = par; par.left = n; par.lthread = 0
6. Else: n.right = par.right; n.left = par; par.right = n; par.rthread = 0
3Worked example
Step by step Build a threaded tree, then walk it
4C code
struct tnode *insert(struct tnode *root, int x) {
struct tnode *p = root, *par = NULL, *n;
while (p != NULL) {
if (x == p->data) return root; /* duplicate */
par = p;
if (x < p->data) { if (p->lthread) break; p = p->left; }
else { if (p->rthread) break; p = p->right; }
}
n = malloc(sizeof *n);
n->data = x; n->lthread = n->rthread = 1; /* a leaf: two threads */
if (par == NULL) { n->left = n->right = NULL; return n; }
if (x < par->data) {
n->left = par->left; n->right = par; /* inherit the pred thread */
par->lthread = 0; par->left = n;
} else {
n->right = par->right; n->left = par; /* inherit the succ thread */
par->rthread = 0; par->right = n;
}
return root;
}
8.4Search, successor and in-order walk without a stack
1Concept
Search is the BST search, except that reaching a thread (instead of NULL) means "not found".
Successor of p: if p's right link is a thread, it points straight to the successor. Otherwise the successor is the leftmost node of p's right subtree.
In-order walk: start at the leftmost node, then keep moving to the successor. Every link is followed once and nothing is pushed on a stack.
2Algorithm
Algorithm LEFTMOST(p)
While p ≠ NULL and p.lthread = 0: p = p.left
Return p
Algorithm SUCCESSOR(p)
If p.rthread = 1: return p.right (the thread itself)
Return LEFTMOST(p.right)
Algorithm THREADED-INORDER(root)
1. p = LEFTMOST(root)
2. While p ≠ NULL:
visit p
p = SUCCESSOR(p)
3Worked example
Step by step In-order walk using the threads
4C code
struct tnode *leftmost(struct tnode *n) {
while (n != NULL && n->lthread == 0) n = n->left;
return n;
}
struct tnode *inSucc(struct tnode *p) {
return p->rthread ? p->right : leftmost(p->right);
}
void inorder(struct tnode *root) { /* no stack, no recursion */
struct tnode *p = leftmost(root);
while (p != NULL) { printf("%d ", p->data); p = inSucc(p); }
}
struct tnode *search(struct tnode *root, int x) {
struct tnode *p = root;
while (p != NULL) {
if (x == p->data) return p;
if (x < p->data) { if (p->lthread) return NULL; p = p->left; }
else { if (p->rthread) return NULL; p = p->right; }
}
return NULL;
}
8.5Delete
1Concept
The three BST cases, plus one extra job in each: repair the thread that pointed to the deleted node.
Case
What to do
Leaf
The parent's link to it becomes a thread again. If it was a left child: parent.left = its left thread, parent.lthread = 1. If a right child: parent.right = its right thread, parent.rthread = 1.
One child
The child takes its place. If the node had a left subtree, its predecessor's right thread pointed to it: point it to the node's successor instead. If it had a right subtree, its successor's left thread pointed to it: point it to the node's predecessor.
Two children
Copy the in-order successor (leftmost of the right subtree) into the node, then delete the successor, which has no left child (case 1 or 2).
2Algorithm
Algorithm TBT-DELETE(root, x)
1. Find p with p.data = x and its parent par (stop at threads). Not found: stop.
2. If p has two children:
s = leftmost node of p's right subtree, ps = parent of s
p.data = s.data; p = s; par = ps
3. If p is a leaf (both links threads):
if p = par.left: par.left = p.left; par.lthread = 1
else: par.right = p.right; par.rthread = 1
4. Else (one child c):
put c in p's place under par
if p had a left subtree: PRED(p).right = SUCC(p)
else: SUCC(p).left = PRED(p)
5. Free p.
3Worked example
Each example shows one case; the last step of every deletion lists exactly which links changed. Choose Your own input to delete any keys.
Step by step Delete from a threaded BST
4C code
struct tnode *inPred(struct tnode *p) {
if (p->lthread) return p->left;
p = p->left;
while (p->rthread == 0) p = p->right; /* rightmost of the left subtree */
return p;
}
struct tnode *delete(struct tnode *root, int x) {
struct tnode *par = NULL, *p = root, *s, *ps, *child;
while (p != NULL && p->data != x) {
par = p;
if (x < p->data) p = p->lthread ? NULL : p->left;
else p = p->rthread ? NULL : p->right;
}
if (p == NULL) return root; /* not found */
if (p->lthread == 0 && p->rthread == 0) { /* two children */
ps = p; s = p->right;
while (s->lthread == 0) { ps = s; s = s->left; }
p->data = s->data; p = s; par = ps;
}
if (p->lthread && p->rthread) { /* leaf */
if (par == NULL) root = NULL;
else if (p == par->left) { par->lthread = 1; par->left = p->left; }
else { par->rthread = 1; par->right = p->right; }
} else { /* one child */
struct tnode *pr = inPred(p), *sc = inSucc(p);
child = p->lthread ? p->right : p->left;
if (par == NULL) root = child;
else if (p == par->left) par->left = child;
else par->right = child;
if (p->lthread == 0) pr->right = sc;
else sc->left = pr;
}
free(p);
return root;
}
8.6Advantages, disadvantages and complexity
Advantages
In-order traversal in O(n) time with O(1) extra space · successor and predecessor are found quickly, often in one step · no link is wasted · a walk can start from any node, not only the root.
Disadvantages
Insert and delete must also fix threads · every node needs two flag bits · the code is harder to write and debug · the tree is not balanced (height can reach n − 1).
Operation
Normal linked BST
Threaded BST
In-order traversal: time / extra space
O(n) / O(h) stack
O(n) / O(1)
In-order successor of a node
O(h), needs the parent or a search from the root
O(1) if the right link is a thread, else O(h)
Search, insert, delete
O(h)
O(h)
8.7Real-life applications
Traversal with fixed memory
On small embedded devices, or for very deep trees where recursion could overflow the call stack, a threaded walk uses the same few variables whatever the tree size.
Why threads: O(1) extra memory.
Morris traversal
The interview version of the same idea: create temporary threads during an in-order walk and remove them afterwards, so an ordinary tree is walked in O(1) space and left unchanged.
Why threads: the classic answer to "traverse without a stack".
Iterating an ordered map
Stepping to "the next key" (an iterator's ++) is cheap when every node can reach its successor directly. Linking neighbours for ordered walks is also the idea behind the leaf chain of a B+ tree.
Why threads: next and previous without going back to the root.
8.8Interview and placement questions
Questions asked in viva and interviews
Q1How many NULL links does a binary tree with n nodes have, and what do threads do with them?
2n links, n − 1 of them are edges, so n + 1 are NULL. In a threaded tree all of them except the first node's left and the last node's right become threads (or all of them point to a header node).
Q2Why are the flag bits needed?
A thread and a child link are both just pointers. Without a flag, a walk cannot tell whether to go down into a subtree or jump to the successor, and search would follow a thread as if it were a child.
Q3How do you find the in-order successor of a node in a threaded tree?
If rthread = 1, the right link is the successor. Otherwise go to the right child and then keep going left (while lthread = 0).
Q4What is the purpose of a header (dummy) node?
The first node's left thread and the last node's right thread point to the header instead of NULL, and the header's left link points to the root. Then no pointer is ever NULL, the walk can loop until it returns to the header, and the empty tree needs no special case.
Q5Can pre-order also be done without a stack in an in-threaded tree?
Yes. Visit a node; if it has a real left child go there; otherwise climb along right threads until a node with a real right child is found and go to that right child. The complete program below includes it.
Q6Threaded tree or Morris traversal?
A threaded tree keeps its threads permanently (extra flags, careful updates). Morris traversal needs no flags: it makes a thread only while walking and removes it on the second visit, so the tree is unchanged afterwards. Both take O(n) time and O(1) space.
Print the in-order traversal of an ordinary binary tree without recursion and without a stack.
Idea. If the current node has no left child, visit it and go right. Otherwise find its in-order predecessor (rightmost node of the left subtree). If that node's right link is NULL, point it to the current node (a temporary thread) and go left. If it already points here, the left subtree is done: remove the thread, visit, go right.
Time O(n) · Space O(1)
Show the C program and its output
/* Q1. Morris in-order traversal: O(1) extra space, using temporary threads */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *insert(struct node *r, int x) {
if (r == NULL) return newNode(x);
if (x < r->data) r->left = insert(r->left, x); else r->right = insert(r->right, x);
return r;
}
void morrisInorder(struct node *cur) {
struct node *pre;
int threads = 0;
while (cur != NULL) {
if (cur->left == NULL) { /* nothing on the left: visit, go right */
printf("%d ", cur->data);
cur = cur->right; /* may be a real child or a thread */
} else {
pre = cur->left; /* in-order predecessor of cur */
while (pre->right != NULL && pre->right != cur) pre = pre->right;
if (pre->right == NULL) { /* first visit: make a thread back to cur */
pre->right = cur; threads++;
cur = cur->left;
} else { /* second visit: remove the thread, visit */
pre->right = NULL;
printf("%d ", cur->data);
cur = cur->right;
}
}
}
printf("\n(%d temporary threads made and removed; the tree is unchanged)\n", threads);
}
void freeTree(struct node *r) { if (r) { freeTree(r->left); freeTree(r->right); free(r); } }
int main(void) {
int keys[] = {50, 30, 70, 20, 40, 60, 80}, i;
struct node *root = NULL;
for (i = 0; i < 7; i++) root = insert(root, keys[i]);
printf("In-order (Morris): ");
morrisInorder(root);
printf("Again: ");
morrisInorder(root);
freeTree(root);
return 0;
}
Output
In-order (Morris): 20 30 40 50 60 70 80
(3 temporary threads made and removed; the tree is unchanged)
Again: 20 30 40 50 60 70 80
(3 temporary threads made and removed; the tree is unchanged)
C2BST to sorted doubly linked list, in placeMediumIn-order with a prev pointer
Reuse the left and right links as prev and next so that the nodes form a sorted doubly linked list.
Idea. Walk in-order and remember the previously visited node. Link prev.right = current and current.left = prev. The first node visited is the head.
Time O(n) · Space O(h)
Show the C program and its output
/* Q2. Convert a BST into a sorted doubly linked list in place (left = prev, right = next) */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right; };
struct node *newNode(int x) {
struct node *n = malloc(sizeof *n);
n->data = x; n->left = n->right = NULL;
return n;
}
struct node *insert(struct node *r, int x) {
if (r == NULL) return newNode(x);
if (x < r->data) r->left = insert(r->left, x); else r->right = insert(r->right, x);
return r;
}
struct node *head = NULL, *prev = NULL;
/* in-order walk: link each node to the one visited just before it */
void convert(struct node *r) {
struct node *right;
if (r == NULL) return;
convert(r->left);
right = r->right; /* save: r->right is about to change */
if (prev == NULL) head = r; /* smallest key starts the list */
else { prev->right = r; r->left = prev; }
prev = r;
convert(right);
}
int main(void) {
int keys[] = {50, 30, 70, 20, 40, 60, 80}, i;
struct node *root = NULL, *p, *q;
for (i = 0; i < 7; i++) root = insert(root, keys[i]);
convert(root);
prev->right = NULL; head->left = NULL;
printf("Forward : ");
for (p = head; p != NULL; p = p->right) { printf("%d ", p->data); if (p->right == NULL) q = p; }
printf("\nBackward: ");
for (p = q; p != NULL; p = p->left) printf("%d ", p->data);
printf("\n");
for (p = head; p != NULL; p = q) { q = p->right; free(p); }
return 0;
}
Insert, search and delete your own keys. Dashed arrows are the threads. Then follow the same steps in the tracer, and read the complete program. It was tested on 3000 random insert and delete sequences, checking the order and every thread after each step.
Try it Threaded BST lab
Algorithm tracer: insert, search, in-order walk and delete
Trace Threaded BST algorithms, step by step
Program 8 · Threaded BST: insert, search, delete, in-order and pre-order without a stack
/* In-threaded binary search tree: insert, search, delete, in-order and pre-order without stack or recursion */
#include <stdio.h>
#include <stdlib.h>
struct tnode {
int data;
struct tnode *left, *right;
int lthread; /* 1: left is a thread to the in-order predecessor (or NULL) */
int rthread; /* 1: right is a thread to the in-order successor (or NULL) */
};
struct tnode *insert(struct tnode *root, int x) {
struct tnode *p = root, *par = NULL, *n;
while (p != NULL) { /* ordinary BST walk ... */
if (x == p->data) { printf("%d already exists\n", x); return root; }
par = p;
if (x < p->data) { if (p->lthread) break; p = p->left; } /* ... that stops at a thread */
else { if (p->rthread) break; p = p->right; }
}
n = malloc(sizeof *n);
n->data = x; n->lthread = n->rthread = 1; /* a new node is a leaf: both links are threads */
if (par == NULL) { n->left = n->right = NULL; return n; }
if (x < par->data) {
n->left = par->left; /* inherit par's predecessor thread */
n->right = par; /* successor of n is par */
par->lthread = 0; par->left = n;
} else {
n->right = par->right; /* inherit par's successor thread */
n->left = par; /* predecessor of n is par */
par->rthread = 0; par->right = n;
}
return root;
}
struct tnode *leftmost(struct tnode *n) {
while (n != NULL && n->lthread == 0) n = n->left;
return n;
}
void inorder(struct tnode *root) {
struct tnode *p = leftmost(root);
while (p != NULL) {
printf("%d ", p->data);
if (p->rthread) p = p->right; /* follow the thread to the successor */
else p = leftmost(p->right); /* or go to the right subtree's leftmost */
}
printf("\n");
}
void preorder(struct tnode *root) {
struct tnode *p = root;
while (p != NULL) {
printf("%d ", p->data);
if (p->lthread == 0) p = p->left; /* a real left child comes next */
else {
while (p != NULL && p->rthread) p = p->right; /* climb by threads ... */
if (p != NULL) p = p->right; /* ... to the next right subtree */
}
}
printf("\n");
}
struct tnode *inSucc(struct tnode *p) { /* in-order successor */
return p->rthread ? p->right : leftmost(p->right);
}
struct tnode *inPred(struct tnode *p) { /* in-order predecessor */
if (p->lthread) return p->left;
p = p->left;
while (p->rthread == 0) p = p->right; /* rightmost node of the left subtree */
return p;
}
struct tnode *search(struct tnode *root, int x) {
struct tnode *p = root;
while (p != NULL) {
if (x == p->data) return p;
if (x < p->data) { if (p->lthread) return NULL; p = p->left; } /* a thread: no child there */
else { if (p->rthread) return NULL; p = p->right; }
}
return NULL;
}
struct tnode *delete(struct tnode *root, int x) {
struct tnode *par = NULL, *p = root, *s, *ps, *child;
while (p != NULL && p->data != x) { /* find the node and its parent */
par = p;
if (x < p->data) p = p->lthread ? NULL : p->left;
else p = p->rthread ? NULL : p->right;
}
if (p == NULL) { printf("%d not found\n", x); return root; }
if (p->lthread == 0 && p->rthread == 0) { /* two children: copy the successor ... */
ps = p; s = p->right;
while (s->lthread == 0) { ps = s; s = s->left; }
p->data = s->data;
p = s; par = ps; /* ... and delete it (it has no left child) */
}
if (p->lthread && p->rthread) { /* leaf: the parent's link becomes a thread */
if (par == NULL) root = NULL;
else if (p == par->left) { par->lthread = 1; par->left = p->left; }
else { par->rthread = 1; par->right = p->right; }
} else { /* one child: it takes p's place */
struct tnode *pr = inPred(p), *sc = inSucc(p);
child = p->lthread ? p->right : p->left;
if (par == NULL) root = child;
else if (p == par->left) par->left = child;
else par->right = child;
if (p->lthread == 0) pr->right = sc; /* pred's thread pointed to p: move it on */
else sc->left = pr; /* succ's thread pointed to p: move it back */
}
free(p);
return root;
}
void links(struct tnode *root) {
struct tnode *p = leftmost(root);
while (p != NULL) {
printf("%3d: left = ", p->data);
if (p->lthread) printf(p->left ? "thread to %-4d" : "NULL ", p->left ? p->left->data : 0);
else printf("child %-7d ", p->left->data);
printf(" right = ");
if (p->rthread) { if (p->right) printf("thread to %d\n", p->right->data); else printf("NULL\n"); }
else printf("child %d\n", p->right->data);
p = p->rthread ? p->right : leftmost(p->right);
}
}
int main(void) {
struct tnode *root = NULL;
int ch, k, i, x;
while (1) {
printf("\n1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &k); printf("Keys: ");
for (i = 0; i < k; i++) { scanf("%d", &x); root = insert(root, x); } break;
case 2: printf("In-order (threads, no stack): "); inorder(root); break;
case 3: printf("Pre-order (threads, no stack): "); preorder(root); break;
case 4: links(root); break;
case 5: printf("Key: "); scanf("%d", &x);
printf(search(root, x) ? "%d found\n" : "%d not found\n", x); break;
case 6: printf("Key: "); scanf("%d", &x); root = delete(root, x); break;
case 7: return 0;
}
}
return 0;
}
Sample run
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 1
How many? 7
Keys: 50 30 70 20 40 60 80
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 2
In-order (threads, no stack): 20 30 40 50 60 70 80
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 3
Pre-order (threads, no stack): 50 30 20 40 70 60 80
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 5
Key: 60
60 found
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 5
Key: 65
65 not found
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 6
Key: 40
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 6
Key: 30
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 6
Key: 50
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 2
In-order (threads, no stack): 20 60 70 80
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 4
20: left = NULL right = thread to 60
60: left = child 20 right = child 70
70: left = thread to 60 right = child 80
80: left = thread to 70 right = NULL
1.Insert many 2.In-order 3.Pre-order 4.Show every link 5.Search 6.Delete 7.Exit
Choice: 7
Common mistakes in this part
Following a thread during search as if it were a child link. A thread means "no child here".
Pointing a right thread to the parent instead of the in-order successor. They are the same only when the node is a left child.
Forgetting to set the parent's flag to 0 when a new child is attached, or back to 1 when a leaf is deleted.
Calling a thread an edge. Threads do not change the shape, height or degree of the tree.
Key points to remember
NULL left → predecessor, NULL right → successor; flags mark threads.
n nodes → n + 1 NULL links, all reused (except two, or none with a header node).
In-order walk: leftmost node, then successor again and again. O(n) time, O(1) space.
Insert: the new leaf inherits one thread from its parent and points to the parent with the other.
Delete: BST cases + repair the thread that pointed to the removed node.
Part 10 · Beyond the syllabus
Red-Black Tree
A binary search tree that colours each node red or black and follows four simple colour rules. The rules keep the height below 2 log2(n + 1) while needing very few rotations, which is why most standard libraries use it for ordered maps and sets.
Beyond the syllabus: a favourite interview topic ("AVL or red-black?", "what does std::map use?").
10.1At a glance
A red-black tree is a BST where every node is red or black, the root is black, a red node never has a red child, and every path from a node down to NIL has the same number of black nodes.
Problem it solves
A plain BST can become a chain. An AVL tree fixes that with strict balance, but may rotate at many levels on a delete. A red-black tree accepts a little more height and in return fixes most updates by recolouring, with at most 2 rotations per insert and 3 per delete.
The rule
Root black · no red node with a red child · equal black count on every root-to-NIL path. Longest path ≤ 2 × shortest path.
Think of it as
A building with a lift in every tower: every route down passes the same number of black "floors"; red floors can be added in between, but never two in a row.
Use it when
A sorted, in-memory map or set with frequent inserts and deletes: library containers, kernel schedulers, timers.
Not the right tool when
Lookups dominate and updates are rare (an AVL tree is a little shorter), or the data is on disk (use a B-tree / B+ tree).
A red-black tree is a binary search tree in which every node is coloured red or black and:
Every node is either red or black.
The root is black.
Every leaf (NIL, the empty link) is considered black.
A red node cannot have a red child (no two reds in a row).
Every path from a node down to its NIL leaves contains the same number of black nodes. This number is the node's black-height.
Why the height stays small: reds cannot be next to each other, so at least half the nodes on any root-to-NIL path are black. All paths have the same number of black nodes, so the longest path is at most twice the shortest. A tree whose root has black-height b contains at least 2b − 1 nodes, which gives h ≤ 2 log2(n + 1).
How to read the red-black pictures
Nodes are drawn in their real colour: dark = black, red = red. NIL leaves are not drawn, but they count as black.
A coloured ring shows the step: amber = current node, violet = the parent, uncle or sibling being checked, blue = new or just moved.
During deletion, the small label x labels the node carrying the missing black ("double black") and w labels its sibling.
To check a tree, count the black nodes on each path from the root to an empty link: all counts must be equal.
#define RED 0
#define BLACK 1
struct node {
int data, color;
struct node *left, *right, *parent; /* the parent is needed to walk back up */
};
10.3Insert
1Concept
Insert as in a BST and colour the new node red (a red node changes no black count). The only rule that can break is "no two reds": if the parent is also red, look at the uncle (the parent's sibling).
Case
Condition
Fix
1
Uncle is red
Recolour: parent and uncle → black, grandparent → red. Move up to the grandparent and check again.
2
Uncle is black, node is an inner child (zig-zag)
Rotate at the parent. This turns it into case 3.
3
Uncle is black, node is an outer child (straight line)
Rotate at the grandparent and swap the colours of parent and grandparent. Done.
Root
The root ended up red
Colour it black.
Uncle red → recolour and move up. Uncle black → rotate (once or twice) and stop.
2Algorithm
Algorithm RB-INSERT(T, z)
1. Insert z as in a BST; colour z RED.
2. While z ≠ root and parent(z) is RED:
p = parent(z), g = grandparent(z), u = uncle(z)
If u is RED: (case 1)
p, u → BLACK; g → RED; z = g
Else:
If z is an inner child: z = p; rotate at z (case 2)
parent(z) → BLACK; g → RED; rotate at g (case 3)
3. Colour the root BLACK.
3Worked example
Step by step Red-black tree insertion
4C code
void fixInsert(struct node *z) {
struct node *p, *g, *u;
while (z != root && z->parent->color == RED) {
p = z->parent; g = p->parent;
if (p == g->left) {
u = g->right;
if (u != NULL && u->color == RED) { /* case 1 */
p->color = BLACK; u->color = BLACK; g->color = RED;
z = g;
} else {
if (z == p->right) { z = p; rotateLeft(z); p = z->parent; } /* case 2 */
p->color = BLACK; g->color = RED; rotateRight(g); /* case 3 */
}
} else { /* the same with left and right exchanged */ }
}
root->color = BLACK;
}
10.4Search
1Concept
Search ignores the colours: it is exactly the BST search. The colour rules only guarantee that the path is short: at most 2 log2(n + 1) comparisons, even for sorted input.
2Algorithm
Algorithm RB-SEARCH(root, x)
1. t = root
2. While t ≠ NIL and t.data ≠ x:
t = (x < t.data) ? t.left : t.right
3. Return t (NIL means not found)
3Worked example
Step by step Search in a red-black tree
4C code
struct node *search(int x) {
struct node *t = root;
while (t != NULL && t->data != x) t = x < t->data ? t->left : t->right;
return t;
}
10.5Delete
1Concept
Delete as in a BST. If the node has two children, copy its in-order successor into it and delete the successor instead. So the node really removed has at most one child.
If the removed node was red: nothing breaks. Done.
If it was black and its child is red: colour the child black. Done.
Otherwise one black is missing on some paths. Mark the node that took its place (maybe NIL) as double black, x, and fix it using its sibling w:
Case
Sibling w
Fix
1
w is red
Colour w black and the parent red, rotate at the parent (towards x). The new sibling is black: go on to case 2, 3 or 4.
2
w black, both its children black
Colour w red. The extra black moves up to the parent. If the parent was red, colour it black and stop; otherwise repeat at the parent.
3
w black, near child red, far child black
Colour the near child black and w red, rotate at w (away from x). This becomes case 4.
4
w black, far child red
w takes the parent's colour; the parent and the far child become black; rotate at the parent (towards x). Done.
Removing a red node is free. The work starts only when a black node disappears; then the sibling decides the case.
2Algorithm
Algorithm RB-DELETE-FIXUP(x) (x is a left child; mirror otherwise)
While x ≠ root and x is BLACK:
w = sibling(x), p = parent(x)
If w is RED: (case 1)
w → BLACK; p → RED; rotate left at p; w = sibling(x)
If w.left and w.right are BLACK: (case 2)
w → RED; x = p
Else:
If w.right is BLACK: (case 3)
w.left → BLACK; w → RED; rotate right at w; w = sibling(x)
w.colour = p.colour; p → BLACK; w.right → BLACK (case 4)
rotate left at p; x = root
Colour x BLACK.
3Worked example
Step by step Red-black tree deletion (every case)
4C code
/* x carries the extra black; xp is its parent (x may be NULL) */
void fixDelete(struct node *x, struct node *xp) {
struct node *w;
while (x != root && colourOf(x) == BLACK) {
if (x == xp->left) {
w = xp->right;
if (colourOf(w) == RED) { /* case 1 */
w->color = BLACK; xp->color = RED; rotateLeft(xp); w = xp->right;
}
if (colourOf(w->left) == BLACK && colourOf(w->right) == BLACK) {
w->color = RED; x = xp; xp = x->parent; /* case 2 */
} else {
if (colourOf(w->right) == BLACK) { /* case 3 */
w->left->color = BLACK; w->color = RED; rotateRight(w); w = xp->right;
}
w->color = xp->color; xp->color = BLACK; /* case 4 */
w->right->color = BLACK; rotateLeft(xp);
x = root;
}
} else { /* the same with left and right exchanged */ }
}
if (x != NULL) x->color = BLACK;
}
10.6AVL tree vs red-black tree
AVL tree
Red-black tree
Balance rule
|h(left) − h(right)| ≤ 1 at every node
Colour rules; longest path ≤ 2 × shortest
Height bound
≈ 1.44 log2 n (shorter)
≤ 2 log2(n + 1)
Extra data per node
Height or balance factor
One colour bit
Rotations per insert
At most one single or double rotation
At most 2
Rotations per delete
Up to O(log n) (one per level)
At most 3
Faster for
Lookup-heavy work
Insert and delete-heavy work
Typical use
In-memory lookup tables, databases in memory
Library maps and sets, kernels
Both give O(log n) for every operation. In an interview the short answer is: AVL is more strictly balanced (faster search), red-black does less work per update (faster insert and delete).
10.7Real-life applications
C++ and Java ordered containers
std::map, std::set (GCC's libstdc++ and LLVM's libc++) and Java's TreeMap and TreeSet are red-black trees.
Why red-black: guaranteed O(log n) with cheap updates.
Linux kernel
The kernel's generic rbtree is used by the CPU scheduler to order runnable tasks, by high-resolution timers, and in many drivers and file systems.
Why red-black: predictable time for frequent inserts and deletes.
Java HashMap buckets
Since Java 8, a HashMap bucket that collects many colliding keys is turned from a linked list into a red-black tree, so a bad hash gives O(log n) lookups instead of O(n).
Why red-black: a balanced fallback inside a hash table.
10.8Interview and placement questions
Questions asked in viva and interviews
Q1Why is a new node coloured red and not black?
A red node adds nothing to any black count, so the equal-black-height rule cannot break. Only "no two reds" can break, and that is fixed locally. A new black node would make one path longer in blacks, which is much harder to repair.
Q2Prove that the height of a red-black tree is O(log n).
Let b be the black-height of the root. By induction, a subtree with black-height b has at least 2b − 1 nodes. No two reds are adjacent, so b ≥ h/2. So n ≥ 2h/2 − 1, giving h ≤ 2 log2(n + 1).
Q3Is every AVL tree a red-black tree (after colouring)? Is every red-black tree an AVL tree?
Every AVL tree can be coloured to satisfy the red-black rules. The reverse is false. Example: 10(black) with left child 5(black) and right child 30(red); 30 has children 20(black) and 40(black); 40 has a right child 50(red). Every path has 2 black nodes, so it is a valid red-black tree, but at 10 the left height is 0 and the right height is 2.
Q4What is the maximum number of rotations for insert and delete?
Insert: 2 (case 2 then case 3). Delete: 3 (case 1, then case 3, then case 4). Recolouring (case 1 of insert, case 2 of delete) can move up O(log n) levels, but it only changes colour bits.
Q5How is a red-black tree related to a 2-3-4 tree?
Merge every red node into its black parent: each black node with its red children becomes one 2-, 3- or 4-node. The result is a 2-3-4 tree, and the black-height is its height. Red-black insertion cases are the 2-3-4 split in disguise. See the 2-3-4 page.
Q6Can a red-black tree have only black nodes?
Yes, if it is a perfect binary tree (all paths the same length). Any other shape needs some red nodes to make the black counts equal.
Q7Why do libraries use red-black trees for maps instead of hash tables?
They keep keys sorted: in-order iteration, lower_bound / upper_bound, first and last key and range queries all come for free, with a worst-case O(log n) guarantee. Hash tables are faster on average for plain lookups but cannot answer ordered queries (C++ offers both: map and unordered_map).
Coding problems (tested C programs)
C1Check the red-black rulesMediumPost-order returning a height
Given a coloured BST, decide whether it is a valid red-black tree.
Idea. A recursive function returns the black-height of a subtree, or −1 if a rule is broken inside it. At each node: a red node must not have a red child, both subtrees must return the same black-height, and the result adds 1 if the node is black. Finally the root must be black.
Time O(n) · Space O(h)
Show the C program and its output
/* Q1. Check the red-black rules: root black, no red node with a red child, equal black-height */
#include <stdio.h>
#include <stdlib.h>
#define RED 0
#define BLACK 1
struct node { int data, color; struct node *left, *right; };
struct node *mk(int d, int c, struct node *l, struct node *r) {
struct node *n = malloc(sizeof *n);
n->data = d; n->color = c; n->left = l; n->right = r;
return n;
}
/* black-height of r (NULL leaves count as 1), or -1 if a rule is broken */
int blackHeight(struct node *r) {
int l, rt;
if (r == NULL) return 1;
if (r->color == RED)
if ((r->left && r->left->color == RED) || (r->right && r->right->color == RED)) return -1;
l = blackHeight(r->left);
rt = blackHeight(r->right);
if (l == -1 || rt == -1 || l != rt) return -1; /* unequal black counts */
return l + (r->color == BLACK);
}
int isRB(struct node *root) { return root == NULL || (root->color == BLACK && blackHeight(root) != -1); }
int main(void) {
struct node *t1 = mk(20, BLACK, mk(10, RED, mk(5, BLACK, mk(1, RED, NULL, NULL), NULL), mk(15, BLACK, NULL, NULL)),
mk(30, BLACK, mk(25, RED, NULL, NULL), NULL));
struct node *t2 = mk(20, BLACK, mk(10, BLACK, NULL, NULL), mk(30, BLACK, NULL, mk(40, BLACK, NULL, NULL)));
struct node *t3 = mk(20, BLACK, mk(10, RED, mk(5, RED, NULL, NULL), NULL), mk(30, BLACK, NULL, NULL));
printf("t1: %s\n", isRB(t1) ? "valid red-black tree" : "invalid");
printf("t2: %s\n", isRB(t2) ? "valid" : "invalid (paths have different black counts)");
printf("t3: %s\n", isRB(t3) ? "valid" : "invalid (red 10 has a red child 5)");
return 0;
}
Output
t1: valid red-black tree
t2: invalid (paths have different black counts)
t3: invalid (red 10 has a red child 5)
10.9Lab and complete program
Try it Red-black tree insertion lab
Algorithm tracer: red-black insert, search and delete
Trace Red-black algorithms, step by step
Program 9 · Red-black tree: insert, search, delete, rule check
/* Red-black tree: insert, search and delete with recolouring and rotations, plus a rule check */
#include <stdio.h>
#include <stdlib.h>
#define RED 0
#define BLACK 1
struct node { int data, color; struct node *left, *right, *parent; };
struct node *root = NULL;
void rotateLeft(struct node *x) {
struct node *y = x->right;
x->right = y->left;
if (y->left) y->left->parent = x;
y->parent = x->parent;
if (x->parent == NULL) root = y;
else if (x == x->parent->left) x->parent->left = y;
else x->parent->right = y;
y->left = x; x->parent = y;
}
void rotateRight(struct node *x) {
struct node *y = x->left;
x->left = y->right;
if (y->right) y->right->parent = x;
y->parent = x->parent;
if (x->parent == NULL) root = y;
else if (x == x->parent->right) x->parent->right = y;
else x->parent->left = y;
y->right = x; x->parent = y;
}
void fixInsert(struct node *z) {
struct node *p, *g, *u;
while (z != root && z->parent->color == RED) {
p = z->parent; g = p->parent;
if (p == g->left) {
u = g->right;
if (u != NULL && u->color == RED) { /* case 1: red uncle */
printf(" case 1 at %d: recolour\n", z->data);
p->color = BLACK; u->color = BLACK; g->color = RED;
z = g;
} else {
if (z == p->right) { /* case 2: inner child */
printf(" case 2 at %d: rotate left at %d\n", z->data, p->data);
z = p; rotateLeft(z); p = z->parent;
}
printf(" case 3 at %d: rotate right at %d\n", z->data, g->data);
p->color = BLACK; g->color = RED; /* case 3: outer child */
rotateRight(g);
}
} else { /* mirror image */
u = g->left;
if (u != NULL && u->color == RED) {
printf(" case 1 at %d: recolour\n", z->data);
p->color = BLACK; u->color = BLACK; g->color = RED;
z = g;
} else {
if (z == p->left) {
printf(" case 2 at %d: rotate right at %d\n", z->data, p->data);
z = p; rotateRight(z); p = z->parent;
}
printf(" case 3 at %d: rotate left at %d\n", z->data, g->data);
p->color = BLACK; g->color = RED;
rotateLeft(g);
}
}
}
root->color = BLACK; /* the root is always black */
}
void insert(int x) {
struct node *y = NULL, *t = root, *z;
while (t != NULL) { /* ordinary BST insert */
y = t;
if (x == t->data) { printf("%d already exists\n", x); return; }
t = x < t->data ? t->left : t->right;
}
z = malloc(sizeof *z);
z->data = x; z->color = RED; z->left = z->right = NULL; z->parent = y;
if (y == NULL) root = z;
else if (x < y->data) y->left = z;
else y->right = z;
fixInsert(z);
}
struct node *search(int x) {
struct node *t = root;
while (t != NULL && t->data != x) t = x < t->data ? t->left : t->right;
return t;
}
int colourOf(struct node *n) { return n == NULL ? BLACK : n->color; } /* NIL leaves are black */
/* put subtree v in the place of subtree u */
void transplant(struct node *u, struct node *v) {
if (u->parent == NULL) root = v;
else if (u == u->parent->left) u->parent->left = v;
else u->parent->right = v;
if (v != NULL) v->parent = u->parent;
}
/* x carries an extra black ("double black"); xp is its parent, because x may be NULL */
void fixDelete(struct node *x, struct node *xp) {
struct node *w;
while (x != root && colourOf(x) == BLACK) {
if (x == xp->left) {
w = xp->right; /* sibling */
if (colourOf(w) == RED) { /* case 1: red sibling */
printf(" case 1: sibling %d is red, rotate left at %d\n", w->data, xp->data);
w->color = BLACK; xp->color = RED; rotateLeft(xp); w = xp->right;
}
if (colourOf(w->left) == BLACK && colourOf(w->right) == BLACK) {
printf(" case 2: sibling %d has two black children, recolour it\n", w->data);
w->color = RED; x = xp; xp = x->parent; /* case 2: move the extra black up */
} else {
if (colourOf(w->right) == BLACK) { /* case 3: near child red */
printf(" case 3: rotate right at sibling %d\n", w->data);
w->left->color = BLACK; w->color = RED; rotateRight(w); w = xp->right;
}
printf(" case 4: rotate left at %d\n", xp->data);
w->color = xp->color; xp->color = BLACK; /* case 4: far child red */
w->right->color = BLACK; rotateLeft(xp);
x = root; xp = NULL;
}
} else { /* mirror image */
w = xp->left;
if (colourOf(w) == RED) {
printf(" case 1: sibling %d is red, rotate right at %d\n", w->data, xp->data);
w->color = BLACK; xp->color = RED; rotateRight(xp); w = xp->left;
}
if (colourOf(w->left) == BLACK && colourOf(w->right) == BLACK) {
printf(" case 2: sibling %d has two black children, recolour it\n", w->data);
w->color = RED; x = xp; xp = x->parent;
} else {
if (colourOf(w->left) == BLACK) {
printf(" case 3: rotate left at sibling %d\n", w->data);
w->right->color = BLACK; w->color = RED; rotateLeft(w); w = xp->left;
}
printf(" case 4: rotate right at %d\n", xp->data);
w->color = xp->color; xp->color = BLACK;
w->left->color = BLACK; rotateRight(xp);
x = root; xp = NULL;
}
}
}
if (x != NULL) x->color = BLACK;
}
void delete(int k) {
struct node *z = search(k), *y, *x, *xp;
int yColour;
if (z == NULL) { printf("%d not found\n", k); return; }
y = z; yColour = y->color;
if (z->left == NULL) { x = z->right; xp = z->parent; transplant(z, z->right); }
else if (z->right == NULL) { x = z->left; xp = z->parent; transplant(z, z->left); }
else { /* two children: use the successor y */
y = z->right; while (y->left != NULL) y = y->left;
yColour = y->color; x = y->right;
if (y->parent == z) xp = y;
else { xp = y->parent; transplant(y, y->right); y->right = z->right; y->right->parent = y; }
transplant(z, y); y->left = z->left; y->left->parent = y; y->color = z->color;
}
free(z);
if (yColour == BLACK) fixDelete(x, xp); /* removing a red node breaks nothing */
}
/* returns the black-height, or -1 if a rule is broken below r */
int check(struct node *r) {
int l, rt;
if (r == NULL) return 1; /* NULL leaves are black */
if (r->color == RED && ((r->left && r->left->color == RED) || (r->right && r->right->color == RED))) return -1;
l = check(r->left); rt = check(r->right);
if (l == -1 || rt == -1 || l != rt) return -1;
return l + (r->color == BLACK);
}
void preorder(struct node *r) {
if (r == NULL) return;
printf("%d%s ", r->data, r->color == RED ? "(R)" : "(B)");
preorder(r->left); preorder(r->right);
}
int main(void) {
int ch, k, i, x;
while (1) {
printf("\n1.Insert many 2.Show 3.Search 4.Delete 5.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &k); printf("Keys: ");
for (i = 0; i < k; i++) { scanf("%d", &x); printf("Insert %d\n", x); insert(x); } break;
case 2: printf("Pre-order: "); preorder(root);
x = check(root);
if (x == -1) printf("\nRed-black rules broken!\n");
else printf("\nAll rules hold. Black-height (counting NULL leaves) = %d\n", x);
break;
case 3: printf("Key: "); scanf("%d", &x);
printf(search(x) ? "%d found\n" : "%d not found\n", x); break;
case 4: printf("Key: "); scanf("%d", &x); printf("Delete %d\n", x); delete(x); break;
case 5: return 0;
}
}
return 0;
}
Sample run
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 1
How many? 7
Keys: 10 20 30 15 25 5 1
Insert 10
Insert 20
Insert 30
case 3 at 30: rotate left at 10
Insert 15
case 1 at 15: recolour
Insert 25
Insert 5
Insert 1
case 1 at 1: recolour
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 2
Pre-order: 20(B) 10(R) 5(B) 1(R) 15(B) 30(B) 25(R)
All rules hold. Black-height (counting NULL leaves) = 3
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 3
Key: 25
25 found
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 3
Key: 12
12 not found
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 4
Key: 1
Delete 1
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 4
Key: 5
Delete 5
case 2: sibling 15 has two black children, recolour it
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 2
Pre-order: 20(B) 10(B) 15(R) 30(B) 25(R)
All rules hold. Black-height (counting NULL leaves) = 3
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 4
Key: 20
Delete 20
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 2
Pre-order: 25(B) 10(B) 15(R) 30(B)
All rules hold. Black-height (counting NULL leaves) = 3
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 5
Compared with the visualizer on 3000 random sequences of inserts and deletes: the trees and colours are identical and every rule holds after each step.
Common mistakes in this part
Colouring a new node black.
Looking at the sibling instead of the uncle during insertion (the sibling is for deletion).
Forgetting that NIL leaves count as black when checking the uncle or the sibling's children.
Stopping after case 2 of insertion without rotating again in case 3.
Counting the node itself differently on different paths when checking black-height.
Insert red. Uncle red → recolour and move up. Uncle black → rotate (inner child: two rotations) and recolour.
Delete: red removed → done; red child → colour it black; otherwise double black, fixed by the sibling's cases 1–4.
At most 2 rotations per insert, 3 per delete.
Used in std::map, TreeMap and the Linux kernel.
Part 11 · Beyond the syllabus
Splay Tree
A self-adjusting binary search tree: every key you search, insert or delete is rotated up to the root. It stores no balance information, yet any sequence of m operations costs O(m log n), and keys used often stay near the top.
Beyond the syllabus: asked in interviews as the example of amortised analysis and self-adjusting structures.
11.1At a glance
A splay tree is a BST that, after every access, moves the accessed node to the root with special rotations called splaying (zig, zig-zig, zig-zag).
Problem it solves
Real access patterns are uneven: some keys are asked for again and again. A splay tree moves each accessed key to the root, so repeated and nearby accesses become cheap, without storing heights or colours.
The rule
BST order, nothing else. After each search, insert or delete, splay the node reached to the root.
Think of it as
Your desk: whatever file you just used goes on top of the pile, so the files you use most are always close at hand.
Use it when
Accesses repeat or cluster (caches, recently used items), and a simple balanced tree with no extra fields per node is wanted.
Not the right tool when
Every single operation must be fast (one access can take O(n)), for real-time systems, or when many threads read the tree at once (even a search changes the tree).
Search, insert, deleteO(log n) amortisedOne operationO(n) worst caseExtra data per nodenone
Used inGCC (libiberty splay-tree)FreeBSD virtual memory mapCaches of recent itemsAdaptive compression
A splay tree is a self-adjusting binary search tree: after every search, insert or delete, the accessed node is moved to the root by a sequence of rotations called splaying. It stores no balance information.
1Concept
Let x be the node to splay, p its parent and g its grandparent. Repeat one of three steps until x is the root:
Step
When
Rotations
Zig
p is the root
One rotation at p
Zig-zig
x and p are both left children (or both right): a straight line
Rotate at g first, then at p
Zig-zag
x is a right child and p a left child (or the mirror): a bend
Rotate at p first, then at g (like an AVL double rotation)
Zig-zig rotates the grandparent first. Rotating x up one level at a time (parent first) would also bring x to the root, but it does not halve the path, and the O(log n) amortised bound is lost.
How to read the splay pictures
Labels under the nodes: x = the node being splayed, p = its parent, g = its grandparent.
Amber = x, violet = p, red = g (the rotation point of the first zig-zig rotation).
Triangles A, B, C, D are whole subtrees. They keep their left-to-right order: only their parent changes.
2Algorithm
Algorithm SPLAY(x)
While x is not the root:
p = parent(x), g = parent(p)
If g = NULL: ZIG: rotate at p
Else if x and p are both left children
or both right children: ZIG-ZIG: rotate at g, then at p
Else: ZIG-ZAG: rotate at p, then at g
3Worked example
Zig p is the root
One rotation at p brings x to the root.
Zig-zig x and p are both left (or both right) children
Rotate at g first, then at p.
Zig-zag x is a right child and p a left child (or the mirror)
Rotate at p first, then at g.
The same cases on real keys. Each example splays one key; the step list names the case at every step.
Step by step Zig, zig-zig and zig-zag on real keys
Insert the key as a new leaf, exactly as in a BST.
Splay the new node to the root.
If the key is already present, nothing is inserted, but that node is splayed.
2Algorithm
Algorithm SPLAY-INSERT(root, x)
1. If x is found at node t during the BST walk: SPLAY(t) and stop.
2. Attach a new node z with key x where the walk ended (BST insert).
3. SPLAY(z). (z becomes the root)
3Worked example
Step by step Insert into a splay tree
Sorted insertion builds a chain, and that is fine. Inserting 10, 20, 30, 40, 50 gives a left chain with 50 at the root, because each new key is splayed with one zig. The cost is paid back later: the first deep search roughly halves the chain.
4C code
void insert(int x) {
struct node *t = root, *y = NULL, *z;
while (t != NULL) {
y = t;
if (x == t->data) { splay(t); return; } /* already present */
t = x < t->data ? t->left : t->right;
}
z = malloc(sizeof *z);
z->data = x; z->left = z->right = NULL; z->parent = y;
if (y == NULL) root = z;
else if (x < y->data) y->left = z;
else y->right = z;
splay(z); /* new key becomes the root */
}
11.4Search
1Concept
Search as in a BST.
If the key is found, splay that node. If not, splay the last node reached (the node where the search fell off the tree).
After the search the answer is simply "is the root equal to x?".
Splaying even on a failed search matters: the search cost is always paid back by the restructuring, which is what the amortised bound needs.
2Algorithm
Algorithm SPLAY-SEARCH(root, x)
1. t = root, last = NULL
2. While t ≠ NULL:
last = t
if x = t.data: break
t = (x < t.data) ? t.left : t.right
3. If last ≠ NULL: SPLAY(last)
4. Return (root ≠ NULL and root.data = x)
3Worked example
Step by step Search and splay
4C code
int search(int x) {
struct node *t = root, *last = NULL;
while (t != NULL) {
last = t;
if (x == t->data) break;
t = x < t->data ? t->left : t->right;
}
if (last != NULL) splay(last); /* the key, or the last node reached */
return root != NULL && root->data == x;
}
11.5Delete
1Concept
Search for x. This splays it to the root (if it is missing, stop).
Remove the root. Its left subtree L and right subtree R become two separate trees. Every key in L is smaller than every key in R.
If L is empty, R is the answer. Otherwise splay the largest key of L to the root of L. It now has no right child.
Attach R as its right subtree (a join).
2Algorithm
Algorithm SPLAY-DELETE(root, x)
1. If not SPLAY-SEARCH(root, x): stop. (x is now the root)
2. L = root.left, R = root.right; free the root
3. If L = NULL: root = R; stop.
4. m = the rightmost node of L; SPLAY(m) inside L
5. m.right = R; root = m
3Worked example
Step by step Delete from a splay tree
4C code
void deleteKey(int x) {
struct node *L, *R, *m;
if (!search(x)) return; /* not found (last node was splayed) */
L = root->left; R = root->right; /* x is the root: cut it out */
free(root);
if (L == NULL) { root = R; if (R) R->parent = NULL; return; }
L->parent = NULL; root = L;
for (m = L; m->right != NULL; m = m->right) ; /* largest key in L */
splay(m); /* now m has no right child ... */
m->right = R; /* ... so hang R there */
if (R) R->parent = m;
}
11.6Amortised cost and comparison
Amortised O(log n) means: a single operation may be slow, but any sequence of m operations on a tree of up to n keys takes O(m log n) in total. A slow access always walks down a long path, and splaying that path roughly halves its depth, so it pays for itself. (The proof uses a "potential" equal to the sum of log(subtree size) over all nodes.)
AVL / red-black
Splay tree
Guarantee
O(log n) for every operation
O(log n) amortised; one operation can be O(n)
Extra data per node
Height or colour
None
Does a search change the tree?
No
Yes (the key moves to the root)
Repeated access to the same few keys
O(log n) each time
Close to O(1) after the first access
Code
Several cases for insert and delete
One splay routine used by every operation
11.7Real-life applications
Compilers
GCC's helper library libiberty provides a splay tree (splay-tree.h) that the compiler uses for some of its internal lookup tables.
Why splay: simple code, and recently used names are looked up again soon.
Operating system memory maps
FreeBSD keeps the list of memory regions of a process in a splay tree, so the region used by the last page fault is found almost at once on the next.
Why splay: memory accesses are strongly local.
Caches and recently used items
Any lookup table where the same few keys are asked for again and again (sessions, symbols, routes) benefits from keeping them at the top.
Why splay: frequent keys stay near the root without any counters.
Adaptive compression
Splay-tree codes change the code of each symbol as it is used, so frequent symbols get shorter codes on the fly (compare Huffman, which needs the frequencies in advance).
Why splay: self-adjusting without a first pass over the data.
11.8Interview and placement questions
Questions asked in viva and interviews
Q1What does "amortised O(log n)" mean for a splay tree?
The total time of any m operations is O(m log n), so the average per operation is O(log n), even though a single operation can take O(n). It is a guarantee over a sequence, not an average over random inputs.
Q2Why must zig-zig rotate the grandparent first?
Rotating the grandparent first folds the path so that its depth roughly halves. Rotating the parent first (plain "move to root") only moves x up and can leave a long path; a bad sequence then costs O(n) every time, and the amortised bound fails.
Q3Why splay even when a search fails?
The failed search still walked a long path. Splaying the last node reached shortens that path, so the cost is paid for. Without it, the same slow failed search could be repeated forever.
Q4When is a splay tree better than an AVL tree?
When accesses are skewed or repeat (a few hot keys, or keys accessed in order), because hot keys sit near the root. It also needs no height field and has simpler code. An AVL tree is better when every single operation must be fast or when many readers share the tree.
Q5Can two threads search a splay tree at the same time without locks?
No. A search rotates nodes, so it writes to the tree. Every access needs exclusive access, which is a real drawback for read-heavy concurrent programs.
Q6What is the worst case height of a splay tree, and when does it happen?
n − 1. Inserting keys in sorted order gives a chain (each new key is splayed with one zig). The next search for the deepest key costs O(n) but roughly halves the chain.
Coding problems
C1The new key becomes the rootEasySplay after insert
Insert a sequence into an empty splay tree and print the root after every insert, then the pre-order.
Idea. Insert exactly as in a BST, then splay the new node. After each insert the root is the key just inserted. Zig is one rotation at the root; zig-zig rotates the grandparent first; zig-zag rotates the parent first.
Time O(n) for this sorted-then-one-key sequence · Space O(n)
Show the C program and its output
/* Q1. After each splay-insert the new key is the root */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right, *parent; };
struct node *root = NULL;
int zig, zigzig, zigzag;
void rotate(struct node *x) {
struct node *p = x->parent, *g = p->parent;
if (x == p->left) { p->left = x->right; if (x->right) x->right->parent = p; x->right = p; }
else { p->right = x->left; if (x->left) x->left->parent = p; x->left = p; }
p->parent = x; x->parent = g;
if (g == NULL) root = x;
else if (g->left == p) g->left = x;
else g->right = x;
}
void splay(struct node *x) {
while (x->parent != NULL) {
struct node *p = x->parent, *g = p->parent;
if (g == NULL) { zig++; rotate(x); }
else if ((x == p->left) == (p == g->left)) { zigzig++; rotate(p); rotate(x); }
else { zigzag++; rotate(x); rotate(x); }
}
}
void insert(int x) {
struct node *t = root, *y = NULL, *z;
while (t != NULL) {
y = t;
if (x == t->data) { splay(t); return; }
t = x < t->data ? t->left : t->right;
}
z = malloc(sizeof *z);
z->data = x; z->left = z->right = NULL; z->parent = y;
if (y == NULL) root = z;
else if (x < y->data) y->left = z;
else y->right = z;
splay(z);
printf("insert %d -> root %d\n", x, root->data);
}
void preorder(struct node *r) { if (r) { printf("%d ", r->data); preorder(r->left); preorder(r->right); } }
int main(void) {
int keys[] = {10, 20, 30, 40, 50, 25}, i;
for (i = 0; i < 6; i++) insert(keys[i]);
printf("zig %d, zig-zig %d, zig-zag %d\n", zig, zigzig, zigzag);
printf("pre-order: "); preorder(root); printf("\n");
return 0;
}
C2Splay even when the search failsMediumSplay the last node
Build a splay tree, search for a missing key, and report the new root.
Idea. Walk down as in a BST. If the key is missing, splay the last node that was reached, not the root you started from. A later search near that key then starts near the top.
Time O(n) worst case, amortised O(log n) · Space O(1) extra besides the tree
Show the C program and its output
/* Q2. A failed search still splays the last node reached */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right, *parent; };
struct node *root = NULL;
void rotate(struct node *x) {
struct node *p = x->parent, *g = p->parent;
if (x == p->left) { p->left = x->right; if (x->right) x->right->parent = p; x->right = p; }
else { p->right = x->left; if (x->left) x->left->parent = p; x->left = p; }
p->parent = x; x->parent = g;
if (g == NULL) root = x;
else if (g->left == p) g->left = x;
else g->right = x;
}
void splay(struct node *x) {
while (x->parent != NULL) {
struct node *p = x->parent, *g = p->parent;
if (g == NULL) rotate(x);
else if ((x == p->left) == (p == g->left)) { rotate(p); rotate(x); }
else { rotate(x); rotate(x); }
}
}
void insert(int x) {
struct node *t = root, *y = NULL, *z;
while (t != NULL) {
y = t;
if (x == t->data) { splay(t); return; }
t = x < t->data ? t->left : t->right;
}
z = malloc(sizeof *z);
z->data = x; z->left = z->right = NULL; z->parent = y;
if (y == NULL) root = z;
else if (x < y->data) y->left = z;
else y->right = z;
splay(z);
}
int search(int x) {
struct node *t = root, *last = NULL;
while (t != NULL) {
last = t;
if (x == t->data) break;
t = x < t->data ? t->left : t->right;
}
if (last != NULL) splay(last);
return root != NULL && root->data == x;
}
void preorder(struct node *r) { if (r) { printf("%d ", r->data); preorder(r->left); preorder(r->right); } }
int main(void) {
int keys[] = {40, 20, 60, 10, 30, 50, 70}, i, found;
for (i = 0; i < 7; i++) insert(keys[i]);
found = search(55);
printf("search 55: %s, root is now %d\n", found ? "found" : "not found", root->data);
found = search(10);
printf("search 10: %s, root is now %d\n", found ? "found" : "not found", root->data);
printf("pre-order: "); preorder(root); printf("\n");
return 0;
}
Output
search 55: not found, root is now 50
search 10: found, root is now 10
pre-order: 10 30 20 50 40 60 70
11.9Lab, algorithm tracer and complete program
Insert, search and delete your own keys. A search moves the key (or the last node reached) to the root. The program below prints each zig, zig-zig and zig-zag. It was compared with the visualizer on 3000 random sequences of inserts, searches and deletes: the trees are identical.
Try it Splay tree lab
Algorithm tracer: splay insert, search and delete
Trace Splay algorithms, step by step
Program 10 · Splay tree: insert, search, delete (bottom-up splaying)
/* Splay tree: every search, insert and delete moves the accessed key to the root */
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *left, *right, *parent; };
struct node *root = NULL;
void rotate(struct node *x) { /* lift x one level above its parent p */
struct node *p = x->parent, *g = p->parent;
if (x == p->left) { p->left = x->right; if (x->right) x->right->parent = p; x->right = p; }
else { p->right = x->left; if (x->left) x->left->parent = p; x->left = p; }
p->parent = x; x->parent = g;
if (g == NULL) root = x;
else if (g->left == p) g->left = x;
else g->right = x;
}
void splay(struct node *x) {
struct node *p, *g;
while (x->parent != NULL) {
p = x->parent; g = p->parent;
if (g == NULL) { printf(" zig at %d\n", p->data); rotate(x); }
else if ((x == p->left) == (p == g->left)) { /* same side: straight line */
printf(" zig-zig at %d\n", g->data); rotate(p); rotate(x);
} else { /* opposite sides: zig-zag */
printf(" zig-zag at %d\n", g->data); rotate(x); rotate(x);
}
}
}
int search(int x) {
struct node *t = root, *last = NULL;
while (t != NULL) {
last = t;
if (x == t->data) break;
t = x < t->data ? t->left : t->right;
}
if (last != NULL) splay(last); /* splay the key, or the last node reached */
return root != NULL && root->data == x;
}
void insert(int x) {
struct node *t = root, *y = NULL, *z;
while (t != NULL) {
y = t;
if (x == t->data) { splay(t); return; }
t = x < t->data ? t->left : t->right;
}
z = malloc(sizeof *z);
z->data = x; z->left = z->right = NULL; z->parent = y;
if (y == NULL) root = z;
else if (x < y->data) y->left = z;
else y->right = z;
splay(z); /* the new key becomes the root */
}
void deleteKey(int x) {
struct node *L, *R, *m;
if (!search(x)) { printf("%d not found\n", x); return; }
L = root->left; R = root->right; /* x is now the root: cut it out */
free(root);
if (L == NULL) { root = R; if (R) R->parent = NULL; return; }
L->parent = NULL; root = L;
for (m = L; m->right != NULL; m = m->right) /* largest key of the left part */
;
splay(m); /* now it has no right child ... */
m->right = R; /* ... so hang the right part there */
if (R) R->parent = m;
}
void preorder(struct node *r) { if (r) { printf("%d ", r->data); preorder(r->left); preorder(r->right); } }
int main(void) {
int ch, k, i, x;
while (1) {
printf("\n1.Insert many 2.Search 3.Delete 4.Show 5.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &k); printf("Keys: ");
for (i = 0; i < k; i++) { scanf("%d", &x); insert(x); } break;
case 2: printf("Key: "); scanf("%d", &x);
printf(search(x) ? "Found; " : "Not found; ");
if (root) printf("root is now %d\n", root->data); else printf("the tree is empty\n"); break;
case 3: printf("Key: "); scanf("%d", &x); deleteKey(x); break;
case 4: printf("Pre-order: "); preorder(root); printf("\n"); break;
case 5: return 0;
}
}
return 0;
}
Sample run
1.Insert many 2.Search 3.Delete 4.Show 5.Exit
Choice: 1
How many? 5
Keys: 10 20 30 40 50
zig at 10
zig at 20
zig at 30
zig at 40
1.Insert many 2.Search 3.Delete 4.Show 5.Exit
Choice: 4
Pre-order: 50 40 30 20 10
1.Insert many 2.Search 3.Delete 4.Show 5.Exit
Choice: 2
Key: 10
zig-zig at 30
zig-zig at 50
Found; root is now 10
1.Insert many 2.Search 3.Delete 4.Show 5.Exit
Choice: 4
Pre-order: 10 40 20 30 50
1.Insert many 2.Search 3.Delete 4.Show 5.Exit
Choice: 3
Key: 30
zig-zag at 40
zig at 10
zig at 10
1.Insert many 2.Search 3.Delete 4.Show 5.Exit
Choice: 4
Pre-order: 20 10 40 50
1.Insert many 2.Search 3.Delete 4.Show 5.Exit
Choice: 5
Common mistakes in this part
In zig-zig, rotating at the parent first. The grandparent comes first.
Forgetting to splay after an unsuccessful search.
In delete, joining L and R without first splaying the largest key of L (it must have no right child).
Claiming O(log n) worst case per operation. It is amortised.
Key points to remember
Every access splays the node to the root: zig (parent is root), zig-zig (straight line, grandparent first), zig-zag (bend, parent first).
Insert: BST insert, then splay the new node. Search: splay the key or the last node reached.
Delete: splay x, remove it, splay the max of L, attach R.
O(log n) amortised, O(n) for a single operation, no extra field per node.
Part 12 · Beyond the syllabus
2-3 and 2-3-4 Trees
The smallest B-trees: every node holds one, two (or three) keys, and all leaves sit on the same level. They are the easiest way to see why B-trees stay balanced, and a 2-3-4 tree is exactly a red-black tree drawn differently.
Beyond the syllabus: the bridge between B-trees and red-black trees; asked in theory interviews.
12.1At a glance
A 2-3 tree is a B-tree of order 3: each internal node has 2 or 3 children (1 or 2 keys). A 2-3-4 tree is a B-tree of order 4: 2, 3 or 4 children (1 to 3 keys). All leaves are on one level.
Problem it solves
A BST gets its shape from the insertion order. A 2-3 tree never grows a long branch: it grows only at the root (by splitting), so every leaf is always at the same depth and the height is at most log2 n.
The rule
B-tree rules with m = 3 or 4: keys sorted in a node, k keys → k + 1 children, all leaves on one level. Overflow → split and push the middle key up.
Think of it as
Shelves that can hold 2 or 3 books: when a shelf would get too many, it splits in two and sends one book up to the shelf above.
Use it when
Learning balanced trees, and as the model behind red-black trees and B-trees.
Not the right tool when
Writing production code: nodes of different sizes are awkward in memory, so in practice a red-black tree (in memory) or a large-order B-tree (on disk) is used.
SearchO(log n)InsertO(log n)DeleteO(log n)Height (2-3)between log3 n and log2 n
Used inModel for red-black treesLeft-leaning red-black treesTeaching B-trees
A 2-3 tree is a B-tree of order 3: every internal node is a 2-node (1 key, 2 children) or a 3-node (2 keys, 3 children), and all leaves are on the same level. A 2-3-4 tree is a B-tree of order 4, which also allows 4-nodes (3 keys, 4 children).
Node
Keys
Children
Where the child keys go
2-node [a]
1
2
< a · > a
3-node [a b]
2
3
< a · between a and b · > b
4-node [a b c]
3
4
< a · a…b · b…c · > c (2-3-4 trees only)
How to read the 2-3 and 2-3-4 pictures
Each box is one node; the keys inside it are sorted. A box with k keys has k + 1 child lines.
The picture style is the same as on the B-tree page: amber = node being worked on, red = overflow or underflow, violet = the median moving up or a borrowed key.
Count the levels: every leaf is on the same one.
#define M 3 /* 3 for a 2-3 tree, 4 for a 2-3-4 tree */
struct bnode {
int n; /* keys in use: 1 .. M-1 */
int key[M]; /* one extra slot for a temporary overflow */
struct bnode *child[M + 1];
int leaf;
};
12.3Insert
1Concept
Walk down to the correct leaf and put the key there in sorted order.
If the leaf now has one key too many (3 keys in a 2-3 tree, 4 in a 2-3-4 tree), split it into two nodes and send the median key up to the parent.
The parent may overflow in turn: split it the same way. If the root splits, a new root is created and the height grows by one, at the top.
Which key moves up? In a 2-3 tree the overflowing node [a b c] sends up b. In a 2-3-4 tree the overflowing node [a b c d] has two middle keys; these notes and Program 4 send up the key at index ⌈m/2⌉ − 1 = 1, that is b. Many books instead split every 4-node on the way down (top-down insertion); both keep the tree balanced.
2Algorithm
Algorithm INSERT-23(root, x)
1. Find the leaf L where x belongs; insert x into L in sorted order.
2. While the node t has m keys (overflow):
med = t.key[⌈m/2⌉ − 1]
split t into Left (keys before med) and Right (keys after med)
insert med into t's parent with Left and Right as its children
(if t was the root, med forms a new root)
t = parent
3Worked example
Step by step Construct a 2-3 or 2-3-4 tree
4C code
This is Program 4 (B-tree) with M = 3 or 4. The split:
At a 2-node compare with one key, at a 3-node with two keys (at a 4-node with three), and go down the child whose range contains x. Stop when x is found or a leaf has been checked. At most one node per level is read, so the cost is O(height) = O(log n).
2Algorithm
Algorithm SEARCH-23(t, x)
1. While t ≠ NULL:
i = number of keys in t that are smaller than x
if i < t.n and t.key[i] = x: return FOUND
if t is a leaf: return NOT FOUND
t = t.child[i]
2. Return NOT FOUND
3Worked example
Step by step Search a 2-3 or 2-3-4 tree
4C code
int slot(struct bnode *t, int x) { int i = 0; while (i < t->n && x > t->key[i]) i++; return i; }
struct bnode *search(struct bnode *t, int x) {
int i;
while (t) {
i = slot(t, x);
if (i < t->n && t->key[i] == x) return t;
if (t->leaf) return NULL;
t = t->child[i];
}
return NULL;
}
12.5Delete
1Concept
If the key is in an internal node, replace it with its in-order successor (a leaf key) and delete that instead. So deletion always removes a key from a leaf.
A 2-3 leaf may not become empty (at least ⌈m/2⌉ − 1 = 1 key). If it does (underflow):
Borrow: if a neighbouring sibling has a spare key, the parent key comes down and the sibling's nearest key goes up.
Merge: otherwise the node, the parent key between them and the sibling become one node. The parent loses a key and may underflow in turn.
If the root loses its last key, its only child becomes the root: the height shrinks by one, at the top.
2Algorithm
Algorithm DELETE-23(root, x)
1. If x is in an internal node: replace it by its successor s and delete s from its leaf.
2. Remove the key from leaf t.
3. While t (not the root) has fewer than ⌈m/2⌉ − 1 keys:
if a sibling S has a spare key: borrow through the parent; stop
else: merge t + parent key + S into one node; t = parent
4. If the root has no keys: root = its only child.
3Worked example
Step by step Delete from a 2-3 or 2-3-4 tree
4C code
Deletion is the B-tree deletion of Program 4 (borrow and merge) with M = 3 or 4. fix(t, i) borrows from a sibling or merges, exactly as in the table above.
void deleteRec(struct bnode *t, int x) {
int i = slot(t, x);
struct bnode *s;
if (i < t->n && t->key[i] == x) {
if (t->leaf) { for (; i < t->n - 1; i++) t->key[i] = t->key[i + 1]; t->n--; return; }
s = t->child[i + 1]; /* internal: use the successor */
while (!s->leaf) s = s->child[0];
t->key[i] = s->key[0];
x = s->key[0]; i = i + 1;
} else if (t->leaf) return; /* not found */
deleteRec(t->child[i], x);
if (t->child[i]->n < MIN) fix(t, i); /* underflow: borrow or merge */
}
12.62-3-4 trees and red-black trees
A red-black tree is a 2-3-4 tree in which every 3-node and 4-node is drawn as a small group of binary nodes: one black node with its red children.
2-3-4 node
Red-black form
2-node [b]
One black node b
3-node [a b]
Black b with red left child a (or black a with red right child b)
4-node [a b c]
Black b with red children a and c
Pick a key set: the 2-3-4 tree is built by B-tree insertion (order 4), and every node is redrawn as a black node with its red children.
Compare 2-3-4 tree and its red-black form
All 2-3-4 leaves on one level ⇔ equal black-height in the red-black tree.
No 5-nodes ⇔ no red node with a red child.
Red-black insertion case 1 (recolour) is a 4-node split: the middle (grandparent) key goes up. Cases 2 and 3 (rotations) only re-draw a 3-node as a 4-node.
Sedgewick's left-leaning red-black tree restricts red links to the left, which makes it a 2-3 tree in disguise and shortens the code.
12.7Real-life applications
The design behind red-black trees
Every red-black tree in std::map, TreeMap or the Linux kernel is a 2-3-4 tree stored as binary nodes; its cases are easiest to understand as 2-3-4 splits and merges.
Why 2-3-4: simple balance rule, binary storage.
Small in-memory B-trees
The same split and merge code with a small order is sometimes used for sorted in-memory indexes, where a node fits in one cache line.
Why: all leaves on one level, few pointers followed.
Learning B-trees
Every B-tree idea (split at the median, grow at the root, borrow, merge) can be done by hand on a 2-3 tree in a few minutes, so it is the standard classroom model.
Why 2-3: the smallest tree with all the B-tree cases.
12.8Interview and placement questions
Questions asked in viva and interviews
Q1What are the minimum and maximum heights of a 2-3 tree with n keys?
All 3-nodes (2 keys each) give the shortest tree: n = 3h+1 − 1, so h ≈ log3(n + 1) − 1. All 2-nodes give the tallest: a perfect binary tree, h = log2(n + 1) − 1. Either way O(log n).
Q2Why does a 2-3 tree stay perfectly balanced?
New keys go into leaves and the tree only gets taller when the root splits, which adds one level above every leaf at once. Deletion shrinks it only when the root disappears. So all leaves always stay on the same level.
Q3How does a 2-3-4 tree correspond to a red-black tree?
Each 2-node is a black node, each 3-node a black node with one red child, each 4-node a black node with two red children. The black-height of the red-black tree is the height of the 2-3-4 tree plus one.
Q4What is top-down insertion in a 2-3-4 tree?
While walking down, split every 4-node you meet. Then the leaf reached always has room, and no split ever has to travel back up. It needs only one pass down the tree, which is useful when many threads work on the tree at once.
Q5Why not use 2-3 trees directly in libraries?
Nodes of different sizes (1 or 2 keys, 2 or 3 children) make the code full of special cases and waste memory in 2-nodes. The red-black encoding keeps the same balance with uniform binary nodes and one colour bit.
Coding problems
C1Split an overflowing nodeEasyMedian moves up
A 2-3 node that receives a third key, and a 2-3-4 node that receives a fourth key, must split. Print the key that moves up and the two nodes left behind.
Idea. Order m allows at most m − 1 keys. The key that moves up sits at index (m + 1) / 2 − 1 (integer division). For order 3 that is the middle of three keys. For order 4 that is the left-middle of four keys.
Time O(m) · Space O(1)
Show the C program and its output
/* Q1. Split an overflowing 2-3 node and an overflowing 2-3-4 node */
#include <stdio.h>
void split(int m, int *key, int n) {
int mid = (m + 1) / 2 - 1, i;
printf("order %d, overflow [", m);
for (i = 0; i < n; i++) printf("%d%s", key[i], i + 1 < n ? " " : "");
printf("]\n median %d moves up\n left [", key[mid]);
for (i = 0; i < mid; i++) printf("%d%s", key[i], i + 1 < mid ? " " : "");
printf("]\n right [");
for (i = mid + 1; i < n; i++) printf("%d%s", key[i], i + 1 < n ? " " : "");
printf("]\n");
}
int main(void) {
int a[] = {10, 20, 30}; /* 2-3 node that just received a third key */
int b[] = {10, 20, 25, 30}; /* 2-3-4 node that just received a fourth key */
split(3, a, 3);
split(4, b, 4);
return 0;
}
Output
order 3, overflow [10 20 30]
median 20 moves up
left [10]
right [30]
order 4, overflow [10 20 25 30]
median 20 moves up
left [10]
right [25 30]
C2Name each node, and the red-black pictureEasy2-node, 3-node, 4-node
Walk a small 2-3-4 tree and label every node by how many keys it holds.
Idea. 1 key is a 2-node (one black node), 2 keys a 3-node (a black node with one red child), 3 keys a 4-node (a black node with two red children). Leaves are classified the same way: a leaf is still a 2-node, 3-node or 4-node.
Time O(n) · Space O(h)
Show the C program and its output
/* Q2. Classify each node of a 2-3-4 tree and name the red-black picture */
#include <stdio.h>
struct node { int n; int key[3]; struct node *c[4]; };
const char *kind(int keys) {
if (keys == 1) return "2-node -> one black node";
if (keys == 2) return "3-node -> black node + one red child";
if (keys == 3) return "4-node -> black node + two red children";
return "?";
}
void show(struct node *t, int level) {
int i;
if (!t) return;
printf("level %d [", level);
for (i = 0; i < t->n; i++) printf("%d%s", t->key[i], i + 1 < t->n ? " " : "");
printf("] %s\n", kind(t->n));
for (i = 0; i < 4; i++) show(t->c[i], level + 1);
}
int main(void) {
struct node L = {3, {10, 20, 25}, {0}}, M = {1, {40}, {0}}, R = {2, {60, 70}, {0}};
struct node root = {2, {30, 50}, {&L, &M, &R, 0}};
printf("2-3-4 tree (order 4), all leaves on one level:\n");
show(&root, 0);
return 0;
}
Output
2-3-4 tree (order 4), all leaves on one level:
level 0 [30 50] 3-node -> black node + one red child
level 1 [10 20 25] 4-node -> black node + two red children
level 1 [40] 2-node -> one black node
level 1 [60 70] 3-node -> black node + one red child
12.9Lab and program
Choose order 3 for a 2-3 tree or order 4 for a 2-3-4 tree. The complete program is Program 4: set #define M 3 or #define M 4.
Try it 2-3 / 2-3-4 tree lab
Common mistakes in this part
Inserting a new key into an internal node. New keys always go into a leaf.
Splitting a 2-3 node that has only 2 keys. A 2-3 node splits only when it gets a third key.
Adding a new level at the bottom. The height grows only at the root.
Calling a 3-node "a node with 3 keys". A 3-node has 3 children and 2 keys.
Key points to remember
2-3 tree = B-tree of order 3; 2-3-4 tree = B-tree of order 4. The name lists the allowed numbers of children.
Insert into a leaf; overflow → split, median up; root split → height + 1.
Delete from a leaf; underflow → borrow, else merge; empty root → height − 1.
2-3-4 tree ⇔ red-black tree: a black node with its red children is one 2-3-4 node.
Part 13 · Beyond the syllabus
B* Tree
A B-tree that keeps every node at least two-thirds full instead of half full. Before splitting, a full node first passes keys to a sibling; when both are full, two nodes become three. Fuller nodes mean fewer nodes, a shorter tree and fewer disk reads.
Beyond the syllabus: appears as a short note or a "B-tree vs B+ vs B*" question.
13.1At a glance
A B* tree is a B-tree in which every non-root node is at least two-thirds full. Overflow is handled by redistribution to a sibling first, and by a 2-to-3 split only when the sibling is full too.
Problem it solves
A B-tree split leaves two half-empty nodes, so on average B-tree nodes are only about 69% full. Wasted space means more nodes, more levels and more disk reads. B* trees keep nodes about 81% full on average.
The rule
B-tree rules, but a non-root node needs at least ⌈(2m − 1)/3⌉ children (about 2/3 of m). Overflow → shift to a sibling; both full → split 2 nodes into 3.
Think of it as
Packing boxes on a shelf: when one box is too full, first push items into the next box; only when both are full, repack the two into three.
Use it when
Disk space and read cost matter more than insert speed: large, read-mostly indexes.
Not the right tool when
Inserts are very frequent (redistribution and 2-to-3 splits touch more nodes), or range scans dominate (use a B+ tree).
A B* tree of order m is a B-tree in which every node other than the root has at least ⌈(2m − 1)/3⌉ children, so it is at least about two-thirds full. When a node overflows, keys are first redistributed to an adjacent sibling; only when that sibling is also full are the two full nodes split into three.
Order m
Max keys (m − 1)
B-tree min keys (⌈m/2⌉ − 1)
B* tree min keys (⌈(2m − 1)/3⌉ − 1)
B* root max keys (2⌊(2m − 2)/3⌋)
4
3
1
2
4
7
6
3
4
8
10
9
4
6
12
All leaves are on the same level, and keys in a node are sorted, as in every B-tree.
The root has no sibling, so it cannot redistribute. It may hold up to 2⌊(2m − 2)/3⌋ keys (4 for order 4) and then splits into two halves, each exactly at the B* minimum. This is Knuth's rule; it keeps every non-root node at least two-thirds full from the start.
How to read the B* pictures
Boxes are nodes with sorted keys, as on the B-tree page. Most examples use order 4: at most 3 keys and at least 2 keys in every node below the root; the root may hold 1 to 4 keys.
Red = overflow or underflow, violet = the sibling taking part, green key = a key that moved up or down through the parent.
#define M 4 /* order: at most M-1 = 3 keys in a node */
#define MIN 2 /* at least ceil((2M-1)/3) - 1 = 2 keys */
struct node { int n, key[M]; /* children as in a B-tree */ };
13.3Insert: redistribution and the 2-to-3 split
1Concept
Insert the key into the correct leaf, as in a B-tree.
If the leaf overflows and an adjacent sibling has room (try the right one first, then the left): the separator comes down into the sibling and the overflowing node's nearest key goes up in its place (redistribution). No new node is made.
If the siblings are full too: take all keys of the node, one full sibling and the separator between them, and share them among three nodes; two separators go up to the parent (2-to-3 split). Each of the three nodes is about two-thirds full.
The parent gained a key and may overflow in turn: repeat there. The root has no sibling: when it passes its limit it splits into two.
2Algorithm
Algorithm BSTAR-INSERT(root, x)
1. Insert x into its leaf L.
2. While L overflows:
If L is the root: split it into two halves, middle key up as the new root; stop.
K = separator between L and its sibling S in the parent
If the right sibling S has fewer than m − 1 keys:
move K down into S; move L's largest key up in place of K; stop
Else if the left sibling S has fewer than m − 1 keys:
move K down into S; move L's smallest key up in place of K; stop
Else:
pool = keys of L + K + keys of S (2m keys in total)
share the pool among L, S and a new node N, with 2 separators up
L = the parent
3Worked example
Pick an example: the first three show the three cases on small trees; the others build whole trees. Choose Your own input to try any keys and orders 4 to 7.
Step by step Insert into a B* tree
Compare with a B-tree of order 4: inserting 35 into [10 20 30] splits it at once into [10] and [30 35] with 20 going up: two nodes only one-third and two-thirds full. The B* tree keeps every node at 2 or 3 keys.
4C code
From Program 11. shiftLeft, splitRoot and fill are in the complete program.
void shiftRight(struct node *L, int *sep, struct node *R) { /* L gives its largest key, through the parent */
int j;
for (j = R->n; j > 0; j--) R->key[j] = R->key[j - 1];
for (j = R->n + 1; j > 0; j--) R->child[j] = R->child[j - 1];
R->key[0] = *sep; R->child[0] = L->child[L->n]; L->child[L->n] = NULL; R->n++;
*sep = L->key[--L->n];
}
void split2to3(struct node *p, int a) { /* children a and a+1 of p become three nodes */
struct node *A = p->child[a], *B = p->child[a + 1], *N = newNode(), *ch[6 * MAXM];
int all[4 * MAXM], t = 0, nc = 0, i, s1, s2, s3, k = 0, c = 0, up1, up2;
for (i = 0; i < A->n; i++) all[t++] = A->key[i];
all[t++] = p->key[a];
for (i = 0; i < B->n; i++) all[t++] = B->key[i];
for (i = 0; i <= A->n; i++) ch[nc++] = A->child[i];
for (i = 0; i <= B->n; i++) ch[nc++] = B->child[i];
s1 = (t - 2) / 3; s2 = (t - 2 - s1) / 2; s3 = t - 2 - s1 - s2;
fill(A, all, ch, &k, &c, s1); up1 = all[k++];
fill(B, all, ch, &k, &c, s2); up2 = all[k++];
fill(N, all, ch, &k, &c, s3);
for (i = p->n; i > a + 1; i--) p->key[i] = p->key[i - 1];
for (i = p->n + 1; i > a + 2; i--) p->child[i] = p->child[i - 1];
p->key[a] = up1; p->key[a + 1] = up2; p->child[a + 2] = N; p->n++;
printf(" 2-to-3 split: "); printNode(A); printf(" %d ", up1); printNode(B); printf(" %d ", up2); printNode(N); printf("\n");
}
void insert(int x) {
struct node *path[64], *n = root, *p, *L, *R;
int top = 0, i, j, ci;
if (root == NULL) { root = newNode(); root->key[0] = x; root->n = 1; return; }
while (1) {
i = 0; while (i < n->n && x > n->key[i]) i++;
if (i < n->n && n->key[i] == x) { printf(" %d already exists\n", x); return; }
if (isLeaf(n)) break;
path[top++] = n; n = n->child[i];
}
for (j = n->n; j > i; j--) n->key[j] = n->key[j - 1];
n->key[i] = x; n->n++;
while (n->n > maxKeys(n)) { /* overflow */
if (n == root) { splitRoot(); return; }
p = path[--top]; ci = indexIn(p, n);
R = ci < p->n ? p->child[ci + 1] : NULL;
L = ci > 0 ? p->child[ci - 1] : NULL;
if (R != NULL && R->n < M - 1) { shiftRight(n, &p->key[ci], R); printf(" redistribute to the right sibling: %d goes up\n", p->key[ci]); return; }
if (L != NULL && L->n < M - 1) { shiftLeft(L, &p->key[ci - 1], n); printf(" redistribute to the left sibling: %d goes up\n", p->key[ci - 1]); return; }
split2to3(p, R != NULL ? ci : ci - 1);
n = p; /* the parent gained a key */
}
}
13.4Search
1Concept
Exactly the B-tree search: scan the sorted keys of a node, stop if x is there, otherwise go down the child whose range contains x. Because B* nodes are fuller, the tree is often one level shorter for the same data, so a search reads fewer nodes.
2Algorithm
Algorithm BSTAR-SEARCH(t, x)
1. While t ≠ NULL:
i = number of keys in t smaller than x
if i < t.n and t.key[i] = x: return FOUND
t = t.child[i] (NULL below a leaf)
2. Return NOT FOUND
3Worked example
Step by step Search a B* tree
4C code
int search(int x) {
struct node *t = root; int i;
while (t != NULL) {
i = 0; while (i < t->n && x > t->key[i]) i++;
if (i < t->n && t->key[i] == x) return 1;
if (isLeaf(t)) return 0;
t = t->child[i];
}
return 0;
}
13.5Delete: borrow and the 3-to-2 merge
1Concept
Delete as in a B-tree (an internal key is replaced by its successor, so a key always leaves a leaf).
If the node drops below the minimum (underflow) and an adjacent sibling has a spare key: borrow it through the parent (left sibling first, then right).
If neither adjacent sibling can lend: take the node, two neighbours and the two separators between them. If the third node has a spare key, share the keys among the three (a borrow across two nodes). Otherwise merge the three nodes into two; the parent loses one separator and may underflow in turn.
If the parent has only two children (only the root can), the two merge into one. When the root loses its last key, its only child becomes the root.
2Algorithm
Algorithm BSTAR-DELETE(root, x)
1. Remove x from its leaf L (via the successor if x is internal).
2. While L is not the root and L has too few keys:
If the left or right sibling has more than the minimum:
borrow one key through the parent; stop
If the parent has only 2 children:
merge both children and the separator into one node; L = the parent
Else:
pool = L, two neighbours and the 2 separators between them
If the pool can fill three nodes: share it among the three; stop
Else: share it between 2 nodes with 1 separator; L = the parent
3. If the root has no keys: its only child becomes the root.
3Worked example
Every case has its own example: borrow from either side, sharing among three, the 3-to-2 merge, an internal key, and the merge that shrinks the tree.
Step by step Delete from a B* tree
4C code
From Program 11. shiftLeft (borrow from the right), fill and deleteKey are in the complete program.
void borrowLeft(struct node *L, int *sep, struct node *n) { /* L lends its largest key */
int j;
for (j = n->n; j > 0; j--) n->key[j] = n->key[j - 1];
for (j = n->n + 1; j > 0; j--) n->child[j] = n->child[j - 1];
n->key[0] = *sep; n->child[0] = L->child[L->n]; L->child[L->n] = NULL; n->n++;
*sep = L->key[--L->n];
}
void delete(int x) {
struct node *path[64], *n = root, *p, *L, *R, *X, *Y, *Z, *host, *ch[9 * MAXM];
int top = 0, i, j, ci, w, t, nc, k, c, all[6 * MAXM], mk = minKeys(), s1, s2, s3, up1, up2, hi;
while (n != NULL) {
i = 0; while (i < n->n && x > n->key[i]) i++;
if (i < n->n && n->key[i] == x) break;
if (isLeaf(n)) n = NULL; else { path[top++] = n; n = n->child[i]; }
}
if (n == NULL) { printf(" %d not found\n", x); return; }
if (!isLeaf(n)) { /* internal key: use the in-order successor */
host = n; hi = i; path[top++] = n; n = n->child[i + 1];
while (!isLeaf(n)) { path[top++] = n; n = n->child[0]; }
host->key[hi] = n->key[0]; i = 0;
printf(" %d is internal: replaced by its successor %d\n", x, n->key[0]);
}
for (j = i; j < n->n - 1; j++) n->key[j] = n->key[j + 1];
n->n--;
while (n != root && n->n < mk) { /* underflow */
p = path[--top]; ci = indexIn(p, n);
L = ci > 0 ? p->child[ci - 1] : NULL;
R = ci < p->n ? p->child[ci + 1] : NULL;
if (L != NULL && L->n > mk) { borrowLeft(L, &p->key[ci - 1], n); printf(" borrow from the left sibling\n"); return; }
if (R != NULL && R->n > mk) { shiftLeft(n, &p->key[ci], R); printf(" borrow from the right sibling\n"); return; }
if (p->n == 1) { /* only two children: merge them into one */
X = p->child[0]; Y = p->child[1];
X->key[X->n] = p->key[0];
for (j = 0; j < Y->n; j++) X->key[X->n + 1 + j] = Y->key[j];
for (j = 0; j <= Y->n; j++) X->child[X->n + 1 + j] = Y->child[j];
X->n += Y->n + 1; free(Y);
p->n = 0; p->child[1] = NULL;
printf(" 2-to-1 merge: "); printNode(X); printf("\n");
n = p; continue;
}
w = ci == 0 ? 0 : (ci == p->n ? ci - 2 : ci - 1);
X = p->child[w]; Y = p->child[w + 1]; Z = p->child[w + 2];
t = 0; nc = 0;
for (j = 0; j < X->n; j++) all[t++] = X->key[j];
all[t++] = p->key[w];
for (j = 0; j < Y->n; j++) all[t++] = Y->key[j];
all[t++] = p->key[w + 1];
for (j = 0; j < Z->n; j++) all[t++] = Z->key[j];
for (j = 0; j <= X->n; j++) ch[nc++] = X->child[j];
for (j = 0; j <= Y->n; j++) ch[nc++] = Y->child[j];
for (j = 0; j <= Z->n; j++) ch[nc++] = Z->child[j];
k = 0; c = 0;
if (t - 2 >= 3 * mk) { /* enough keys: share among the three */
s1 = (t - 2) / 3; s2 = (t - 2 - s1) / 2; s3 = t - 2 - s1 - s2;
fill(X, all, ch, &k, &c, s1); up1 = all[k++];
fill(Y, all, ch, &k, &c, s2); up2 = all[k++];
fill(Z, all, ch, &k, &c, s3);
p->key[w] = up1; p->key[w + 1] = up2;
printf(" share among three nodes\n");
return;
}
s1 = (t - 1) / 2; /* 3-to-2 merge */
fill(X, all, ch, &k, &c, s1); up1 = all[k++];
fill(Y, all, ch, &k, &c, t - 1 - s1);
free(Z);
p->key[w] = up1; deleteKey(p, w + 1, w + 2);
printf(" 3-to-2 merge: "); printNode(X); printf(" %d ", up1); printNode(Y); printf("\n");
n = p;
}
if (root->n == 0) {
n = root;
root = isLeaf(n) ? NULL : n->child[0];
free(n);
if (root != NULL) printf(" root removed: height - 1\n");
}
}
13.6B-tree vs B+ tree vs B* tree
B-tree
B+ tree
B* tree
Minimum fill of a non-root node
1/2
1/2
2/3
Where data lives
Every node
Leaves only (linked)
Every node
On overflow
Split 1 → 2
Split 1 → 2 (leaf: copy up)
Shift to a sibling; else split 2 → 3
On underflow
Borrow, else merge 2 → 1
Borrow, else merge 2 → 1
Borrow, else merge 3 → 2
Space use
About 69% on average
About 69% on average
About 81% on average
Strength
Simple, search can stop early
Range scans, high fan-out
Fewer nodes, shorter tree
Cost
—
Keys repeated in the index
Inserts touch more nodes
The ideas combine: a B+ tree can also use B*-style redistribution and 2-to-3 splits to keep its leaves fuller.
13.7Real-life applications
Space-efficient disk indexes
When an index holds billions of keys, raising the average page fill from about 69% to about 81% saves many pages and can remove a whole level.
Why B*: fuller pages, fewer reads per lookup.
Read-mostly databases
Tables that are loaded once and then mostly queried pay the extra insert work rarely and gain on every read.
Why B*: slower inserts, faster and smaller reads.
Leaf redistribution in B+ trees
Many B+ tree implementations borrow the B* rule of trying a sibling before splitting, so their leaves stay fuller.
Why: the same benefit, applied to the leaf level.
13.8Interview and placement questions
Questions asked in viva and interviews
Q1What is the main difference between a B-tree and a B* tree?
Minimum fill: 1/2 for a B-tree, 2/3 for a B* tree. To keep it, a B* tree redistributes to a sibling before splitting, and splits two full nodes into three instead of one into two.
Q2Why split two nodes into three, and not one into two?
Splitting one full node gives two half-full nodes, which breaks the 2/3 rule. Two full nodes hold about 2m keys; shared among three nodes, each gets about 2m/3, which is two-thirds full.
Q3What is the minimum number of keys in a non-root node of a B* tree of order 7?
Children ≥ ⌈(2 × 7 − 1)/3⌉ = ⌈13/3⌉ = 5, so keys ≥ 4 (a B-tree of order 7 needs only 3).
Q4What does a B* tree give up in return for fuller nodes?
Insert and delete are more complex and touch more nodes (a sibling and the parent), so updates are slower. Search is the same algorithm but usually reads fewer nodes.
Coding problems (tested C programs)
C1B* leaf operations: redistribute, 2-to-3 split, borrow, 3-to-2 mergeMediumPool the keys, share them out
Carry out the four B* operations on the leaf level of a tree of order 4.
Idea. Every B* restructuring does the same thing: collect the keys of the nodes involved together with the separators between them, in order, then cut this list into the required number of nodes, sending the keys at the cut points up as separators.
Time O(m) per operation · Space O(m)
Show the C program and its output
/* Q1. B* tree leaf operations (order M = 4): redistribution, 2-to-3 split, borrow, 3-to-2 merge */
#include <stdio.h>
#define M 4 /* at most M-1 = 3 keys in a node */
#define MIN 2 /* at least 2 keys: two-thirds full */
struct node { int n, key[M]; }; /* one spare slot for an overflow */
void show(const char *name, struct node *t) {
int i; printf("%s[", name);
for (i = 0; i < t->n; i++) printf(i ? " %d" : "%d", t->key[i]);
printf("] ");
}
void insertSorted(struct node *t, int x) {
int j = t->n;
while (j > 0 && t->key[j - 1] > x) { t->key[j] = t->key[j - 1]; j--; }
t->key[j] = x; t->n++;
}
/* L overflowed and its right sibling R has room: pass one key across the parent */
void shiftRight(struct node *L, int *sep, struct node *R) {
int j;
for (j = R->n; j > 0; j--) R->key[j] = R->key[j - 1];
R->key[0] = *sep; R->n++; /* the separator comes down into R */
*sep = L->key[--L->n]; /* the largest key of L goes up */
}
/* L overflowed and R is full: make three nodes, each about two-thirds full */
void split2to3(struct node *L, int sep, struct node *R, struct node *N, int up[2]) {
int all[2 * M + 1], t = 0, i, a, b, k = 0;
for (i = 0; i < L->n; i++) all[t++] = L->key[i];
all[t++] = sep;
for (i = 0; i < R->n; i++) all[t++] = R->key[i];
a = (t - 2) / 3; b = (t - 2 - a) / 2; /* sizes of the first two nodes */
L->n = a; for (i = 0; i < a; i++) L->key[i] = all[k++];
up[0] = all[k++];
R->n = b; for (i = 0; i < b; i++) R->key[i] = all[k++];
up[1] = all[k++];
N->n = t - k; for (i = 0; k < t; i++) N->key[i] = all[k++];
}
/* L underflowed and its right sibling R can spare a key: borrow across the parent */
void borrowRight(struct node *L, int *sep, struct node *R) {
int j;
L->key[L->n++] = *sep; /* the separator comes down into L */
*sep = R->key[0]; /* R's smallest key goes up */
for (j = 0; j < R->n - 1; j++) R->key[j] = R->key[j + 1];
R->n--;
}
/* A underflowed and no sibling can lend: three nodes become two */
void merge3to2(struct node *A, int s1, struct node *B, int s2, struct node *C, int *up) {
int all[3 * M], t = 0, i, a, k = 0;
for (i = 0; i < A->n; i++) all[t++] = A->key[i];
all[t++] = s1;
for (i = 0; i < B->n; i++) all[t++] = B->key[i];
all[t++] = s2;
for (i = 0; i < C->n; i++) all[t++] = C->key[i];
a = (t - 1) / 2; /* keys in the first node */
A->n = a; for (i = 0; i < a; i++) A->key[i] = all[k++];
*up = all[k++]; /* the one separator left */
B->n = t - k; for (i = 0; k < t; i++) B->key[i] = all[k++];
C->n = 0; /* C is freed */
}
void removeKey(struct node *t, int x) {
int i = 0, j;
while (i < t->n && t->key[i] != x) i++;
for (j = i; j < t->n - 1; j++) t->key[j] = t->key[j + 1];
t->n--;
}
int main(void) {
struct node L = {3, {10, 20, 30}}, R = {2, {50, 60}}, L2 = {3, {10, 20, 30}}, R2 = {3, {50, 60, 70}}, N;
int sep = 40, up[2];
printf("Case 1: parent [40], children [10 20 30] [50 60]; insert 35\n ");
insertSorted(&L, 35); show("overflow ", &L); printf("\n ");
shiftRight(&L, &sep, &R);
printf("parent [%d] ", sep); show("", &L); show("", &R); printf(" (no split)\n");
printf("Case 2: parent [40], children [10 20 30] [50 60 70]; insert 35\n ");
insertSorted(&L2, 35); show("overflow ", &L2); printf("\n ");
split2to3(&L2, 40, &R2, &N, up);
printf("parent [%d %d] ", up[0], up[1]); show("", &L2); show("", &R2); show("", &N); printf("\n");
{
struct node A = {2, {10, 20}}, B = {3, {35, 40, 45}}, C = {2, {60, 70}};
struct node A2 = {2, {10, 20}}, B2 = {2, {35, 40}}, C2 = {2, {60, 70}};
int s1 = 30, one;
printf("Case 3: parent [30 50], children [10 20] [35 40 45] [60 70]; delete 20\n ");
removeKey(&A, 20); show("underflow ", &A); printf("\n ");
if (B.n > MIN) borrowRight(&A, &s1, &B);
printf("parent [%d 50] ", s1); show("", &A); show("", &B); show("", &C); printf("\n");
printf("Case 4: parent [30 50], children [10 20] [35 40] [60 70]; delete 20\n ");
removeKey(&A2, 20); show("underflow ", &A2); printf("\n ");
merge3to2(&A2, 30, &B2, 50, &C2, &one);
printf("parent [%d] ", one); show("", &A2); show("", &B2); printf("\n");
}
return 0;
}
Insert, search and delete any keys; the lab checks the B* rules after every step. Orders 4 to 7.
Try it B* tree lab
Algorithm tracer: B* insert, search and delete
Trace B* tree algorithms, step by step
The program asks for the order, then prints every redistribution, split, borrow and merge as it happens. Option 2 prints the tree level by level and checks every B* rule. It was compared with the visualizer above on 1900 random insert and delete sequences (orders 3 to 7): the trees were identical after every step and all rules held.
Program 11 · B* tree: insert, search, delete, level print and rule check
/* B* tree of order M: insert with redistribution and the 2-to-3 split,
delete with borrowing and the 3-to-2 merge, search, level print and rule check */
#include <stdio.h>
#include <stdlib.h>
#define MAXM 8 /* largest order this program accepts */
struct node {
int n; /* number of keys */
int key[2 * MAXM]; /* room for a temporary overflow */
struct node *child[2 * MAXM + 1];
};
struct node *root = NULL;
int M = 4; /* order, read at the start */
int minKeys(void) { return (2 * M + 1) / 3 - 1; } /* ceil((2M-1)/3) - 1 */
int rootMax(void) { return 2 * ((2 * M - 2) / 3); } /* Knuth: the root may be a little bigger */
int maxKeys(struct node *n) { return n == root ? rootMax() : M - 1; }
int isLeaf(struct node *n) { return n->child[0] == NULL; }
struct node *newNode(void) { return calloc(1, sizeof(struct node)); }
int indexIn(struct node *p, struct node *c) { int i = 0; while (p->child[i] != c) i++; return i; }
void printNode(struct node *n) { int i; printf("["); for (i = 0; i < n->n; i++) printf(i ? " %d" : "%d", n->key[i]); printf("]"); }
int search(int x) {
struct node *t = root; int i;
while (t != NULL) {
i = 0; while (i < t->n && x > t->key[i]) i++;
if (i < t->n && t->key[i] == x) return 1;
if (isLeaf(t)) return 0;
t = t->child[i];
}
return 0;
}
/* ---- insert ---- */
void shiftRight(struct node *L, int *sep, struct node *R) { /* L gives its largest key, through the parent */
int j;
for (j = R->n; j > 0; j--) R->key[j] = R->key[j - 1];
for (j = R->n + 1; j > 0; j--) R->child[j] = R->child[j - 1];
R->key[0] = *sep; R->child[0] = L->child[L->n]; L->child[L->n] = NULL; R->n++;
*sep = L->key[--L->n];
}
void shiftLeft(struct node *L, int *sep, struct node *R) { /* R gives its smallest key, through the parent */
int j;
L->key[L->n] = *sep; L->child[L->n + 1] = R->child[0]; L->n++;
*sep = R->key[0];
for (j = 0; j < R->n - 1; j++) R->key[j] = R->key[j + 1];
for (j = 0; j < R->n; j++) R->child[j] = R->child[j + 1];
R->child[R->n] = NULL; R->n--;
}
/* fill node d with cnt keys from all[] (and cnt+1 children from ch[]) */
void fill(struct node *d, int *all, struct node **ch, int *k, int *c, int cnt) {
int i;
for (i = 0; i <= 2 * MAXM; i++) d->child[i] = NULL;
for (i = 0; i < cnt; i++) d->key[i] = all[(*k)++];
for (i = 0; i <= cnt; i++) d->child[i] = ch[(*c)++];
d->n = cnt;
}
void split2to3(struct node *p, int a) { /* children a and a+1 of p become three nodes */
struct node *A = p->child[a], *B = p->child[a + 1], *N = newNode(), *ch[6 * MAXM];
int all[4 * MAXM], t = 0, nc = 0, i, s1, s2, s3, k = 0, c = 0, up1, up2;
for (i = 0; i < A->n; i++) all[t++] = A->key[i];
all[t++] = p->key[a];
for (i = 0; i < B->n; i++) all[t++] = B->key[i];
for (i = 0; i <= A->n; i++) ch[nc++] = A->child[i];
for (i = 0; i <= B->n; i++) ch[nc++] = B->child[i];
s1 = (t - 2) / 3; s2 = (t - 2 - s1) / 2; s3 = t - 2 - s1 - s2;
fill(A, all, ch, &k, &c, s1); up1 = all[k++];
fill(B, all, ch, &k, &c, s2); up2 = all[k++];
fill(N, all, ch, &k, &c, s3);
for (i = p->n; i > a + 1; i--) p->key[i] = p->key[i - 1];
for (i = p->n + 1; i > a + 2; i--) p->child[i] = p->child[i - 1];
p->key[a] = up1; p->key[a + 1] = up2; p->child[a + 2] = N; p->n++;
printf(" 2-to-3 split: "); printNode(A); printf(" %d ", up1); printNode(B); printf(" %d ", up2); printNode(N); printf("\n");
}
void splitRoot(void) {
struct node *old = root, *L = newNode(), *R = newNode();
int t = old->n, mid = (t - 1) / 2, i;
for (i = 0; i < mid; i++) L->key[i] = old->key[i];
for (i = 0; i <= mid; i++) L->child[i] = old->child[i];
L->n = mid;
for (i = mid + 1; i < t; i++) R->key[i - mid - 1] = old->key[i];
for (i = mid + 1; i <= t; i++) R->child[i - mid - 1] = old->child[i];
R->n = t - mid - 1;
root = newNode(); root->key[0] = old->key[mid]; root->child[0] = L; root->child[1] = R; root->n = 1;
printf(" root split: %d is the new root\n", old->key[mid]);
free(old);
}
void insert(int x) {
struct node *path[64], *n = root, *p, *L, *R;
int top = 0, i, j, ci;
if (root == NULL) { root = newNode(); root->key[0] = x; root->n = 1; return; }
while (1) {
i = 0; while (i < n->n && x > n->key[i]) i++;
if (i < n->n && n->key[i] == x) { printf(" %d already exists\n", x); return; }
if (isLeaf(n)) break;
path[top++] = n; n = n->child[i];
}
for (j = n->n; j > i; j--) n->key[j] = n->key[j - 1];
n->key[i] = x; n->n++;
while (n->n > maxKeys(n)) { /* overflow */
if (n == root) { splitRoot(); return; }
p = path[--top]; ci = indexIn(p, n);
R = ci < p->n ? p->child[ci + 1] : NULL;
L = ci > 0 ? p->child[ci - 1] : NULL;
if (R != NULL && R->n < M - 1) { shiftRight(n, &p->key[ci], R); printf(" redistribute to the right sibling: %d goes up\n", p->key[ci]); return; }
if (L != NULL && L->n < M - 1) { shiftLeft(L, &p->key[ci - 1], n); printf(" redistribute to the left sibling: %d goes up\n", p->key[ci - 1]); return; }
split2to3(p, R != NULL ? ci : ci - 1);
n = p; /* the parent gained a key */
}
}
/* ---- delete ---- */
void borrowLeft(struct node *L, int *sep, struct node *n) { /* L lends its largest key */
int j;
for (j = n->n; j > 0; j--) n->key[j] = n->key[j - 1];
for (j = n->n + 1; j > 0; j--) n->child[j] = n->child[j - 1];
n->key[0] = *sep; n->child[0] = L->child[L->n]; L->child[L->n] = NULL; n->n++;
*sep = L->key[--L->n];
}
void deleteKey(struct node *p, int i, int ci) { /* remove key i and child ci from p */
int j;
for (j = i; j < p->n - 1; j++) p->key[j] = p->key[j + 1];
for (j = ci; j < p->n; j++) p->child[j] = p->child[j + 1];
p->child[p->n] = NULL; p->n--;
}
void delete(int x) {
struct node *path[64], *n = root, *p, *L, *R, *X, *Y, *Z, *host, *ch[9 * MAXM];
int top = 0, i, j, ci, w, t, nc, k, c, all[6 * MAXM], mk = minKeys(), s1, s2, s3, up1, up2, hi;
while (n != NULL) {
i = 0; while (i < n->n && x > n->key[i]) i++;
if (i < n->n && n->key[i] == x) break;
if (isLeaf(n)) n = NULL; else { path[top++] = n; n = n->child[i]; }
}
if (n == NULL) { printf(" %d not found\n", x); return; }
if (!isLeaf(n)) { /* internal key: use the in-order successor */
host = n; hi = i; path[top++] = n; n = n->child[i + 1];
while (!isLeaf(n)) { path[top++] = n; n = n->child[0]; }
host->key[hi] = n->key[0]; i = 0;
printf(" %d is internal: replaced by its successor %d\n", x, n->key[0]);
}
for (j = i; j < n->n - 1; j++) n->key[j] = n->key[j + 1];
n->n--;
while (n != root && n->n < mk) { /* underflow */
p = path[--top]; ci = indexIn(p, n);
L = ci > 0 ? p->child[ci - 1] : NULL;
R = ci < p->n ? p->child[ci + 1] : NULL;
if (L != NULL && L->n > mk) { borrowLeft(L, &p->key[ci - 1], n); printf(" borrow from the left sibling\n"); return; }
if (R != NULL && R->n > mk) { shiftLeft(n, &p->key[ci], R); printf(" borrow from the right sibling\n"); return; }
if (p->n == 1) { /* only two children: merge them into one */
X = p->child[0]; Y = p->child[1];
X->key[X->n] = p->key[0];
for (j = 0; j < Y->n; j++) X->key[X->n + 1 + j] = Y->key[j];
for (j = 0; j <= Y->n; j++) X->child[X->n + 1 + j] = Y->child[j];
X->n += Y->n + 1; free(Y);
p->n = 0; p->child[1] = NULL;
printf(" 2-to-1 merge: "); printNode(X); printf("\n");
n = p; continue;
}
w = ci == 0 ? 0 : (ci == p->n ? ci - 2 : ci - 1);
X = p->child[w]; Y = p->child[w + 1]; Z = p->child[w + 2];
t = 0; nc = 0;
for (j = 0; j < X->n; j++) all[t++] = X->key[j];
all[t++] = p->key[w];
for (j = 0; j < Y->n; j++) all[t++] = Y->key[j];
all[t++] = p->key[w + 1];
for (j = 0; j < Z->n; j++) all[t++] = Z->key[j];
for (j = 0; j <= X->n; j++) ch[nc++] = X->child[j];
for (j = 0; j <= Y->n; j++) ch[nc++] = Y->child[j];
for (j = 0; j <= Z->n; j++) ch[nc++] = Z->child[j];
k = 0; c = 0;
if (t - 2 >= 3 * mk) { /* enough keys: share among the three */
s1 = (t - 2) / 3; s2 = (t - 2 - s1) / 2; s3 = t - 2 - s1 - s2;
fill(X, all, ch, &k, &c, s1); up1 = all[k++];
fill(Y, all, ch, &k, &c, s2); up2 = all[k++];
fill(Z, all, ch, &k, &c, s3);
p->key[w] = up1; p->key[w + 1] = up2;
printf(" share among three nodes\n");
return;
}
s1 = (t - 1) / 2; /* 3-to-2 merge */
fill(X, all, ch, &k, &c, s1); up1 = all[k++];
fill(Y, all, ch, &k, &c, t - 1 - s1);
free(Z);
p->key[w] = up1; deleteKey(p, w + 1, w + 2);
printf(" 3-to-2 merge: "); printNode(X); printf(" %d ", up1); printNode(Y); printf("\n");
n = p;
}
if (root->n == 0) {
n = root;
root = isLeaf(n) ? NULL : n->child[0];
free(n);
if (root != NULL) printf(" root removed: height - 1\n");
}
}
/* ---- print and check ---- */
void printLevels(void) {
struct node *q[512]; int lev[512], h = 0, tl = 0, cur = -1, i;
if (root == NULL) { printf("empty\n"); return; }
q[tl] = root; lev[tl++] = 0;
while (h < tl) {
struct node *n = q[h]; int d = lev[h++];
if (d != cur) { printf(d ? "\nLevel %d: " : "Level %d: ", d); cur = d; } else printf(" ");
printNode(n);
if (!isLeaf(n)) for (i = 0; i <= n->n; i++) { q[tl] = n->child[i]; lev[tl++] = d + 1; }
}
printf("\n");
}
int leafDepth, lastKey, okFlag;
void check(struct node *n, int d, int isRoot) {
int i;
if (n->n > (isRoot ? rootMax() : M - 1) || n->n < (isRoot ? 1 : minKeys())) okFlag = 0;
if (isLeaf(n)) {
if (leafDepth < 0) leafDepth = d; else if (leafDepth != d) okFlag = 0;
for (i = 0; i < n->n; i++) { if (n->key[i] <= lastKey) okFlag = 0; lastKey = n->key[i]; }
return;
}
for (i = 0; i <= n->n; i++) {
if (n->child[i] == NULL) { okFlag = 0; return; }
check(n->child[i], d + 1, 0);
if (i < n->n) { if (n->key[i] <= lastKey) okFlag = 0; lastKey = n->key[i]; }
}
}
int rulesHold(void) { leafDepth = -1; lastKey = -2147483647; okFlag = 1; if (root) check(root, 0, 1); return okFlag; }
int main(void) {
int ch, k, i, x;
printf("Order m (3 to %d): ", MAXM - 1);
if (scanf("%d", &M) != 1 || M < 3 || M >= MAXM) return 0;
printf("Non-root nodes keep %d to %d keys; the root keeps 1 to %d keys\n", minKeys(), M - 1, rootMax());
while (1) {
printf("\n1.Insert many 2.Show 3.Search 4.Delete 5.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &k); printf("Keys: ");
for (i = 0; i < k; i++) { scanf("%d", &x); printf("Insert %d\n", x); insert(x); } break;
case 2: printLevels(); printf(rulesHold() ? "All B* rules hold\n" : "B* rules broken!\n"); break;
case 3: printf("Key: "); scanf("%d", &x); printf(search(x) ? "%d found\n" : "%d not found\n", x); break;
case 4: printf("Key: "); scanf("%d", &x); printf("Delete %d\n", x); delete(x); break;
case 5: return 0;
}
}
return 0;
}
Sample run
Order m (3 to 7): 4
Non-root nodes keep 2 to 3 keys; the root keeps 1 to 4 keys
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 1
How many? 12
Keys: 40 10 20 30 50 60 35 70 80 90 25 45
Insert 40
Insert 10
Insert 20
Insert 30
Insert 50
root split: 30 is the new root
Insert 60
Insert 35
redistribute to the left sibling: 35 goes up
Insert 70
2-to-3 split: [10 20] 30 [35 40] 50 [60 70]
Insert 80
Insert 90
redistribute to the left sibling: 60 goes up
Insert 25
Insert 45
2-to-3 split: [35 40] 45 [50 60] 70 [80 90]
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 2
Level 0: [30 45 70]
Level 1: [10 20 25] [35 40] [50 60] [80 90]
All B* rules hold
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 3
Key: 35
35 found
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 3
Key: 55
55 not found
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 4
Key: 90
Delete 90
3-to-2 merge: [35 40 45] 50 [60 70 80]
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 4
Key: 20
Delete 20
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 4
Key: 30
Delete 30
30 is internal: replaced by its successor 35
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 2
Level 0: [35 50]
Level 1: [10 25] [40 45] [60 70 80]
All B* rules hold
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 4
Key: 10
Delete 10
share among three nodes
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 4
Key: 35
Delete 35
3-to-2 merge: [25 40 45] 50 [60 70 80]
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 2
Level 0: [50]
Level 1: [25 40 45] [60 70 80]
All B* rules hold
1.Insert many 2.Show 3.Search 4.Delete 5.Exit
Choice: 5
Common mistakes in this part
Splitting a B* node as soon as it overflows. First try to redistribute to a sibling.
Using the B-tree minimum ⌈m/2⌉ − 1 for B* nodes. It is ⌈(2m − 1)/3⌉ − 1.
Applying the 2/3 rule to the root. The root may hold as few as 1 key, and up to 2⌊(2m − 2)/3⌋ before it splits.
Merging two nodes when a B* node underflows. Two underfull neighbours plus a third node become two: 3 → 2.
Key points to remember
B* tree = B-tree with every non-root node at least 2/3 full.
Overflow: redistribute to a sibling; if the sibling is full, split 2 → 3.
Underflow: borrow; if no sibling can lend, merge 3 → 2.
About 81% average fill (B-tree about 69%): fewer nodes, shorter tree, slower updates.
Part 14 · Beyond the syllabus
Trie (Prefix Tree)
A tree for strings in which each edge is one character, so every path from the root spells a prefix. Looking up a word takes as many steps as the word has letters, however many words are stored, and all words with a given prefix sit in one subtree: that is autocomplete.
Beyond the syllabus: one of the most asked string structures in coding rounds (autocomplete, word search, longest common prefix).
14.1At a glance
A trie stores strings character by character: the root is the empty string, each edge adds one character, and a marked node means a word ends there. Words with a common prefix share the same path.
Problem it solves
A BST of words compares whole strings at every node (O(L log n)), and a hash table cannot answer "all words starting with ca". A trie finds a word in O(L) and lists every word with a prefix by walking one subtree.
The rule
One character per edge, children of a node have different characters, end-of-word flag on the node where a word stops.
Think of it as
A dictionary's thumb index: open the C section, then CA, then CAR: every step narrows the choice by one letter.
Use it when
Prefix queries: autocomplete, spell check, word games, longest-prefix match of IP addresses.
Not the right tool when
You only need exact lookups (a hash table uses less memory) or the keys share few prefixes (a plain trie then wastes many almost-empty nodes).
InsertO(L)SearchO(L)Prefix listingO(L + output)DeleteO(L)Llength of the word
Used inSearch-box autocompleteSpell checkersIP routing tablesWord games
Is amultiway tree (up to 26 children)Variantsradix tree, ternary search tree, suffix tree
14.2Definition and structure
Definition
A trie (from retrieval; also called a prefix tree or digital search tree) is a multiway tree for storing strings in which each edge represents one character, the path from the root to a node spells a prefix, and a marked node shows that a complete word ends there. The root represents the empty string.
The keys are not stored in the nodes: a node's position (its path) is its key.
The height of the trie is the length of the longest word. It does not depend on the number of words.
A word can end at an internal node ("car" ends inside "cart" and "care"), which is why the end-of-word flag is needed.
How children are stored
Find a child
Memory per node
Array of 26 pointers (lower-case letters)
O(1)
26 pointers, most of them NULL
Linked list of children
O(26)
Only the children that exist
Hash map from character to child
O(1) average
Only the children that exist, plus hash overhead
How to read the trie pictures
The letter in a node is the character on the edge coming into it. Read a word by following letters down from the root (•).
A double ring shows the end of a word.
Colours: blue path = characters already matched, amber = current node, dark blue = newly created node, green = word found or prefix reached, red = mismatch or node being removed.
#define ALPHA 26
struct trienode {
struct trienode *child[ALPHA]; /* child['c' - 'a'], NULL if absent */
int isEnd; /* 1 if a word ends here */
};
14.3Insert
1Concept
Start at the root.
For each character of the word: if the edge for it exists, follow it; otherwise create a new node for it.
After the last character, mark the node as the end of a word.
Inserting "cab" into a trie that has "car" creates only one new node: "ca" is shared.
2Algorithm
Algorithm TRIE-INSERT(root, w)
1. p = root
2. For each character c of w:
if p.child[c] = NULL: p.child[c] = new node
p = p.child[c]
3. p.isEnd = TRUE
3Worked example
Step by step Build a trie
4C code
struct trienode *newNode(void) { return calloc(1, sizeof(struct trienode)); }
void insert(struct trienode *root, const char *w) {
struct trienode *p = root;
for (; *w; w++) {
int c = *w - 'a';
if (p->child[c] == NULL) p->child[c] = newNode(); /* create the edge */
p = p->child[c];
}
p->isEnd = 1; /* mark the end of the word */
}
14.4Search and prefix listing (autocomplete)
1Concept
Search a word: follow its characters from the root. If an edge is missing, the word is not stored. If all characters match, the word is stored only if the last node is marked; otherwise it is just a prefix.
Words with a prefix: follow the prefix, then list every marked node in that subtree (a depth-first walk, which gives the words in alphabetical order).
2Algorithm
Algorithm TRIE-SEARCH(root, w)
1. p = root
2. For each character c of w:
if p.child[c] = NULL: return FALSE
p = p.child[c]
3. Return p.isEnd (TRUE: word, FALSE: only a prefix)
Algorithm STARTS-WITH(root, s)
1. Follow s from the root as above; if the path breaks, there are no words.
2. LIST(p, s): if p.isEnd, output s
for each child c of p in order: LIST(p.child[c], s + c)
3Worked example
Step by step Search and autocomplete
4C code
/* node reached after following s, or NULL if the path breaks */
struct trienode *walk(struct trienode *p, const char *s) {
for (; *s && p != NULL; s++) p = p->child[*s - 'a'];
return p;
}
int search(struct trienode *root, const char *w) {
struct trienode *p = walk(root, w);
return p != NULL && p->isEnd;
}
char buf[64];
void list(struct trienode *p, int d) { /* every word below p */
int c;
if (p->isEnd) { buf[d] = '\0'; printf("%s ", buf); }
for (c = 0; c < ALPHA; c++)
if (p->child[c]) { buf[d] = (char)('a' + c); list(p->child[c], d + 1); }
}
14.5Delete
1Concept
Search the word. If it is not stored, stop.
Unmark the end of the word.
Walk back up: remove a node if it has no children and is not the end of another word. Stop at the first node that is still needed.
Three situations: deleting "cart" removes the node t only ("car" still ends at r). Deleting "car" removes nothing: its nodes are still used by "cart" and "care", so only the mark goes. Deleting "dog" and then "do" removes the whole d-o-g branch.
2Algorithm
Algorithm TRIE-DELETE(p, w) (returns TRUE if p can be freed)
1. If w is empty:
if not p.isEnd: word not found
p.isEnd = FALSE
return (p has no children)
2. c = first character of w
3. If TRIE-DELETE(p.child[c], rest of w): free p.child[c]; p.child[c] = NULL
4. Return (not p.isEnd and p has no children)
3Worked example
Step by step Delete a word
4C code
int hasChild(struct trienode *p) { int c; for (c = 0; c < ALPHA; c++) if (p->child[c]) return 1; return 0; }
/* returns 1 if p can be freed, 0 if it must stay, -1 if the word is not stored */
int removeWord(struct trienode *p, const char *w) {
int c;
if (*w == '\0') { if (!p->isEnd) return -1; p->isEnd = 0; return !hasChild(p); }
c = *w - 'a';
if (p->child[c] == NULL) return -1;
switch (removeWord(p->child[c], w + 1)) {
case -1: return -1;
case 1: free(p->child[c]); p->child[c] = NULL; /* unused branch */
}
return !p->isEnd && !hasChild(p);
}
14.6Trie vs BST vs hash table, and trie variants
n words of length L
Trie
Balanced BST of strings
Hash table
Search / insert
O(L)
O(L log n)
O(L) average
Words with a prefix
O(L + output)
O(L log n + output)
Not supported (scan everything)
Sorted listing
Yes (depth-first walk)
Yes (in-order)
No
Memory
High (many pointers)
n nodes
Low
Worst case
Always O(L)
Always O(L log n)
O(nL) with bad collisions
Compressed trie (radix tree, Patricia trie)
Chains of single-child nodes are merged into one edge labelled with a whole string ("c" → "ar" → "e"). Same operations, far fewer nodes.
Ternary search tree
Each node holds one character and three children: smaller, equal (next character), larger. Uses much less memory than 26-way nodes, still supports prefix search.
Suffix trie / suffix tree
A trie of all suffixes of one text. Any pattern of length m is found in O(m); used in DNA analysis and text search.
Binary trie
Keys are bit strings (0 = left, 1 = right). Used for IP addresses and for "maximum XOR of two numbers" problems.
14.7Real-life applications
Autocomplete and search suggestions
Typing "ca" walks two edges; everything below is a suggestion. Storing a popularity score in each node lets the best few be shown first.
Why a trie: prefix queries in O(prefix length).
Spell checkers and word games
Checking whether a word exists, suggesting words one letter away, and solvers for crosswords or word-grid games (stop exploring a path as soon as no word has that prefix).
Why a trie: early stop on a dead prefix.
IP routing
A router picks the longest matching prefix of the destination address. Linux stores its IPv4 routing table in a compressed binary trie (the "LC-trie", fib_trie).
Why a trie: longest-prefix match along one path.
Predictive text and dictionaries
Phone keyboards and dictionary apps map typed prefixes to candidate words; compressed tries keep large dictionaries small.
Why a trie: shared prefixes stored once.
14.8Interview and placement questions
Questions asked in viva and interviews
Q1Why does trie search not depend on the number of words?
Each step follows one character, so the walk has at most L steps. Adding more words adds more branches, but never makes an existing path longer.
Q2Why is the end-of-word flag needed?
A word can be a prefix of another ("car" and "cart"). Without the flag, a search for "car" could not tell a stored word from a path that only leads to longer words.
Q3What is the space cost of a trie with 26-pointer nodes?
At most (total characters of all words + 1) nodes, each with 26 pointers: O(26 × total length). Most pointers are NULL, which is why compressed tries, ternary search trees or hash-map children are used for big dictionaries.
Q4Trie or hash table for a dictionary?
Hash table if you only ask "is this word present?": less memory, O(L) average. Trie if you need prefix search, autocomplete, sorted output or longest-prefix match, or a guaranteed O(L) worst case.
Q5How would you count how many words start with a prefix, in O(prefix length)?
Keep a counter in every node: add 1 to each node passed during insert (subtract during delete). The answer is the counter of the node reached by the prefix. See coding problem C2.
Q6What is a radix (Patricia) tree?
A compressed trie: every node with a single child is merged with it, so edges carry strings instead of single characters. It has only O(n) nodes (at most about 2n) for n words, and is used in routing tables and in-memory key-value stores.
Coding problems (tested C programs)
C1Longest common prefix of a set of wordsEasyWalk while there is one child
Find the longest prefix shared by all the words.
Idea. Insert all words. From the root, keep going down while the node has exactly one child and is not the end of a word. The characters passed form the answer.
Time O(total characters) · Space O(total characters)
Show the C program and its output
/* Q1. Longest common prefix of a set of words, using a trie */
#include <stdio.h>
#include <stdlib.h>
#define ALPHA 26
struct trienode { struct trienode *child[ALPHA]; int isEnd, kids; };
void insert(struct trienode *root, const char *w) {
struct trienode *p = root;
int c;
for (; *w; w++) {
c = *w - 'a';
if (p->child[c] == NULL) { p->child[c] = calloc(1, sizeof *p); p->kids++; }
p = p->child[c];
}
p->isEnd = 1;
}
/* follow the single path while every node has exactly one child and no word ends there */
void lcp(struct trienode *root, char *out) {
struct trienode *p = root;
int c, len = 0;
while (p->kids == 1 && !p->isEnd) {
for (c = 0; p->child[c] == NULL; c++) ;
out[len++] = (char)('a' + c);
p = p->child[c];
}
out[len] = '\0';
}
int main(void) {
const char *w1[] = {"flower", "flow", "flight"}, *w2[] = {"interview", "internet", "internal", "interval"};
char out[64];
struct trienode *a = calloc(1, sizeof *a), *b = calloc(1, sizeof *b);
int i;
for (i = 0; i < 3; i++) insert(a, w1[i]);
for (i = 0; i < 4; i++) insert(b, w2[i]);
lcp(a, out); printf("flower flow flight -> \"%s\"\n", out);
lcp(b, out); printf("interview internet internal interval -> \"%s\"\n", out);
return 0;
}
C2Count the words that start with a prefixEasyCounter in every node
After inserting a list of words, answer "how many words start with p?" quickly.
Idea. During insert, add 1 to a pass-counter in every node on the path. The count for a prefix is the counter of the node where the prefix ends (0 if the path breaks).
Time O(L) per insert and per query · Space O(total characters)
Show the C program and its output
/* Q2. Count the words that start with a prefix: keep a counter in every trie node */
#include <stdio.h>
#include <stdlib.h>
#define ALPHA 26
struct tnode { struct tnode *child[ALPHA]; int pass; int isEnd; }; /* pass = words through here */
struct tnode *newNode(void) { return calloc(1, sizeof(struct tnode)); }
void insert(struct tnode *root, const char *w) {
struct tnode *p = root;
for (; *w; w++) {
int c = *w - 'a';
if (p->child[c] == NULL) p->child[c] = newNode();
p = p->child[c];
p->pass++; /* one more word uses this prefix */
}
p->isEnd = 1;
}
int countPrefix(struct tnode *root, const char *pre) {
struct tnode *p = root;
for (; *pre; pre++) {
p = p->child[*pre - 'a'];
if (p == NULL) return 0; /* no word has this prefix */
}
return p->pass;
}
void freeTrie(struct tnode *p) { int i; if (!p) return; for (i = 0; i < ALPHA; i++) freeTrie(p->child[i]); free(p); }
int main(void) {
const char *words[] = {"apple", "app", "apply", "ape", "bat", "bath", "batch"};
const char *q[] = {"ap", "app", "bat", "c", "b"};
struct tnode *root = newNode();
int i;
for (i = 0; i < 7; i++) insert(root, words[i]);
for (i = 0; i < 5; i++) printf("prefix \"%s\": %d word(s)\n", q[i], countPrefix(root, q[i]));
freeTrie(root);
return 0;
}
Can a string with no spaces be split into dictionary words? Print one split.
Idea. ok[i] means the first i characters can be split. From every good position i, walk the trie along s[i..]; each marked node reached at position j makes ok[j + 1] true. The trie tests all words starting at i in one walk.
Time O(n × longest word) · Space O(n + dictionary)
Show the C program and its output
/* Q3. Word break: can a string be split into dictionary words? Trie + dynamic programming */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ALPHA 26
struct tnode { struct tnode *child[ALPHA]; int isEnd; };
struct tnode *newNode(void) { return calloc(1, sizeof(struct tnode)); }
void insert(struct tnode *root, const char *w) {
struct tnode *p = root;
for (; *w; w++) { int c = *w - 'a'; if (!p->child[c]) p->child[c] = newNode(); p = p->child[c]; }
p->isEnd = 1;
}
/* ok[i] = 1 if s[0..i-1] can be split; from every good i, walk the trie along s[i..] */
int wordBreak(struct tnode *root, const char *s, int from[]) {
int n = strlen(s), i, j, ok[64] = {0};
ok[0] = 1;
for (i = 0; i < n; i++) {
struct tnode *p = root;
if (!ok[i]) continue;
for (j = i; j < n; j++) {
p = p->child[s[j] - 'a'];
if (p == NULL) break; /* no word continues this way */
if (p->isEnd && !ok[j + 1]) { ok[j + 1] = 1; from[j + 1] = i; }
}
}
return ok[n];
}
void show(const char *s, int from[], int end) {
if (end == 0) return;
show(s, from, from[end]);
printf("%.*s ", end - from[end], s + from[end]);
}
void freeTrie(struct tnode *p) { int i; if (!p) return; for (i = 0; i < ALPHA; i++) freeTrie(p->child[i]); free(p); }
int main(void) {
const char *dict[] = {"i", "like", "ice", "cream", "icecream", "sam", "sung", "samsung", "man", "go", "mango"};
const char *tests[] = {"ilikeicecream", "ilikesamsung", "ilikemangoes"};
struct tnode *root = newNode();
int i, from[64];
for (i = 0; i < 11; i++) insert(root, dict[i]);
for (i = 0; i < 3; i++) {
printf("%-14s ", tests[i]);
if (wordBreak(root, tests[i], from)) { printf("yes: "); show(tests[i], from, strlen(tests[i])); printf("\n"); }
else printf("no\n");
}
freeTrie(root);
return 0;
}
Output
ilikeicecream yes: i like icecream
ilikesamsung yes: i like samsung
ilikemangoes no
14.9Lab and complete program
Try it Trie builder and search
Algorithm tracer: trie insert, search, prefix and delete
Trace Trie algorithms, step by step
Program 12 · Trie: insert, search, words with a prefix, delete, list all words
/* Trie of lower-case words: insert, search, prefix listing, delete */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ALPHA 26
struct trienode { struct trienode *child[ALPHA]; int isEnd; };
struct trienode *newNode(void) { return calloc(1, sizeof(struct trienode)); }
void insert(struct trienode *root, const char *w) {
struct trienode *p = root;
int c;
for (; *w; w++) {
c = *w - 'a';
if (c < 0 || c >= ALPHA) continue; /* skip anything that is not a-z */
if (p->child[c] == NULL) p->child[c] = newNode();
p = p->child[c];
}
p->isEnd = 1; /* mark the end of a word */
}
/* node reached after following s, or NULL if the path breaks */
struct trienode *walk(struct trienode *p, const char *s) {
int c;
for (; *s && p != NULL; s++) {
c = *s - 'a';
if (c < 0 || c >= ALPHA) return NULL;
p = p->child[c];
}
return p;
}
int search(struct trienode *root, const char *w) { struct trienode *p = walk(root, w); return p != NULL && p->isEnd; }
char buf[64];
int listed;
void list(struct trienode *p, int d) { /* print every word below p */
int c;
if (p->isEnd) { buf[d] = '\0'; printf("%s ", buf); listed++; }
for (c = 0; c < ALPHA; c++)
if (p->child[c]) { buf[d] = (char)('a' + c); list(p->child[c], d + 1); }
}
int hasChild(struct trienode *p) { int c; for (c = 0; c < ALPHA; c++) if (p->child[c]) return 1; return 0; }
/* returns 1 if the node p can be freed by its parent */
int removeWord(struct trienode *p, const char *w) {
int c;
if (*w == '\0') { if (!p->isEnd) return -1; p->isEnd = 0; return !hasChild(p); }
c = *w - 'a';
if (c < 0 || c >= ALPHA || p->child[c] == NULL) return -1;
switch (removeWord(p->child[c], w + 1)) {
case -1: return -1; /* word was not there */
case 1: free(p->child[c]); p->child[c] = NULL; /* unused branch: free it */
}
return !p->isEnd && !hasChild(p);
}
int main(void) {
struct trienode *root = newNode(), *p;
char w[64];
int ch, k, i;
while (1) {
printf("\n1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit\nChoice: ");
if (scanf("%d", &ch) != 1) break;
switch (ch) {
case 1: printf("How many? "); scanf("%d", &k); printf("Words: ");
for (i = 0; i < k; i++) { scanf("%63s", w); insert(root, w); } break;
case 2: printf("Word: "); scanf("%63s", w);
printf(search(root, w) ? "\"%s\" is stored\n" : "\"%s\" is not stored\n", w); break;
case 3: printf("Prefix: "); scanf("%63s", w); p = walk(root, w); listed = 0;
if (p) { strcpy(buf, w); list(p, (int)strlen(w)); }
printf("%s(%d word(s))\n", listed ? "" : "none ", listed); break;
case 4: printf("Word: "); scanf("%63s", w);
printf(removeWord(root, w) == -1 ? "not found\n" : "deleted\n"); break;
case 5: listed = 0; list(root, 0); printf("(%d word(s))\n", listed); break;
case 6: return 0;
}
}
return 0;
}
Sample run
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 1
How many? 6
Words: car cart care cat dog do
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 5
car care cart cat do dog (6 word(s))
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 2
Word: car
"car" is stored
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 2
Word: ca
"ca" is not stored
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 3
Prefix: car
car care cart (3 word(s))
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 4
Word: car
deleted
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 5
care cart cat do dog (5 word(s))
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 2
Word: cart
"cart" is stored
1.Insert words 2.Search word 3.Words with prefix 4.Delete word 5.All words 6.Exit
Choice: 6
Common mistakes in this part
Reporting a word as found when only its path exists. Check the end-of-word mark.
Deleting nodes that other words still use. Remove a node only if it has no children and no end mark.
Storing the whole word in each node. The path is the key.
Forgetting that the root stands for the empty string and has no character.
Key points to remember
One character per edge; the path spells the prefix; a mark ends a word.
Insert, search and delete are O(L), independent of the number of words.
All words with a prefix are in one subtree: autocomplete in O(L + output).
Memory is the cost; compressed tries and ternary search trees reduce it.
Part 15 · Beyond the syllabus
Huffman Tree
A binary tree built from character frequencies that gives every character a bit code: frequent characters get short codes, rare ones long codes, and no code is the start of another. It is the optimal code of this kind and part of ZIP, PNG, JPEG and MP3.
Beyond the syllabus: the standard example of a greedy algorithm; asked as "build the tree and find the codes" and "why is it a prefix code?".
15.1At a glance
A Huffman tree is built by repeatedly joining the two smallest frequencies. Each character is a leaf; its code is the path from the root (left = 0, right = 1).
Problem it solves
A fixed-length code spends the same number of bits on every character (8 in ASCII). Real text is uneven: some characters are far more common. Giving them shorter codes shrinks the file without losing any information.
The rule
Greedy: always merge the two lightest trees into one whose weight is their sum, until one tree is left. Characters sit only at leaves.
Think of it as
Morse code done properly: the most frequent letter (E) gets the shortest signal. Huffman also makes sure no code is the beginning of another, so no pauses are needed.
Use it when
Lossless compression when the symbol frequencies are known or can be counted first.
Not the right tool when
All symbols are about equally frequent (little gain), or the frequencies change as you go (adaptive or arithmetic coding does better).
Build (n characters)O(n log n) with a min-heapEncodeO(total bits)Decodeone tree step per bit
Used inZIP, gzip, PNG (DEFLATE)JPEGMP3Fax machines
15.2Definition: prefix codes and weighted path length
Definition
A Huffman tree is a full binary tree whose leaves are the characters of a message, built so that the weighted path length Σ (frequency × depth) is as small as possible. Reading the path from the root (left = 0, right = 1) gives each character's Huffman code, a variable-length prefix code: no code is the beginning of another.
Prefix-free: every character is a leaf, and a path to one leaf never passes through another leaf. So a bit string can be decoded without separators.
Total bits of the encoded message = Σ frequency × code length = the weighted path length of the tree.
With n characters the tree has n leaves and n − 1 internal nodes, and takes n − 1 merges to build.
Code
A:5
B:9
C:12
D:13
E:16
F:45
Total bits
Fixed length (3 bits)
000
001
010
011
100
101
300
Huffman
1100
1101
100
101
111
0
224
How to read the Huffman pictures
Leaves show a character; the small number under a leaf is its frequency. Internal nodes show the sum of the frequencies below them.
Edge labels: 0 for a left edge, 1 for a right edge. A character's code is the labels from the root down to it.
While building, the picture is a forest: several trees side by side, sorted by weight. The newest merged tree is highlighted.
struct hnode {
char ch; /* the character (only in leaves) */
int freq; /* frequency, or sum for an internal node */
struct hnode *left, *right;
};
15.3Build the tree (greedy merging)
1Concept
Make a single-node tree for every character, with its frequency as its weight.
Pick the two trees with the smallest weights.
Join them under a new root whose weight is their sum (the smaller on the left, edge 0).
Repeat steps 2–3 until one tree is left. With n characters this takes n − 1 merges.
Always merge the two smallest. Rare characters are merged first, so they end up deepest and get the longest codes.
2Algorithm
Algorithm HUFFMAN(C) (C = characters with frequencies)
1. Q = min-heap of single-node trees, keyed by frequency
2. Repeat n − 1 times:
x = EXTRACT-MIN(Q); y = EXTRACT-MIN(Q)
z = new node; z.left = x; z.right = y; z.freq = x.freq + y.freq
INSERT(Q, z)
3. Return EXTRACT-MIN(Q) (the root of the Huffman tree)
3Worked example
Step by step Build a Huffman tree
4C code
/* push / pop keep heap[0..n-1] ordered by freq (a min-heap of tree roots) */
while (n > 1) { /* n - 1 merges */
a = pop(); b = pop(); /* the two smallest */
push(newNode('*', a->freq + b->freq, a, b)); /* smaller on the left */
}
root = pop();
15.4Read the codes and encode
1Concept
Walk the tree from the root, writing 0 for every left edge and 1 for every right edge.
When a leaf is reached, the bits written so far are that character's code.
To encode a message, replace each character by its code and join the codes.
2Algorithm
Algorithm ASSIGN(t, path) (call with the root and an empty path)
1. If t is a leaf: code[t.ch] = path; return
2. ASSIGN(t.left, path + "0")
3. ASSIGN(t.right, path + "1")
Algorithm ENCODE(message)
For each character c: output code[c]
3Worked example
Type any text: the lab counts the frequencies, builds the tree, prints every code and the encoded bits, and compares the size with a fixed-length code and with 8-bit ASCII.
Try it Huffman encoder and decoder
4C code
char code[128][32]; /* code[c] = bits of c */
void assign(struct hnode *t, char *path, int d) {
if (t->left == NULL && t->right == NULL) { /* a leaf */
path[d] = '\0'; strcpy(code[(unsigned char)t->ch], d ? path : "0");
return;
}
path[d] = '0'; assign(t->left, path, d + 1); /* left edge = 0 */
path[d] = '1'; assign(t->right, path, d + 1); /* right edge = 1 */
}
/* encode: join the codes */
for (i = 0; word[i]; i++) strcat(stream, code[(unsigned char)word[i]]);
15.5Decode (walk the tree bit by bit)
1Concept
Start at the root. Read one bit: 0 → go left, 1 → go right.
When a leaf is reached, output its character and jump back to the root.
Repeat until the bits run out.
Because it is a prefix code, reaching a leaf always means one complete code has been read. No separators are needed.
2Algorithm
Algorithm DECODE(root, bits)
1. t = root
2. For each bit b:
t = (b = 0) ? t.left : t.right
if t is a leaf: output t.ch; t = root
3Worked example
Step by step Decode bit by bit
4C code
t = root;
for (i = 0; stream[i]; i++) { /* one step per bit */
t = stream[i] == '0' ? t->left : t->right;
if (t->left == NULL && t->right == NULL) { /* a leaf: one character */
putchar(t->ch);
t = root; /* start again at the root */
}
}
15.6Properties, cost and why it is optimal
Property
Value
Build time
O(n log n): n − 1 merges, each with a few O(log n) heap operations (O(n) if the frequencies are already sorted, using two queues)
Shape
Full binary tree: n leaves, n − 1 internal nodes
Longest code
At most n − 1 bits (when frequencies grow like the Fibonacci numbers)
Are the codes unique?
No. Swapping left and right, or breaking ties differently, gives other codes with the same total bits
Total bits
Minimum over all prefix codes for these frequencies
Why greedy works (idea of the proof): in some optimal tree the two rarest characters are siblings at the deepest level (swapping a rarer character deeper never adds bits). Merging them into one character of weight x + y leaves a smaller problem whose optimal tree, with that leaf expanded back, is optimal for the original. Repeating this argument gives Huffman's algorithm.
Checking an answer: tie-breaking may give different codes from the book, but the code lengths for most characters and the total bits must match. Always state the total Σ frequency × length.
15.7Real-life applications
ZIP, gzip and PNG
They use DEFLATE: first repeated strings are replaced by back-references, then the result is Huffman-coded.
Why Huffman: fast, simple, and optimal for the counted frequencies.
JPEG images
After the lossy steps, the quantised values are packed with Huffman codes (the baseline JPEG standard).
Why Huffman: small values are far more common than large ones.
MP3 audio
The final lossless stage of an MP3 encoder Huffman-codes the quantised frequency values using fixed tables.
Why Huffman: cheap to decode on small devices.
Fax machines
Group 3 fax codes the lengths of runs of white and black pixels with a Huffman-style table ("modified Huffman").
Why Huffman: short white runs and long white runs have very different frequencies.
15.8Interview and placement questions
Questions asked in viva and interviews
Q1Why is a Huffman code a prefix code?
Every character is a leaf. The code of a character is its root-to-leaf path, and a path to one leaf cannot continue through another leaf. So no code is the beginning of another.
Q2Why must codes be prefix-free?
Without it the bits are ambiguous. With codes A = 0, B = 01, C = 1, the string 01 could be "B" or "A C". A prefix code can always be decoded left to right with no separators.
Q3Is Huffman coding greedy? Which data structure makes it fast?
Yes: it always merges the two smallest weights. A min-heap gives the two smallest in O(log n), so building takes O(n log n).
Q4When does Huffman coding give no saving?
When all n frequencies are equal and n is a power of 2: every code has length log2 n, the same as a fixed-length code.
Q5How does the decoder know the tree?
The tree (or just the code length of each symbol) is stored in the file header. DEFLATE stores only the code lengths and rebuilds a "canonical" Huffman code from them, which takes very little space.
Q6Huffman vs Shannon-Fano?
Shannon-Fano splits the sorted symbol list top-down into two halves of nearly equal weight; it is not always optimal. Huffman builds bottom-up by merging the two smallest and is always optimal among prefix codes.
Coding problems (tested C programs)
C1Minimum cost to connect ropesMediumHuffman's greedy rule
Joining two ropes of lengths a and b costs a + b. Join all ropes into one at minimum total cost.
Idea. This is exactly building a Huffman tree: each length is counted once for every join above it, i.e. length × depth. Always join the two shortest ropes, using a min-heap.
Time O(n log n) · Space O(n)
Show the C program and its output
/* Q1. Minimum cost to join ropes (cost of a join = sum of the two lengths): Huffman's greedy rule */
#include <stdio.h>
int h[100], n = 0; /* min-heap */
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
void push(int x) {
int i = n++;
h[i] = x;
while (i > 0 && h[(i - 1) / 2] > h[i]) { swap(&h[i], &h[(i - 1) / 2]); i = (i - 1) / 2; }
}
int pop(void) {
int top = h[0], i = 0, l, r, s;
h[0] = h[--n];
while (1) {
l = 2 * i + 1; r = 2 * i + 2; s = i;
if (l < n && h[l] < h[s]) s = l;
if (r < n && h[r] < h[s]) s = r;
if (s == i) break;
swap(&h[i], &h[s]); i = s;
}
return top;
}
int main(void) {
int len[] = {4, 3, 2, 6}, i, a, b, total = 0;
for (i = 0; i < 4; i++) push(len[i]);
while (n > 1) {
a = pop(); b = pop(); /* always join the two shortest */
total += a + b;
printf("join %d + %d = %d (total so far %d)\n", a, b, a + b, total);
push(a + b);
}
printf("Minimum total cost: %d\n", total);
return 0;
}
Output
join 2 + 3 = 5 (total so far 5)
join 4 + 5 = 9 (total so far 14)
join 6 + 9 = 15 (total so far 29)
Minimum total cost: 29
15.9Algorithm tracer and complete program
Algorithm tracer: build the tree, then encode
Trace Huffman algorithms, step by step
Program 13 · Huffman coding: build with a min-heap, codes table, encode and decode
/* Huffman coding: build the tree with a min-heap, print the codes, encode and decode a word */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct hnode { char ch; int freq; struct hnode *left, *right; };
struct hnode *heap[128]; /* min-heap of tree roots, ordered by freq */
int n = 0;
struct hnode *newNode(char ch, int f, struct hnode *l, struct hnode *r) {
struct hnode *t = malloc(sizeof *t);
t->ch = ch; t->freq = f; t->left = l; t->right = r;
return t;
}
void push(struct hnode *t) {
int i = n++;
while (i > 0 && heap[(i - 1) / 2]->freq > t->freq) { heap[i] = heap[(i - 1) / 2]; i = (i - 1) / 2; }
heap[i] = t;
}
struct hnode *pop(void) {
struct hnode *top = heap[0], *last = heap[--n];
int i = 0, c;
while ((c = 2 * i + 1) < n) {
if (c + 1 < n && heap[c + 1]->freq < heap[c]->freq) c++;
if (last->freq <= heap[c]->freq) break;
heap[i] = heap[c]; i = c;
}
heap[i] = last;
return top;
}
char code[128][32]; /* code[c] = bit string of character c */
void assign(struct hnode *t, char *path, int d) {
if (t->left == NULL && t->right == NULL) {
path[d] = '\0'; strcpy(code[(unsigned char)t->ch], d ? path : "0");
return;
}
path[d] = '0'; assign(t->left, path, d + 1); /* left edge = 0 */
path[d] = '1'; assign(t->right, path, d + 1); /* right edge = 1 */
}
int main(void) {
int k, i, f, total = 0, bits = 0;
char c, word[64], path[32], chars[64], stream[2048];
int freq[64];
struct hnode *a, *b, *t, *root;
printf("Number of characters: "); scanf("%d", &k);
for (i = 0; i < k; i++) {
printf("Character and frequency: "); scanf(" %c %d", &c, &f);
chars[i] = c; freq[i] = f; push(newNode(c, f, NULL, NULL));
}
while (n > 1) { /* n - 1 merges */
a = pop(); b = pop(); /* the two smallest */
push(newNode('*', a->freq + b->freq, a, b)); /* smaller on the left */
}
root = pop();
assign(root, path, 0);
printf("\nChar Freq Code\n");
for (i = 0; i < k; i++) {
printf(" %c %4d %s\n", chars[i], freq[i], code[(unsigned char)chars[i]]);
total += freq[i]; bits += freq[i] * (int)strlen(code[(unsigned char)chars[i]]);
}
for (f = 1, i = 0; f < k; f *= 2) i++; /* bits of a fixed-length code */
printf("Total bits: %d (a fixed %d-bit code would need %d)\n", bits, i, total * i);
printf("\nWord to encode: "); scanf("%63s", word);
stream[0] = '\0';
for (i = 0; word[i]; i++) strcat(stream, code[(unsigned char)word[i]]);
printf("Encoded: %s (%d bits)\n", stream, (int)strlen(stream));
printf("Decoded: ");
t = root;
for (i = 0; stream[i] && root->left == NULL; i++) putchar(root->ch); /* only one character */
for (i = 0; stream[i] && root->left != NULL; i++) { /* walk down one bit at a time */
t = stream[i] == '0' ? t->left : t->right;
if (t->left == NULL && t->right == NULL) { /* a leaf: one character done */
putchar(t->ch);
t = root; /* start again at the root */
}
}
printf("\n");
return 0;
}
Sample run
Number of characters: 6
Character and frequency: A 5
Character and frequency: B 9
Character and frequency: C 12
Character and frequency: D 13
Character and frequency: E 16
Character and frequency: F 45
Char Freq Code
A 5 1100
B 9 1101
C 12 100
D 13 101
E 16 111
F 45 0
Total bits: 224 (a fixed 3-bit code would need 300)
Word to encode: FACE
Encoded: 01100100111 (11 bits)
Decoded: FACE
Common mistakes in this part
Merging the two largest weights, or the first two in the input.
Forgetting to put the merged tree back into the list and re-sort it.
Putting characters on internal nodes: characters are only at leaves.
Giving the total bits as the sum of code lengths. It is Σ frequency × code length.
Key points to remember
Merge the two smallest weights, n − 1 times; left = 0, right = 1.
Characters at leaves ⇒ prefix code ⇒ decoding needs no separators.
Total bits = Σ frequency × code length = weighted path length (minimum possible).
O(n log n) with a min-heap; used in ZIP, PNG, JPEG, MP3.
Unit IV · Compare
All Trees Compared
Every tree in these notes side by side: the problem each one solves, its rule, its cost and where it is used. Finish with the chooser, then the differences that are asked again and again.
C.1Trees grouped by the problem they solve
Show a hierarchy
General tree, binary tree, expression tree. Folders, organisation charts, the structure of a program.
Answer three questions; the notes suggest a tree and say why.
Choose Tree chooser
C.5Differences asked again and again
1Binary tree vs binary search tree
A binary tree has no key order, so a search may visit every node. A BST keeps left < node < right, so the search follows one path and an in-order walk is sorted. Full table: BST uses and comparison.
2BST vs AVL tree
A BST can become a chain. An AVL tree is that same BST with |BF| ≤ 1 at every node, repaired by a rotation. Full table: AVL compared with a BST.
3AVL vs red-black tree
AVL: stricter balance, shorter tree, faster lookups, more rotations on delete. Red-black: looser balance, at most 2 rotations per insert and 3 per delete, preferred by libraries. Full table: AVL vs red-black.
4B-tree vs B+ tree vs B* tree
B-tree: data in every node, search may stop early. B+ tree: data only in linked leaves, every search ends at a leaf, range scans are fast. B* tree: nodes at least 2/3 full, redistribution and 2-to-3 splits. Full tables: B vs B+ and B vs B+ vs B*.
5m-way search tree vs B-tree
An m-way search tree can grow tall and lopsided. A B-tree of the same order keeps every leaf on one level and every non-root node at least half full, and it grows by splitting the root. Full table: m-way uses and comparison.
6Heap vs BST
A heap only orders a parent against its children, stays complete, and lives in an array, so the maximum is at the root. A BST orders left against right, so any key can be found in O(h) and in-order is sorted. Full table: heap complexity and comparison.
7Trie vs hash table vs BST for strings
Hash table: fastest exact lookup on average, no prefix or sorted queries. BST of strings: sorted, O(L log n). Trie: O(L), prefix search and sorted listing, but more memory. Full table: trie comparison.
8Splay tree vs AVL tree
Splay: no extra field, amortised O(log n), hot keys near the root, but a single operation can be O(n) and even searches change the tree. AVL: every operation O(log n), searches read only. See splay cost and comparison.
9Full vs complete vs perfect binary tree
Full: every node has 0 or 2 children. Complete: all levels full except possibly the last, which is filled from the left (heaps). Perfect: all internal nodes have 2 children and all leaves are on the same level. See types of binary trees.
102-3-4 tree vs red-black tree
The same tree drawn two ways: each 2-3-4 node is a black node with 0, 1 or 2 red children. See 2-3-4 and red-black.
Unit IV · Practice
Practice Arena
Unlimited exam-style questions with fresh random keys every time. Type your answer, check it instantly, and open the step-by-step solution when you need it.
P.1How to use it
Pick a topic (or Mixed for a random mix).
Work the question on paper first, the way you would in the exam.
Type only the final answer: keys separated by spaces, e.g. 50 30 20 40 70.
Press Check answer. If it is wrong, press Show solution and use All steps to find the exact step where your tree differed.
P.2Questions
Tip: aim for a streak of 5 correct answers in a row on each topic before the exam. Your best streak is saved in this browser.
Unit IV · Revision
Revision & Exam
One comparison table, one formula sheet, the common mistakes to avoid, and solved questions in the patterns that repeat, arranged in the same order as the parts.
R.1All trees in one table
The syllabus trees side by side. For how they grow out of one another see the family map; for every tree in these notes, with complexity and a chooser, see All Trees Compared.
Binary tree
BST
AVL
m-way search tree
B-tree (order m)
B+ tree (order m)
Children per node
≤ 2
≤ 2
≤ 2
≤ m
⌈m/2⌉ to m (internal, non-root)
Same as B-tree
Keys per node
1
1
1
≤ m − 1
⌈m/2⌉−1 to m−1
Same as B-tree; leaves hold all keys
Ordering rule
None
L < N < R
L < N < R
Keys sorted, subtrees between keys
Same as m-way
Keys ≥ separator go right
Balanced?
No
No
Yes, |BF| ≤ 1
No
Yes, all leaves on one level
Yes, all leaves on one level
Search
O(n)
O(h): O(log n) to O(n)
O(log n)
O(h)
O(logm n) node reads
O(logm n), always to a leaf
How balance is kept
—
—
Rotations
—
Split on insert, borrow or merge on delete
Leaf split copies up, index split moves up
Grows at
Leaves
Leaves
Leaves (then rotates)
Leaves
The root
The root
Typical use
Expression trees, heaps
In-memory sorted data
Frequent lookups in memory
Idea behind disk trees
Database and file indexes
Database indexes, range queries
Threaded trees, heaps, red-black trees, splay trees, 2-3-4 trees, B* trees, tries and Huffman trees are in the full comparison, each under the problem it solves.
R.2Formula sheet
Edges in a tree of n nodes
n − 1
Max nodes on level l (binary)
2l
Max nodes, binary tree of height h
2h+1 − 1
Min height, n nodes (binary)
⌈log2(n+1)⌉ − 1
Leaves in a binary tree
n0 = n2 + 1
NULL links, n nodes (linked binary)
n + 1
Array children, 0-based
2i+1, 2i+2; parent ⌊(i−1)/2⌋
Binary tree shapes with n nodes
(2n)! / ((n+1)! n!)
AVL balance factor
h(L) − h(R) ∈ {−1, 0, +1}
AVL height bound
< 1.44 log2(n+2)
Max keys, m-way tree of height h
mh+1 − 1
B-tree min keys (non-root)
⌈m/2⌉ − 1
B-tree height bound
h ≤ log⌈m/2⌉((n+1)/2)
B-tree keys at height h
2⌈m/2⌉h − 1 to mh+1 − 1
Minimum nodes in an AVL tree of height h
N(h) = N(h−1) + N(h−2) + 1
Red-black tree height
≤ 2 log2(n + 1)
Heap: children / parent of i
2i+1, 2i+2 / ⌊(i−1)/2⌋
Splay cost
amortised O(log n); one operation can be O(n)
Splay worst height
n − 1
B* min keys (non-root)
⌈(2m − 1)/3⌉ − 1
Threaded NULL links
n + 1; all but the first and last become threads
Trie search / insert
O(L), L = length of the word
Huffman merges
n − 1 merges for n symbols
Huffman message length
Σ (frequency × code length)
R.3Common mistakes to avoid
Not stating the convention. Write "root at level 0, height in edges" (or your book's version) before any numerical answer.
BST check done only locally. Verify with the in-order sequence; it must be sorted.
Two-children BST delete done wrongly. The successor is the left-most node of the right subtree, not simply the right child.
AVL rotation done at the wrong node. Rotate at the lowest ancestor with BF ±2, not at the root.
LR/RL fixed with one rotation. A zig-zag always needs two.
Showing only the final tree. Examiners check every intermediate step. Draw the tree after every rotation or split and label it ("RR at 10", "split, 20 moves up").
B-tree keys inserted into an internal node. New keys always go into a leaf; only medians move up.
B-tree order confused with keys. Order 5 means at most 5 children and 4 keys.
AVL deletion stopped after the first rotation. Unlike insertion, deletion must keep checking every ancestor up to the root.
B+ leaf split done like a B-tree. In a leaf split the key is copied up and stays in the right leaf; only index splits move the key up.
Heap treated like a BST. A heap only orders parent and child; siblings have no order, and in-order does not give sorted output.
Pre-order + post-order used to rebuild a tree. The result is not unique; in-order is needed.
Zig-zig done by rotating the parent first. In a splay tree the grandparent rotates first. Rotating the parent first is ordinary move-to-root, and the amortised bound fails.
A failed splay search left unchanged. Splay the last node reached, or the same slow miss can be repeated.
A red node given a red child. In a red-black tree a red node’s parent and children are black. Recolour or rotate before you finish the insert.
A trie node storing a whole word. Each edge is one character. The word is the path, and only the last node is marked as an end.
Characters placed in Huffman internal nodes. Only leaves are characters. An internal node holds the sum of the frequencies below it.
Following a thread as a child while searching. A thread is an in-order neighbour, not a subtree. Search stops when the next link is a thread.
R.4Solved questions
Tree basics and binary trees
Q1Define: complete binary tree, full binary tree, degree of a tree, height of a node, forest.
Complete binary tree: every level is completely filled except possibly the last, and the last level's nodes are as far left as possible.
Full (strict) binary tree: every node has either 0 or 2 children.
Degree of a tree: the maximum degree (number of children) of any node in the tree.
Height of a node: the number of edges on the longest path from that node down to a leaf.
Forest: a set of n ≥ 0 disjoint trees. Removing the root of a tree gives a forest of its subtrees.
Q2Short numericals.
Question
Working
Answer
Max nodes in a binary tree of height 5
26 − 1
63
Min height of a binary tree with 100 nodes
⌈log2 101⌉ − 1 = 7 − 1
6
A binary tree has 15 nodes of degree 2. Leaves?
n0 = n2 + 1
16
NULL pointers in a linked binary tree of 20 nodes
n + 1
21
Keys in a non-root node of a B-tree of order 7
⌈7/2⌉ − 1 to 7 − 1
3 to 6
Different binary trees with 3 nodes
6! / (4! · 3!)
5
Minimum nodes in an AVL tree of height 4
N(4) = N(3) + N(2) + 1 = 7 + 4 + 1
12
Q3Write the pre-order, in-order, post-order and level-order of the tree A(B(D, E(H, )), C(F(I, J), G)).
Pre-order
A B D E H C F I J G
In-order
D B H E A I F J C G
Post-order
D H E B I J F G C A
Level-order
A B C D E F G H I J
Q4Show the array representation of the tree A(B(D, E), C( , F)). Find the parent of F and the children of B using the formulas.
Index
0
1
2
3
4
5
6
tree[ ]
A
B
C
D
E
—
F
C has no left child, so position 2·2 + 1 = 5 stays empty. F is at index 6, so its parent is at ⌊(6 − 1)/2⌋ = 2, which is C. B is at index 1, so its children are at 2·1 + 1 = 3 (D) and 2·1 + 2 = 4 (E).
Q5Convert the general tree A(B(E, F), C(G), D(H, I)) into a binary tree.
Use left-child right-sibling: for every node, the left pointer goes to its first child and the right pointer goes to its next sibling.
A: first child B; no sibling.
B: first child E; next sibling C. C: first child G; next sibling D. D: first child H.
E: next sibling F. H: next sibling I.
Q6Construct the binary tree whose in-order is 4 2 5 1 6 3 and post-order is 4 5 2 6 3 1.
The last element of post-order, 1, is the root. In-order splits into left {4 2 5} and right {6 3}.
Left part: of 4, 5, 2 in post-order the last is 2, the root of the left subtree, with children 4 and 5.
Right part: post-order 6, 3 gives root 3. In in-order, 6 comes before 3, so 6 is its left child.
Q7Draw the expression tree for (a + b * c) − (d / e) and write its prefix and postfix forms.
The operator applied last becomes the root: here the minus between the two brackets. * binds tighter than +, so b * c is a subtree under +.
Prefix (pre-order)
− + a * b c / d e
Postfix (post-order)
a b c * + d e / −
Binary search tree
Q8Construct a BST for 45, 15, 79, 90, 10, 55, 12, 20, 50. Write all three traversals. Then delete 15.
Pre-order
45 15 10 12 20 79 55 50 90
In-order
10 12 15 20 45 50 55 79 90 (sorted, which confirms the BST)
Post-order
12 10 20 15 50 55 90 79 45
Delete 15: it has two children, so find the in-order successor: go right to 20, which has no left child, so the successor is 20. Copy 20 into 15's node and remove the old leaf 20.
AVL tree
Q14Insert 1 to 7 in order into an empty AVL tree. What is the final tree?
Every insertion goes to the right, so only RR rotations occur (at 1, at 3, at 2, at 5). The result is a perfect tree.
Final pre-order: 4 2 1 3 6 5 7.
Q15Construct an AVL tree by inserting 50, 20, 60, 10, 8, 15, 32, 46, 11, 48. Show each rotation.
Both keep left < node < right. A BST does nothing about height, so sorted keys make a chain. An AVL tree also requires |BF| ≤ 1 at every node and rotates when a node reaches ±2, so every operation stays O(log n). Full table: AVL compared with a BST.
Multiway tree
Q9Construct an m-way search tree of order 3 by inserting 35, 60, 20, 80, 10, 25, 40, 50, 90, 70, 15. Then delete 40, 35 and 10.
Order 3: at most 2 keys and 3 pointers per node. A key goes into a node only if the node has room and the pointer for its range is NULL; otherwise it goes down or opens a new node.
After all insertions: root [35 60]; children [10 20], [40 50], [80 90]; [15] and [25] hang below [10 20], [70] below [80 90].
Delete 40: both pointers beside it are NULL, so remove it directly → [50].
Delete 35: its left subtree exists, so replace it with the largest key there, 25 → root [25 60].
Delete 10: left pointer NULL, right subtree [15] exists, so replace it with the smallest key there, 15 → [15 20].
B-tree and B+ tree
Q10Insert 3, 14, 7, 1, 8, 5, 11, 17, 13, 6, 23, 12, 20, 26, 4, 16, 18, 24, 25, 19 into a B-tree of order 5.
Order 5: at most 4 keys per node, so a node splits when it gets its 5th key, and the 3rd key (the median) moves up.
Delete 20 (internal key): replace it with its in-order successor 23, then remove 23 from leaf [23 24 25 26], which keeps 3 keys. Parent becomes [17 23].
Delete 18 (underflow, borrow): leaf [18 19] becomes [19]. The left sibling [14 16] cannot lend, but the right sibling [24 25 26] can: separator 23 comes down, 24 goes up. Parent [17 24], leaves [19 23] [25 26].
Delete 5 (underflow, merge twice, root shrinks): leaf [5 6] becomes [6]. Neither sibling can lend, so merge [1 3] + 4 + [6] = [1 3 4 6]. The parent [4 7] drops to [7] and underflows; its sibling [17 24] cannot lend, so merge [7] + 13 + [17 24] = [7 13 17 24]. The root is now empty and the tree becomes one level shorter.
Q12Construct a B+ tree of order 4 by inserting 1, 4, 7, 10, 17, 21, 31, 25, 19, 20, 28, 42.
Order 4: at most 3 keys per node. A leaf that gets a 4th key splits: the left leaf keeps 2 keys, and the first key of the right leaf is copied up. An index node that overflows splits like a B-tree: its median moves up.
Final tree: root [17]; index nodes [7] and [20 25 31]; leaf chain [1 4] → [7 10] → [17 19] → [20 21] → [25 28] → [31 42].
Q13Differentiate between a B-tree and a B+ tree.
A B-tree can store a key in any node, so a search may stop before a leaf. A B+ tree keeps the data only in linked leaves, so every search ends at a leaf and a range follows the leaf chain. A leaf split copies a key up; an index split moves it up. Full table: B-tree vs B+ tree.
More trees
Q17Build a max-heap by inserting 40, 20, 60, 10, 50, 30, then delete the maximum twice. Show the array after each step.
Each insert goes to the next array position and moves up while it is larger than its parent. Each delete moves the last element to the root and moves it down, swapping with the larger child.
R.5All C programs in one place
Every program was compiled with gcc and tested with the inputs shown in its sample run. Tap a program to open it where it is explained, or open its code tracer to watch it run line by line on your own keys.
Interview-style coding problems (37 short tested programs) are at the end of each tree's page, under Interview and placement questions.
Every tree in these notes is drawn by the same code that runs the visualizers, and every C program was compiled with gcc and tested against the same inputs, so figures, animations and program output agree.