Skip to content

2-3 trees

Nodes hold one key or two. A node that would hold three splits and pushes its middle key up, so every leaf stays at the same depth and the tree grows only at the root.

Helps to know firstBinary search trees

Helps to know first
This topic

First, the picture

What if a node could hold two keys, and split when a third arrives? Watch where the tree grows.

Splitting a full node in two

Seven keys inserted in order into a 2-3 tree

    if (n->nkeys == 1) {        if (i == 0) {            n->keys[1] = n->keys[0];            n->child[2] = n->child[1];            n->keys[0] = key;            n->child[1] = extra;        } else {            n->keys[1] = key;            n->child[2] = extra;        }        n->nkeys = 2;        return false;    }    int k3[3];    struct node23 *c4[4];    for (int j = 0, a = 0; j < 3; j++)        k3[j] = j == i ? key : n->keys[a++];    for (int j = 0, a = 0; j < 4; j++)        c4[j] = j == i + 1 ? extra : n->child[a++];    n->nkeys = 1;    n->keys[0] = k3[0];    n->child[0] = c4[0];    n->child[1] = c4[1];    n->child[2] = NULL;    *right = make_node(k3[2], c4[2], c4[3]);    *up = k3[1];    splits++;    return true;}    struct node23 *root = NULL;    for (int k = 10; k <= 70; k += 10)        root = insert(root, k);    print_inorder(root);

main has started.

height
0
splits
0
plain search tree
0
0.00 / 23.80 s
Describe this picture

A picture across the frame, with the code under it. Each node is a box divided into one slot per key, with its keys in order, and a line from the node to each of its children. The tree sits on its lowest row, so a new level appears at the top. A ring marks the node that has just taken a key. A node that would hold three keys is drawn with all three, tinted in the warning colour, for one beat before it splits. The code pane shows the part of insert_rec that places a key or splits a node, and main’s loop, with the line about to run marked. The clip inserts 10, 20, 30, 40, 50, 60 and 70, about 24 seconds. 10 and 20 share one node; 30 makes it 10 20 30, which splits: 20 rises into a new root over 10 and 30. 40 joins 30; 50 makes 30 40 50, and 40 rises into the root beside 20. 60 joins 50; 70 makes 50 60 70, 60 rises, the root would then hold 20 40 60 and splits too, and 40 becomes a new root over 20 and 60, which sit over the leaves 10, 30, 50 and 70. Under the picture are one sentence on the current step and the readouts “height”, “splits” and “plain search tree” (the height a search tree without splits would reach with the same keys), which end at 2, 4 and 6. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as two-three.step.

One key or two, and a split

A binary search tree makes one choice at each node: left or right. A 2-3 tree allows a node to hold one key or two:

  • a 2-node has one key and two children, smaller keys on the left and larger on the right;
  • a 3-node has two keys and three children: smaller than both, between them, and larger than both.

A search compares the key with the one or two keys in a node, picks the child, and goes down, as in Binary search trees.

The difference is in insertion. A new key always goes into the leaf where its search ends. If the leaf has one key, it now has two, and that is all. If it already had two, it would hold three, which is not allowed, so it splits: the smallest key stays, the largest goes to a new node, and the middle key goes up into the parent. The parent may then hold three and split in turn. If the root splits, its middle key becomes a new root.

Follow the clip. 10 and 20 share a leaf. 30 makes it 10 20 30: it splits, and 20 goes up into a new root over 10 and 30. At 70 a split goes up twice: the leaf 50 60 70 splits and sends 60 up, the root becomes 20 40 60 and splits too, and 40 becomes the new root.

Key idea

A 2-3 tree never grows at the leaves. It grows only when the root splits, so every leaf is always at the same depth.

Sorted keys made a plain search tree into a chain of height 6. Here they make a tree of height 2.

The code and its cost

The node

The full program, tree23.c, stores a node as:

struct node23 {
    int nkeys;
    int keys[2];
    struct node23 *child[3];
};

nkeys is 1 or 2. A 2-node uses keys[0] and child[0] and child[1]; a 3-node uses both keys and all three children. A leaf’s children are NULL. The struct is 40 bytes: nkeys at offset 0, the two keys at 4 and 8, 4 bytes of padding so that the pointers start at 16, and the three child pointers at 16, 24 and 32.

Inserting: down, then back up

insert_rec(n, key, &up, &right) puts key somewhere below n. It returns false if everything fitted, or true if n itself split, and then it hands back the middle key through up and the new right node through right, because the caller must place them.

It first finds i, the place of the new key among the node’s keys:

int i = 0;
while (i < n->nkeys && key > n->keys[i])
    i++;

If n is not a leaf, it calls itself on child[i]. If that call returns false, there is nothing more to do. If it returns true, the child split, and n must now take the key that came up, with the new node as the child to its right.

Taking a key into a node with room is a few assignments: with i == 0 the old key moves to place 1 and the new one goes in front; otherwise the new key goes in place 1. nkeys becomes 2.

Splitting

A node that already has two keys gathers all three keys in order, and all four children, into small arrays:

int k3[3];
struct node23 *c4[4];
for (int j = 0, a = 0; j < 3; j++)
    k3[j] = j == i ? key : n->keys[a++];
for (int j = 0, a = 0; j < 4; j++)
    c4[j] = j == i + 1 ? extra : n->child[a++];

j == i ? key : n->keys[a++] reads: if this is the new key’s place, use the new key; otherwise take the next of the node’s own keys and move a on. So k3 holds the three keys in order. The children work the same way, with the new node at place i + 1, just right of the new key.

Then the node keeps k3[0] with c4[0] and c4[1], a new node gets k3[2] with c4[2] and c4[3], and k3[1] goes up through *up. If the split reaches the root, insert makes a new root holding that middle key, over the old root and the new node: the tree is one level taller.

What it costs

Every leaf is at the same depth, so the height is set by how many keys fit. A tree of height h has h + 1 levels. With only 2-nodes, it has 2^(h+1) − 1 nodes holding as many keys; with only 3-nodes, 3^(h+1) − 1 keys. So for n keys

log⁡3(n+1)−1≤h≤log⁡2(n+1)−1.\log_3(n+1) - 1 \le h \le \log_2(n+1) - 1.

A search, or an insertion with all its splits, visits one node per level, with one or two comparisons each: a number of steps proportional to log n, whatever order the keys arrive in.

Running it yourself

gcc -std=c11 -Wall -Wextra -pedantic tree23.c -o tree23
./tree23

It prints the keys in order, 10 20 30 40 50 60 70, then height 2, 4 splits.

Worked example

Insert 25 into the final tree. The search goes left of 40, right of 20, and ends at the leaf 30, which has one key: it becomes 25 30. No split.

Then insert 27. The search ends at the leaf 25 30, which would hold 25 27 30. It splits: 25 stays, 30 goes to a new leaf, and 27 goes up into 20, which had room and becomes 20 27. One split, and the height is still 2.

The height of 7 keys. At most log₂ 8 − 1 = 2, and at least log₃ 8 − 1 ≈ 0.89, so at least 1. The clip’s tree has height 2, but the same seven keys could also form a tree of height 1: a root 30 60 over the leaves 10 20, 40 50 and 70.

Two checks. How many keys can a 2-3 tree of height 1 hold? (3 to 8: a root with one key over two leaves with one key each, up to a root with two keys over three leaves with two each.) When does the height grow? (Only when the root splits.)

Where it’s used

As a model. 2-3 trees are seldom used as they stand: nodes of two shapes make the code longer than it needs to be. Their idea lives on in two forms that are used everywhere.

Red–black trees. A binary tree that colours some links red to stand for 3-nodes; it is how many libraries store ordered maps (Red–black trees).

B-trees. The same splitting with hundreds of keys per node, sized to a disk block; databases and file systems use them (B-trees).

Reference card

Code or ideaMeaning
2-nodeone key, two children
3-nodetwo keys, three children
insert_rec returns truethis node split: *up and *right go to the caller
k3, c4the three keys and four children of a node about to split
splitkeep k3[0], new node k3[2], send k3[1] up
root splita new root: the only way the height grows
heightbetween log₃(n + 1) − 1 and log₂(n + 1) − 1
sizeof (struct node23)40 bytes

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.

Keep reading with a free account.

Trees is a members’ section. An account costs nothing.

By creating an account you accept the terms of use.

Sign in

Already have an account? Use the way you signed up.

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look