Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Structures & Algorithms (DSA)  Module 11 – Graphs: modelling, traversal and connectivity

Union-find (disjoint set union)

How union-find keeps items in groups that only merge, answers "same group?" with find and union, and stays nearly constant per step with two simple rules.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 9 of 30 minutes free
Free: Groups as trees of parent pointers · find and union, step by step · Two rules that keep the trees short · Exercise and self-check. With a pass: 3 in-depth parts and 3 interview answers, about 21 minutes.

What you will learn

  • Implement union by size or rank with path compression
  • Explain the near-constant amortised bound
  • Apply union-find to connectivity and grouping problems

Before you start

Union-find, also called disjoint set union (DSU), tells you in almost no time whether two items belong together while the items are being joined into groups, step by step. It offers two operations. find(x) names the group of x by returning one chosen member of it, the representative, and union(a, b) joins the group of a to the group of b. A group can grow, but it can never split. Inside, each group is a tree of parent pointers whose root is the representative, and two rules keep the trees short: the smaller tree goes under the larger root, and every find points the items it passes straight at the root. With both rules, any m operations on n items take O(m α(n)) time, where α, an inverse of Ackermann’s function, is at most 4 for any number of items a computer could ever store. That makes it the tool for growing networks, merging duplicates and Kruskal’s spanning trees.

Groups as trees of parent pointers

The whole structure is two arrays. parent[x] is the item one step above x in its tree, and an item that is its own parent is a root. Every group has exactly one root, so two items share a group exactly when they reach the same root. size[r] counts the items under the root r. For a million items that is two arrays of a million numbers, about 8 MB as 4-byte integers.

Two trees of four items with roots 0 and 4, then one tree of eight items after root 4 is linked under root 0.After union(5, 7): roots 0 and 4After union(3, 7): one root, 00412356701234567

The lab computers as a forest of parent pointers

Text description of the diagram

Arrows point from each item to its parent; a root has no arrow of its own.

The first picture shows the forest after the first six cables. There are two trees of four items each. In the first, items 1 and 2 point to the root 0, and item 3 points to 2, so 3 is two steps from its root. In the second, items 5 and 6 point to the root 4, and item 7 points to 6.

The second picture shows the same items after the question same(3, 7) and the cable union(3, 7). The question walked from 3 to 0 and from 7 to 4, and path compression pointed 3 straight at 0 and 7 straight at 4. The union then found the two roots, 0 and 4, saw that both trees had 4 items, and linked root 4 under root 0. Now items 1, 2, 3 and 4 point to 0, and items 5, 6 and 7 point to 4, so no item is more than two steps from the root.

find(x) follows parent pointers from x until it reaches a root. union(a, b) finds both roots; if they are the same item, the two were already in one group, and otherwise one root becomes the parent of the other, which merges the trees in one step. Nothing else ever changes a pointer, so a group can grow but never split.

find and union, step by step

Eight lab computers, numbered 0 to 7, are joined by cables one at a time. After each cable the program prints the parent array and the number of groups, and it answers two questions on the way:

Eight computers joined by cables, traced Python · trace/union_find.py
# Union-find with union by size and path compression, traced on eight lab computers joined by cables one at a time.
# parent[x] is x's parent in its tree; a root is its own parent and stands for its whole group. size[r] is the number
# of items in the tree of root r (it is only kept up to date for roots).


class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))   # every item starts as a group of its own
        self.size = [1] * n
        self.groups = n

    def find(self, x):
        root = x
        while self.parent[root] != root:       # walk up to the root
            root = self.parent[root]
        while self.parent[x] != root:          # path compression: point every item on the way at the root
            self.parent[x], x = root, self.parent[x]
        return root

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False                       # already in one group: nothing changes
        if self.size[ra] < self.size[rb]:      # union by size: the smaller tree goes under the larger root
            ra, rb = rb, ra
        self.parent[rb] = ra
        self.size[ra] += self.size[rb]
        self.groups -= 1
        return True

    def same(self, a, b):
        return self.find(a) == self.find(b)


uf = UnionFind(8)
print(f"{'start':<16} parent {uf.parent}  groups {uf.groups}")
steps = [("union", 0, 1), ("union", 2, 3), ("union", 1, 3), ("union", 4, 5), ("union", 6, 7), ("union", 5, 7),
         ("same", 3, 7), ("union", 1, 2), ("union", 3, 7), ("same", 0, 6)]
for op, a, b in steps:
    if op == "union":
        joined = uf.union(a, b)
        note = "" if joined else "  (already one group)"
        print(f"{f'union({a}, {b})':<16} parent {uf.parent}  groups {uf.groups}{note}")
    else:
        answer = "yes" if uf.same(a, b) else "no"
        print(f"{f'same({a}, {b})?':<16} parent {uf.parent}  {answer}")
print("sizes of the roots:", {r: uf.size[r] for r in range(8) if uf.parent[r] == r})

Output

start            parent [0, 1, 2, 3, 4, 5, 6, 7]  groups 8
union(0, 1)      parent [0, 0, 2, 3, 4, 5, 6, 7]  groups 7
union(2, 3)      parent [0, 0, 2, 2, 4, 5, 6, 7]  groups 6
union(1, 3)      parent [0, 0, 0, 2, 4, 5, 6, 7]  groups 5
union(4, 5)      parent [0, 0, 0, 2, 4, 4, 6, 7]  groups 4
union(6, 7)      parent [0, 0, 0, 2, 4, 4, 6, 6]  groups 3
union(5, 7)      parent [0, 0, 0, 2, 4, 4, 4, 6]  groups 2
same(3, 7)?      parent [0, 0, 0, 0, 4, 4, 4, 4]  no
union(1, 2)      parent [0, 0, 0, 0, 4, 4, 4, 4]  groups 2  (already one group)
union(3, 7)      parent [0, 0, 0, 0, 0, 4, 4, 4]  groups 1
same(0, 6)?      parent [0, 0, 0, 0, 0, 4, 0, 4]  yes
sizes of the roots: {0: 8}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 union_find.py

Each union of two separate groups lowers the count by one, from 8 to 1 over seven unions. The third cable, union(1, 3), finds the roots 0 and 2, two trees of two items, and hangs 2 under 0, so item 3 is now two steps from its root. The question same(3, 7) walks 3 → 2 → 0 and 7 → 6 → 4, answers no, and leaves parent[3] = 0 and parent[7] = 4 behind: those paths are one step long from now on. union(1, 2) changes nothing, because both items already reach root 0. The last union links root 4 under root 0, and the final question shortens the path of item 6.

Two rules that keep the trees short

Without any rule, unions can stack the trees into one long chain, and every find then walks the whole chain. The first rule decides which root goes on top. Union by size hangs the root of the smaller tree under the root of the larger one. An item then moves one level deeper only when its tree joins one at least as large, so the size of its tree at least doubles each time, and no tree is ever taller than log₂ n. Union by rank compares ranks instead of sizes, a rank being an upper bound on a tree’s height, and it caps the height just as well.

The second rule works inside find. Path compression points every item on the walk straight at the root, so the next find from any of them takes one step. Path halving points every other item at its grandparent during the same single walk, which needs no second loop.

Galler and Fischer described the structure in 1964. Tarjan proved in 1975 that union by rank with path compression runs any sequence of m operations in O(m α(m, n)) time, and Tarjan and van Leeuwen extended the bound to path halving and to union by size in 1984. A single operation can still be slower than average; the bound is for whole sequences.

Union-find on n items with union by size and path compression
Operation Time Extra space
Make n groups of one item O(n) O(n): two arrays
find(x) O(α(n)) amortised, O(log n) worst case O(1)
union(a, b) O(α(n)) amortised, O(log n) worst case O(1)
Split a group Not supported -

Exercise and self-check

Process unions and same-group questions on up to 200,000 items and return the number of groups after each union.

Exercise · Medium · Python, JavaScript

Groups after each union, and same-group questions

n items are numbered from 0 to n - 1, and each starts in a group of its own. Write group_counts(n, operations) (JavaScript: groupCounts) that processes the operations in order and returns one answer per operation:

  • ("union", a, b) (JavaScript: ["union", a, b]) puts the groups of a and b together; its answer is the number of groups right after it, which stays the same when a and b were already in one group;
  • ("same", a, b) asks whether a and b are in one group; its answer is True or False (JavaScript: true or false). Every item is in the same group as itself.

The sample tests replay the eight lab computers of the lesson, check repeated and redundant unions, compare your answers with a slow recount on random small cases, and finish with a chain of 200,000 items followed by 200,000 questions about it, which needs short trees and a find that does not recurse.

Python · Starter code · groups.py

def group_counts(n, operations):
    """One answer per operation: the number of groups after a union, True or False for a same-group question."""
    raise NotImplementedError("write group_counts")
The sample tests · test_groups.py
import random

from groups import group_counts

LAB = [("union", 0, 1), ("union", 2, 3), ("union", 1, 3), ("union", 4, 5), ("union", 6, 7), ("union", 5, 7),
       ("same", 3, 7), ("union", 1, 2), ("union", 3, 7), ("same", 0, 6)]


def slow_counts(n, operations):
    """Recolours every item after each union: right, and far too slow for large inputs."""
    label = list(range(n))
    out = []
    for op, a, b in operations:
        if op == "union":
            old, new = label[b], label[a]
            if old != new:
                label = [new if x == old else x for x in label]
            out.append(len(set(label)))
        else:
            out.append(label[a] == label[b])
    return out


def test_lab_computers():
    """the eight lab computers of the lesson"""
    assert group_counts(8, LAB) == [7, 6, 5, 4, 3, 2, False, 2, 1, True]


def test_redundant_and_self_unions():
    """a union of one group with itself, or of an item with itself, keeps the count"""
    ops = [("union", 0, 0), ("union", 0, 1), ("union", 1, 0), ("same", 2, 2), ("same", 0, 2)]
    assert group_counts(3, ops) == [3, 2, 2, True, False]


def test_single_item():
    """one item is one group, and in the same group as itself"""
    assert group_counts(1, [("same", 0, 0), ("union", 0, 0)]) == [True, 1]


def test_random_against_recount():
    """300 random small cases, compared with a slow recount"""
    rng = random.Random(5)
    for _ in range(300):
        n = rng.randint(1, 9)
        ops = [(rng.choice(["union", "same"]), rng.randrange(n), rng.randrange(n)) for _ in range(rng.randint(0, 15))]
        assert group_counts(n, ops) == slow_counts(n, ops), (n, ops)


def test_long_chain():
    """200,000 unions that form a chain, then 200,000 questions about its far ends"""
    n = 200_000
    ops = [("union", i, i + 1) for i in range(n - 1)] + [("same", 0, i) for i in range(n - 1, -1, -1)]
    answers = group_counts(n, ops)
    assert answers[: n - 1] == list(range(n - 1, 0, -1))
    assert all(answers[n - 1:])

JavaScript · Starter code · groups.mjs

/** One answer per operation: the number of groups after a union, true or false for a same-group question. */
export function groupCounts(n, operations) {
  throw new Error('write groupCounts');
}
The sample tests · groups.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { groupCounts } from './groups.mjs';

const LAB = [['union', 0, 1], ['union', 2, 3], ['union', 1, 3], ['union', 4, 5], ['union', 6, 7], ['union', 5, 7],
  ['same', 3, 7], ['union', 1, 2], ['union', 3, 7], ['same', 0, 6]];

/** Recolours every item after each union: right, and far too slow for large inputs. */
function slowCounts(n, operations) {
  let label = Array.from({ length: n }, (_, i) => i);
  const out = [];
  for (const [op, a, b] of operations) {
    if (op === 'union') {
      const oldLabel = label[b];
      const newLabel = label[a];
      if (oldLabel !== newLabel) label = label.map((x) => (x === oldLabel ? newLabel : x));
      out.push(new Set(label).size);
    } else out.push(label[a] === label[b]);
  }
  return out;
}

test('the eight lab computers of the lesson', () => {
  assert.deepEqual(groupCounts(8, LAB), [7, 6, 5, 4, 3, 2, false, 2, 1, true]);
});

test('a union of one group with itself, or of an item with itself, keeps the count', () => {
  const ops = [['union', 0, 0], ['union', 0, 1], ['union', 1, 0], ['same', 2, 2], ['same', 0, 2]];
  assert.deepEqual(groupCounts(3, ops), [3, 2, 2, true, false]);
});

test('one item is one group, and in the same group as itself', () => {
  assert.deepEqual(groupCounts(1, [['same', 0, 0], ['union', 0, 0]]), [true, 1]);
});

test('300 random small cases, compared with a slow recount', () => {
  let seed = 5;
  const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
  const pick = (k) => Math.floor(random() * k);
  for (let t = 0; t < 300; t++) {
    const n = 1 + pick(9);
    const ops = Array.from({ length: pick(16) }, () => [random() < 0.5 ? 'union' : 'same', pick(n), pick(n)]);
    assert.deepEqual(groupCounts(n, ops), slowCounts(n, ops), JSON.stringify([n, ops]));
  }
});

test('200,000 unions that form a chain, then 200,000 questions about its far ends', () => {
  const n = 200000;
  const ops = [];
  for (let i = 0; i < n - 1; i++) ops.push(['union', i, i + 1]);
  for (let i = n - 1; i >= 0; i--) ops.push(['same', 0, i]);
  const answers = groupCounts(n, ops);
  let ok = answers.length === 2 * n - 1;
  for (let i = 0; i < n - 1 && ok; i++) if (answers[i] !== n - 1 - i) ok = false;
  for (let i = n - 1; i < answers.length && ok; i++) if (answers[i] !== true) ok = false;
  assert.ok(ok, 'the counts fall from 199,999 to 1 and every question is answered true');
});
A hint

Keep parent, size and a running groups count, as in the lesson's code. A union first finds both roots: when they are equal, nothing changes and the answer is the current count; otherwise put the smaller tree under the larger root and subtract 1. Write find with a loop, not recursion, so a tall tree cannot exhaust the call stack.

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 Twelve items start in groups of their own. Seven unions join two different groups each, and three more unions name two items that are already in one group. How many groups are left?

    Type a number.

    Show the answer to question 1

    Answer: 5 groups

    Only a union of two different groups lowers the count, by one each time: 12 − 7 = 5. The three other unions find the same root twice and change nothing, which is why a correct union checks the roots before it counts.

  2. Question 2 of 5 With union by size and no path compression, what is the greatest possible height of a tree of 4,096 items?

    Type a number.

    Show the answer to question 2

    Answer: 12 levels

    An item moves one level down only when its tree joins one at least as large, so its tree's size at least doubles each time. A tree of 4,096 = 2¹² items allows at most 12 doublings, so no item is more than 12 steps from the root.

  3. Question 3 of 5 Which test tells correctly whether items a and b are in the same group?

    Choose one answer.

    Show the answer to question 3

    Answer: find(a) == find(b)

    Two items share a group exactly when they reach the same root. Their parents may differ while the roots agree, for example when one item sits two levels down and the other one level down in the same tree.

  4. Question 4 of 5 What does path compression change during find(x)?

    Choose one answer.

    Show the answer to question 4

    Answer: It points every item on the path from x to the root straight at the root

    Compression only rewrites the parent pointers of the items that find walked past, so the next find from any of them takes one step. Which tree goes under which root is the linking rule's job, and no operation of union-find ever splits a group.

  5. Question 5 of 5 A network loses links over time and must answer "are a and b still connected?" after each loss, with the losses not known in advance. Why is a plain union-find the wrong tool?

    Choose one answer.

    Show the answer to question 5

    Answer: It can merge groups but has no way to split one

    Union-find only ever merges: a union links two roots, and no operation ever undoes a link. A network that loses links needs a different approach.

Why it is nearly constant: the height bound proved and the steps counted Pro: opens with a Pro, Premium or Ultimate pass

Each rule caps the height of the trees in its own way, and together they keep a find below one pointer step on average. This part proves the logarithmic bound for union by size and counts the steps of four versions of the structure on up to a million items.

In this part:

  • Union by size keeps every tree within log₂ n levels: the proof
  • What path compression adds to the bound
  • Steps per find on four versions and three workloads, measured

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Union-find in five languages, with memory measured and four bugs reproduced Pro: opens with a Pro, Premium or Ultimate pass

The structure is the same two arrays in every language; what differs is the type of the arrays and what each entry costs. Below, a single program handles a million items identically in all five interview languages, followed by four bugs that slip through small tests.

In this part:

  • The same program in five languages, with run records
  • Four bugs that pass small tests, reproduced
  • Edge cases worth a test

4 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Loops, spanning trees, shrinking networks and a practice set Pro: opens with a Pro, Premium or Ultimate pass

A union that finds both items already joined is information too: the link it was asked to add closes a loop. This part uses that for loop checks and minimum spanning trees, groups accounts that share an identifier, answers questions about a network that loses links by running time backwards, and ends with a practice set.

In this part:

  • A failed union closes a loop, and Kruskal keeps the cheapest links
  • Groups that share an identifier
  • Questions about a shrinking network, answered backwards
  • A practice set, with worked answers

3 runnable examples (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    find(x) returned 4 a minute ago and returns 9 now, although nothing touched x itself. Is that a bug?

    No. find returns the representative of the group that holds x, the root of its tree, and a later union can hang that root under another one. When the group of x joined a larger group whose root is 9, root 4 became a child of 9, so every item of the old group, x included, now reaches 9. What union-find promises holds at every moment: two items are in one group exactly when find gives the same answer for both right now. So never keep a find result to compare later; call find again when you need it. Path compression rewrites pointers too, but it never changes which item is the root, so it cannot change the answer on its own.

  2. Mid to senior

    A deduplication job built on union-find has run for months. On one customer's file of 300,000 records it now crashes with a stack overflow, yet every test passes. What is the likely cause, how do you reproduce it in a test, and what do you change?

    Model answer, Pro. First step and follow-ups

  3. Mid to staff

    A matcher emits 30 million "same product" pairs over 8 million listings, and every listing needs a group id. A colleague stores the pairs as a graph and runs breadth-first search. Compare that with union-find in time, in memory, and when new pairs keep arriving every minute.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Union-find keeps groups that only merge: find returns a group’s root and union links two roots.
  • Two items share a group exactly when they have the same root; compare roots, never parents.
  • Union by size (or by rank) caps the height at log₂ n; path compression makes later finds take one step.
  • With both rules, m operations cost O(m α(n)), where α is at most 4 for any realistic n.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.