Taking a key out of a search tree must leave every smaller key on the left. Watch what happens when the root goes.
Removing a node from a tree
Eight keys; deleting the root, which has two children
void bst_remove(struct tnode **link, int key){ while (*link != NULL && (*link)->key != key) { if (key < (*link)->key) link = &(*link)->left; else link = &(*link)->right; } struct tnode *t = *link; if (t == NULL) return; if (t->left == NULL) { *link = t->right; free(t); } else if (t->right == NULL) { *link = t->left; free(t); } else { struct tnode **s = &t->right; while ((*s)->left != NULL) s = &(*s)->left; struct tnode *succ = *s; t->key = succ->key; *s = succ->right; free(succ); }} bst_remove(&root, KEY); inorder(root);All 8 keys are in the tree. Now remove 50.
- links re-aimed
- 0
- keys moved
- 0
Describe this picture
A picture across the frame, with the code under it. On the left is the tree of Binary search trees with one more key: 50 at the root, 30 and 70 under it, 20 and 40 under 30, 60 and 80 under 70, and 65 as 60’s right child. Each node is a box with its key, its address under it and two dots for its left and right pointers. A ring marks the node the search has reached; once found, the node to remove is marked as the target and tagged t, and during the successor search the ring moves to the node s leads to. On the right is the card for the call bst_remove(&root, 50), with its boxes link, key and t, and later s and succ; link and s are written as the link they hold the address of, such as &root or &70->left. Under both, a row shows the keys in order, read from the tree at every step. The code pane shows bst_remove and main’s line that calls it, with the line about to run marked. The clip removes 50, about 14 seconds: the ring starts on 50, which is found at once; s goes right once, to 70, then left to 60, which has no left child; 60’s key is copied into the root, so for one step the row reads 20 30 40 60 60 65 70 80; then 70’s left pointer swings to 65, the old 60 node leaves the tree and is drawn aside, and it is hatched once freed. The row ends 20 30 40 60 65 70 80. Under the picture are one sentence on the current step, the readouts “links re-aimed” and “keys moved”, which end at 1 and 1, the control and the output. The control, “Remove”, has one button for each key, 20 to 80 in order; a press plays that removal from the full eight-key tree. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as remove.step, and the key as remove.key.
Find the link, then re-aim it
A search tree keeps one promise at every node: smaller keys in the left subtree, larger in the right. Removing a key must keep it. In memory, a node is in the tree because one pointer points to it, the root pointer or its parent’s left or right. So removing comes down to one question: which pointer should change, and what should it point to now?
Press the leaves first: 20, 40, 65 or 80. The search walks down to the node, the pointer that led there becomes NULL, and the node is freed. Nothing else moves; the readout says one link re-aimed.
Now press 60. It has one child, 65. The pointer that led to 60 (70’s left) is aimed at 65 instead, and 60 is freed. 65 moves up into 60’s place, and the order still holds: everything under 60 was already between 50 and 70, as 60 was.
Now 50, the root, which the clip plays. Its two children can’t both hang from one pointer. The trick is to leave the node where it is and change its key: copy in the successor, the smallest key larger than 50, which is 60. Then remove the old 60 node instead, and that node is easy to remove, because it has no left child. Only one key moves, and one link is re-aimed: 70’s left, from the old 60 to 65.
Key idea
Leaf or one child: the link that points to the node takes its other child, which may be NULL. Two children: copy the successor’s key up, then remove the successor, which has at most one child.
The code and its cost
The program
The full program, bst-remove.c, starts with the two #include lines, struct tnode, new_node and bst_insert copied from bst.c (Binary search trees), and inorder and free_tree from traversals.c (Tree traversals). Its main inserts the eight keys 50 30 70 20 40 60 80 65, removes KEY, prints the tree in order and frees it. The new function is this:
void bst_remove(struct tnode **link, int key)
{
while (*link != NULL && (*link)->key != key) {
if (key < (*link)->key)
link = &(*link)->left;
else
link = &(*link)->right;
}
struct tnode *t = *link;
if (t == NULL)
return;
if (t->left == NULL) {
*link = t->right;
free(t);
} else if (t->right == NULL) {
*link = t->left;
free(t);
} else {
struct tnode **s = &t->right;
while ((*s)->left != NULL)
s = &(*s)->left;
struct tnode *succ = *s;
t->key = succ->key;
*s = succ->right;
free(succ);
}
}
A pointer to the link
link is not a pointer to a node. It holds the address of a pointer: first main’s root, then a node’s left or right field. This is the idea of Removing from a linked list: walk with a pointer to the link, so that when the node is found, *link = … changes exactly the pointer that leads to it. The root needs no special case. If the root itself is removed and has one child, *link is main’s root, and the child becomes the new root.
The call passes &root for that reason: bst_remove may need to change main’s root, so it is given the address of it.
The three cases
t is the node found. If it has no left child, *link = t->right covers two cases at once: a leaf (t->right is NULL, so the link becomes NULL) and a node whose one child is on the right. If it has a left child but no right, the link takes the left child. Either way the child keeps its whole subtree, and every key in that subtree was already on the correct side of every node above.
The successor
With two children, bst_remove finds the successor: one step right, then left until there is no left child. The right subtree holds every key larger than t’s, and its leftmost node holds the smallest of them. s makes the walk as link did, as a pointer to a link, so that afterwards *s is the pointer that leads to the successor.
The successor can’t have a left child. A left child would be smaller than the successor and still in t’s right subtree, so it would be the successor instead. With at most one child, on the right, the successor is removed by the one-child rule: *s = succ->right.
Why the successor? The node’s new key must be larger than everything left of it and smaller than everything right of it. The successor is: it is the smallest key on the right. The predecessor, the largest key on the left (one step left, then right to the end), works the same way. Any other key would break the order somewhere.
| Key removed | Case | What changes |
|---|---|---|
| 20, 40, 65, 80 | leaf | the parent’s pointer becomes NULL |
| 60 | one child | 70’s left points to 65 |
| 50 | two children | the root holds 60; 70’s left points to 65 |
| 30 | two children | 30’s node holds 40; its right becomes NULL |
| 70 | two children | 70’s node holds 80; its right becomes NULL |
What it costs
The walk to the node and the walk on to its successor go down one path from the root, so a removal visits at most h + 1 nodes, where h is the tree’s height (counted in edges), and does a fixed amount of work at each. Like a search, a removal costs the height: about log₂ n steps in a bushy tree, up to n in a chain. Removing 50 here visits 50, 70 and 60; the tree’s height is 3.
Running it yourself
KEY is a macro, 50 unless the build says otherwise, so one file removes any key:
gcc -std=c11 -Wall -Wextra -pedantic bst-remove.c -o bst-remove
./bst-remove
gcc -std=c11 -Wall -Wextra -pedantic -DKEY=30 bst-remove.c -o bst-remove
./bst-remove
The first prints 20 30 40 60 65 70 80, the second 20 40 50 60 65 70 80. Whatever key is removed, the line is the other seven keys in order, which is the quickest check that the tree is still a search tree.
Worked example
Start from the eight-key tree and remove 50, then 20, then 30.
| Remove | Case | Keys in order afterwards | Preorder afterwards |
|---|---|---|---|
| 50 | two children: 60 moves up | 20 30 40 60 65 70 80 | 60 30 20 40 70 65 80 |
| 20 | leaf: 30’s left becomes NULL | 30 40 60 65 70 80 | 60 30 40 70 65 80 |
| 30 | one child: 60’s left becomes 40 | 40 60 65 70 80 | 60 40 70 65 80 |
Each inorder list is the one before it minus the key. The preorder shows the shape: the root is 60 from the first removal on, and after the third, 60 has 40 on its left and 70 (with 65 and 80) on its right.
Two checks. What is the successor of 30 in the eight-key tree? (40: one step right, to 40, which has no left child.) Why can’t the successor have a left child? (A left child would be smaller and still in the right subtree, so it would be the successor.)
Where it’s used
Ordered maps. Every ordered map or set built on a search tree supports removal, and starts with exactly this: find the link, then one of the three cases.
Balanced trees. AVL trees and red–black trees begin their deletions the same way, then repair the balance on the way back up.
Reference card
| Code or idea | Meaning |
|---|---|
struct tnode **link | a pointer to a link: main’s root, or a node’s left or right |
link = &(*link)->left | move down, keeping the address of the pointer that leads on |
| leaf | *link = NULL, then free |
| one child | *link takes the child, then free |
| two children | copy the successor’s key, then remove the successor |
| successor | one step right, then left to the end; never has a left child |
| predecessor | one step left, then right to the end |
| cost | at most h + 1 nodes, one path from the root |
| check | inorder prints the remaining keys, still sorted |
Platform: C11, gcc on x86-64 Linux: int 4 bytes, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.