Skip to content

Union–find

An array of parent indices keeps items in groups as trees; joining links one root under another, and finding flattens the path it climbs.

Helps to know first
This topic

First, the picture

Ten items, joined into groups pair by pair, kept in one array of ten numbers. Watch the groups grow as trees, and a search flatten them.

Who belongs to which group

Ten items in an array of parents, joined into groups and flattened by finds

int find(int x){    if (parent[x] != x)        parent[x] = find(parent[x]);    return parent[x];} bool unite(int a, int b){    int ra = find(a);    int rb = find(b);    if (ra == rb)        return false;    if (rank[ra] < rank[rb]) {        int t = ra;        ra = rb;        rb = t;    }    parent[rb] = ra;    if (rank[ra] == rank[rb])        rank[ra]++;    return true;}    const int pairs[8][2] = {        { 0, 1 }, { 2, 3 }, { 0, 2 },        { 4, 5 }, { 6, 7 }, { 4, 6 },        { 0, 4 }, { 8, 9 }    };    for (int i = 0; i < 8; i++)        unite(pairs[i][0], pairs[i][1]);    print_parents("unions:");    int r7 = find(7);    print_parents("find(7):");    int r3 = find(3);    int r9 = find(9);    print_parents("find(3), find(9):");    int groups = 0;    for (int i = 0; i < N; i++)        if (parent[i] == i)            groups++;    printf("7 in %d, 3 in %d, 9 in %d; %d groups\n", r7, r3, r9, groups);    return 0;

Eight pairs to join.

groups
10
7's steps to its root
0

Output

0.00 / 20.35 s
Describe this picture

The picture has three parts, one above the other. On top are the ten items, 0 to 9, drawn as circles; each item that has a parent has an arrow up to it, and an item that is its own parent, a root, has a heavier ring and stands at the top of its tree. Under them are two strips of ten boxes: parent, the parent of each item, and rank, a number kept for each root. A box the program has just written is outlined. Under the picture are the lines of uf.c the clip runs, with the line about to run marked, one sentence on the current step, two readouts (how many groups there are, and how many steps item 7 is from its root), and the output. The clip plays once, about 20 seconds, and has no control. It starts with ten roots, parent reading 0 1 2 3 4 5 6 7 8 9 and every rank 0. Then eight unions, one beat each: (0, 1), (2, 3), (0, 2), (4, 5), (6, 7), (4, 6), (0, 4) and (8, 9). Each hangs one root under another, ringed as it moves; its arrow appears, the trees slide together, and its box in parent is outlined, with the rank of the root it went under when that rank grows. After the unions there are 2 groups, parent reads 0 0 0 2 0 4 4 6 8 8, rank reads 3 0 1 0 2 0 1 0 1 0, and 7 is 3 steps from its root. Then the search for 7’s root: 7, 6 and 4 are ringed in turn and keep a highlight as the search climbs, and 0, the root, is marked found. On the way back the arrow of 6 swings from 4 to 0, then the arrow of 7 swings from 6 to 0, and the readout drops from 3 to 1. The search for 3’s root swings 3’s arrow from 2 to 0; the search for 9’s root changes nothing it draws, but outlines 9’s box, written again with the 8 it held. The clip rests on the output: unions: 0 0 0 2 0 4 4 6 8 8, find(7): 0 0 0 2 0 4 0 0 8 8, find(3), find(9): 0 0 0 0 0 4 0 0 8 8 and 7 in 0, 3 in 0, 9 in 8; 2 groups. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as uf.step.

Groups that only grow

Some problems hand over pairs one at a time: these two computers are now linked, these two pixels touch, these two names mean the same thing. After each pair, the question is whether two items are in the same group yet. Groups merge but never split.

Union–find, also called a disjoint-set structure, answers that with two operations. unite(a, b) merges the groups of a and b. find(x) names x’s group. Two items are in the same group exactly when find gives the same answer for both.

The code and its cost

The forest in an array

Each group is a tree, stored in one array: parent[x] is the item above x. A root is its own parent, parent[x] == x, and the root names the group. No pointers and no allocation: ten items need forty bytes of parents. To start, every item is a root on its own:

for (int i = 0; i < N; i++) {
    parent[i] = i;
    rank[i] = 0;
}

The groups can be read straight from the array: follow parents up from any item until one points to itself. After the program’s eight unions, parent is 0 0 0 2 0 4 4 6 8 8. From 7: 7 → 6 → 4 → 0, so 7 is in group 0. From 9: 9 → 8, group 8. Two groups: 0 to 7, and 8 with 9.

find and unite

int find(int x)
{
    if (parent[x] != x)
        parent[x] = find(parent[x]);
    return parent[x];
}

find climbs to the root by recursion. Returning, each call writes the root into parent[x]. So find does not only read: it rewrites the parent of every item it passed. In the clip, find(7) went four calls deep, 7, 6, 4 and 0. Returning, it wrote 0 into parent[4] (which already held 0), then into parent[6] and parent[7].

unite finds both roots. If they are the same, the items are already in one group and it returns false. Otherwise it hangs one root under the other, parent[rb] = ra, and returns true. After the program’s unions, unite(1, 3) would return false: both roots are 0.

Union by rank

Which root goes under which matters. If every union hung the old root under the newcomer, unite(0, 1), then unite(1, 2) and so on up to unite(8, 9) would build one path, 0 → 1 → 2 → … → 9, and find(0) would climb 9 steps.

Union by rank keeps a number for each root, its rank, and hangs the root of lower rank under the root of higher rank. Only when the ranks are equal does the new root’s rank grow by one. A tree of rank r then holds at least 2^r items: it got rank r by joining two trees of rank r − 1, each of at least 2^(r−1). So with n items no rank passes log₂ n, and without path compression no tree is taller than its rank. In the program the unions are all between equal ranks, and 0 ends with rank 3: (0, 1) gave it 1, (0, 2) gave it 2, (0, 4) gave it 3.

Path compression

Path compression is the rewriting find does on its way back: every item it passed now points straight at the root. After find(7), 7 is one step from 0, and so is 6; the next find(7) is a single step. Items the search did not pass keep their parents: at the end 5 still points at 4, two steps from 0, until some find passes it.

Compression changes heights but never ranks. So a rank is only an upper bound on the height of its tree. At the end, 0’s rank is 3, but its tallest path, 5 → 4 → 0, is 2 steps.

The cost and α

With both rules, m operations on n items take O(m α(n)) steps in all. α is the inverse of Ackermann’s function from indirect and nested recursion: Ackermann’s function grows so fast that its inverse is at most 4 for any n that fits in a computer. In practice, each operation costs a few steps.

Running it yourself

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

Worked example

Without union by rank. With the newcomer always on top, the nine unions (0, 1), (1, 2), …, (8, 9) leave one path of ten items and find(0) climbs 9 steps. With union by rank, unite(0, 1) puts 1 under 0 with rank 1; every later newcomer has rank 0 and goes under 0 as well, so the tree has height 1.

Union by rank alone. Eight items, joined in rounds of equal-rank pairs: (0, 1), (2, 3), (4, 5), (6, 7) give four trees of rank 1; (0, 2), (4, 6) give two of rank 2; (0, 4) gives one of rank 3. Three rounds, height 3 = log₂ 8.

Reading the final array. parent is 0 0 0 0 0 4 0 0 8 8. Are 3 and 7 connected? 3 → 0 and 7 → 0: yes. Are 5 and 9? 5 → 4 → 0 and 9 → 8: no.

Two checks. After the unions, how many steps does find(7) climb, and how many the next time? (3, then 1.) Why does unite(1, 3) return false after the unions? (Both roots are 0.)

Where it’s used

Kruskal’s algorithm. It adds roads cheapest first and asks union–find whether a road’s two towns are already joined.

Labelling regions in images. Joining each pixel to its matching neighbours groups an image into its connected regions.

Networks as links come up. Each new link unites two groups of computers; one find each tells whether two machines can reach each other.

Compilers. Grouping names or types that have been declared equal, one equation at a time.

Reference card

Code or ideaMeaning
parent[x] == xx is a root; the root names the group
find(x)climb to the root; returning, point every item passed at it
unite(a, b)hang one root under the other; false if already one group
union by ranklower rank under higher; equal ranks: the new root’s rank grows
rankan upper bound on height, not the height
rank rat least 2^r items, so rank ≤ log₂ n
both rulesO(m α(n)) for m operations; α(n) ≤ 4 in practice

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.

Graphs 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