Chapter 13: Sets and Disjoint Sets

Data Structures
Set ADTUnion, intersection, differenceHash- and tree-based sets MultisetsUnion-FindPath compression, rankConnected components
Sercan KÜLCÜ · Giresun University · Computer Engineering

Introduction

Learning Outcomes

The set model

Explain the set ADT and Java's Set interface.

Multisets

Use a multiset for problems where items repeat.

Union-Find

Implement path compression and union by rank.

Applications

Apply Union-Find to connectivity problems such as counting connected components.

Set ADT

Set

  • A set is a collection of unique elements: no element appears twice.
  • It is the data-structure counterpart of a set in mathematics.
  • Order does not matter: {1, 2, 3} = {3, 1, 2}.
  • The key question, "is x in the set?" (the membership test), must be answered fast.
S = {7, 2, 9, 5}7295add(2) → no change (2 already there)
Removing duplicatesThe distinct words in a list.new HashSet<>(list)
MembershipBanned users, visited nodes.
Set algebraMutual friends = intersection; all tags = union.

Set ADT

Set Operations

AB
Union A ∪ B
in A or in B
AB
Intersection A ∩ B
in both A and B
AB
Difference A − B
in A but not in B
AB
Subset A ⊆ B
every element of A is in B
A = {1, 2, 3, 4, 5}, B = {3, 4, 5, 6, 7}Result
Union A ∪ B{1, 2, 3, 4, 5, 6, 7}
Intersection A ∩ B{3, 4, 5}
Difference A − B  /  B − A{1, 2}  /  {6, 7}
Subset?A ⊄ B (1 ∉ B) · {3, 4} ⊆ A ✓

Set ADT

The java.util.Set Interface

MethodDescription
add(e)adds; false if present
remove(o), contains(o)removes; tests membership
addAll(T)union: S ← S ∪ T
retainAll(T)intersection: S ← S ∩ T
removeAll(T)difference: S ← S − T
Set<Integer> a = new HashSet<>(List.of(1,2,3,4,5));
Set<Integer> b = new HashSet<>(List.of(3,4,5,6,7));
Set<Integer> uni = new HashSet<>(a);
uni.addAll(b);     // [1..7]
Set<Integer> both = new HashSet<>(a);
both.retainAll(b); // [3, 4, 5]
Set<Integer> diff = new HashSet<>(a);
diff.removeAll(b); // [1, 2]
a.containsAll(b);  // false

addAll, retainAll, and removeAll modify the set in place; copy a first if you need to keep it.

Set ADT · Step through the code

Code Trace: First Repeat via the Result of add

a 4 0 7 1 2 2 7 3 4 4 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

We create an empty HashSet: 16 empty buckets. Each element goes to bucket hash & 15.

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 4 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

The loop takes the next element: x = 4.

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 4 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 3 4 4 5 6 7 8 9 10 11 12 13 14 15

add(4): bucket 4 is empty, so the element goes in and true comes back → !true is false and the loop continues. One bucket checked: O(1).

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 7 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 3 4 4 5 6 7 8 9 10 11 12 13 14 15

The loop takes the next element: x = 7.

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 7 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 3 4 4 5 6 7 7 8 9 10 11 12 13 14 15

add(7): bucket 7 is empty, so the element goes in and true comes back → !true is false and the loop continues. One bucket checked: O(1).

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 2 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 3 4 4 5 6 7 7 8 9 10 11 12 13 14 15

The loop takes the next element: x = 2.

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 2 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 2 3 4 4 5 6 7 7 8 9 10 11 12 13 14 15

add(2): bucket 2 is empty, so the element goes in and true comes back → !true is false and the loop continues. One bucket checked: O(1).

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 7 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 2 3 4 4 5 6 7 7 8 9 10 11 12 13 14 15

The loop takes the next element: x = 7.

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 7 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 2 3 4 4 5 6 7 7 8 9 10 11 12 13 14 15 already there!

add(7): 7 is already in bucket 7; the set is unchanged and false comes back → the condition holds.

a 4 0 7 1 2 2 7 3 4 4 ▲ x = 7 seen: HashSet, 16 buckets bucket = hash(x) & 15 (Integer: hash = x) 0 1 2 2 3 4 4 5 6 7 7 8 9 10 11 12 13 14 15 already there!

The first repeated element, 7, is returned. Total work O(n): one add per element, no nested loop.

int firstRepeat(int[] a) {
  Set<Integer> seen = new HashSet<>();
  for (int x : a) {
    if (!seen.add(x))   // not added?
      return x;            // already there: a repeat
  }
  return -1;               // no repeats
}

Set Implementations

java.util.Set Implementations

ClassInternal structureIteration orderadd / contains / remove
HashSethash tabledepends on hash codes, unpredictableO(1) average
LinkedHashSethash table + doubly linked listinsertion orderO(1) average
TreeSetred-black treesorted (ascending, alphabetical)O(log n)
for (String s : List.of("lemon", "peach", "fig", "lemon")) {
    hash.add(s);  linked.add(s);  tree.add(s);
}
// HashSet       → [lemon, fig, peach]   (bucket order: depends on hash)
// LinkedHashSet → [lemon, peach, fig]   (insertion order)
// TreeSet       → [fig, lemon, peach]   (alphabetical)
The repeated "lemon" appears once in all three. If order doesn't matter, use HashSet; for insertion order, LinkedHashSet; for sorted access, TreeSet.

Set Implementations

Hash-Based Set

  • The set is stored as a hash table that holds keys only, with no values.
  • Each element's position is computed quickly from its hash code.
  • Insertion, removal, and lookup take O(1) on average; with a bad hash, O(n) in the worst case.
  • Java's HashSet uses a HashMap internally.
0 1 2 3 4 5 6

Start: an empty table with 7 buckets. Position = hashCode(x) mod 7.

0 1 2 3 4 5 12 6

add(12): 12 mod 7 = 5 → bucket 5 is empty, so 12 goes in.

0 1 2 3 4 25 5 12 6

add(25): 25 mod 7 = 4 → into bucket 4.

0 7 1 2 3 4 25 5 12 6

add(7): 7 mod 7 = 0 → into bucket 0.

0 7 1 2 3 4 25 5 12 19 6

add(19): 19 mod 7 = 5 → collision! Bucket 5 holds 12; 19 goes at the end of the chain.

0 7 1 2 3 4 25 5 12 19 6

add(12) again: bucket 5 is scanned and 12 is found → not added, false is returned. This is what makes a set a set.

0 7 1 2 3 4 25 5 12 19 6

contains(26): 26 mod 7 = 5 → only bucket 5 (12, 19) is scanned; 26 is not found → false. The other buckets are never examined.

Set Implementations · Step through the code

Code Trace: add / contains (m = 7)

// fields: List<Integer>[] bucket; n; m = 7
boolean add(int x) {          // x ≥ 0
  int i = x % m;               // bucket no.
  for (int y : bucket[i])
    if (y == x) return false;  // already there
  bucket[i].add(x);
  n++;
  return true;
}
boolean contains(int x) {
  int i = x % m;
  for (int y : bucket[i])
    if (y == x) return true;
  return false;
}
0 1 2 3 4 5 6

add(12): bucket no. = 12 % 7 = 5. The bucket is empty, so the scan ends at once.

0 1 2 3 4 5 12 6

12 is not in the bucket → appended to the chain, n = 1, return true.

0 1 2 3 4 5 12 6

add(25): bucket no. = 25 % 7 = 4. The bucket is empty, so the scan ends at once.

0 1 2 3 4 25 5 12 6

25 is not in the bucket → appended to the chain, n = 2, return true.

0 1 2 3 4 25 5 12 6

add(7): bucket no. = 7 % 7 = 0. The bucket is empty, so the scan ends at once.

0 7 1 2 3 4 25 5 12 6

7 is not in the bucket → appended to the chain, n = 3, return true.

0 7 1 2 3 4 25 5 12 6

add(19): bucket no. = 19 % 7 = 5. The chain in the bucket must be scanned: 12.

0 7 1 2 3 4 25 5 12 6

y = 12 ≠ 19 → move along the chain.

0 7 1 2 3 4 25 5 12 19 6

19 is not in the bucket → appended to the chain, n = 4, return true. (Collision: bucket 5 now holds 2 elements.)

0 7 1 2 3 4 25 5 12 19 6

add(12): bucket no. = 12 % 7 = 5. The chain in the bucket must be scanned: 12, 19.

0 7 1 2 3 4 25 5 12 19 6

y = 12 == 12 → the element is already in the set: return false, nothing is added. This is what makes a set a set.

0 7 1 2 3 4 25 5 12 19 6

contains(26): 26 % 7 = 5 → only bucket 5 is scanned; the other buckets are never examined.

0 7 1 2 3 4 25 5 12 19 6

y = 12 ≠ 26 → keep going.

0 7 1 2 3 4 25 5 12 19 6

y = 19 ≠ 26 → keep going.

0 7 1 2 3 4 25 5 12 19 6

End of chain, no 26 → return false. The average chain length n/m is constant → O(1).

0 7 1 2 3 4 25 5 12 19 6

contains(19): 19 % 7 = 5 → only bucket 5 is scanned; the other buckets are never examined.

0 7 1 2 3 4 25 5 12 19 6

y = 12 ≠ 19 → keep going.

0 7 1 2 3 4 25 5 12 19 6

y = 19 == 19 → found, return true.

Set Implementations

Tree-Based (Sorted) Set

  • The set is stored as a balanced binary search tree (Java's TreeSet: a red-black tree).
  • Elements are kept sorted; an inorder traversal produces them in order.
  • Lookup, insertion, and removal take O(log n).
  • Elements must be comparable (Comparable or Comparator).
  • TreeSet treats two elements as the same when compare(a, b) == 0, so the ordering must be consistent with equals.
20 10 30 5 15 25 40

Extra operations a sorted structure provides (S = {5, 10, 15, 20, 25, 30, 40}):

first / last5 / 40
floor(17)largest ≤ 1715
ceiling(17)smallest ≥ 1720
subSet(10, 30)range query[10, 15, 20, 25]

Set Implementations · Step through the code

Union and Intersection of Sorted Arrays (Two Pointers)

void unionIntersection(int[] a, int[] b,
    List<Integer> uni, List<Integer> both) {
  int i = 0, j = 0;
  while (i < a.length && j < b.length) {
    if (a[i] < b[j]) uni.add(a[i++]);
    else if (a[i] > b[j]) uni.add(b[j++]);
    else {                     // in both
      uni.add(a[i]);  both.add(a[i]);
      i++;  j++;
    }
  }
  while (i < a.length) uni.add(a[i++]);
  while (j < b.length) uni.add(b[j++]);
}
a 1 3 5 7 9 ▲ i = 0 b 3 4 5 8 ▲ j = 0 uni both

The sets are in sorted arrays. Like merge in merge sort, two pointers advance in a single pass (Goodrich: generic merge).

a 1 3 5 7 9 ▲ i = 1 b 3 4 5 8 ▲ j = 0 uni 1 both

a[i] = 1 < b[j] = 3 → 1 is only in a: added to the union, i++.

a 1 3 5 7 9 ▲ i = 2 b 3 4 5 8 ▲ j = 1 uni 1 3 both 3

a[i] = b[j] = 3 → in both: added once to the union and to the intersection; both pointers advance.

a 1 3 5 7 9 ▲ i = 2 b 3 4 5 8 ▲ j = 2 uni 1 3 4 both 3

a[i] = 5 > b[j] = 4 → 4 is only in b: added to the union, j++.

a 1 3 5 7 9 ▲ i = 3 b 3 4 5 8 ▲ j = 3 uni 1 3 4 5 both 3 5

a[i] = b[j] = 5 → in both: added once to the union and to the intersection; both pointers advance.

a 1 3 5 7 9 ▲ i = 4 b 3 4 5 8 ▲ j = 3 uni 1 3 4 5 7 both 3 5

a[i] = 7 < b[j] = 8 → 7 is only in a: added to the union, i++.

a 1 3 5 7 9 ▲ i = 4 b 3 4 5 8 ▲ j = 4 (done) uni 1 3 4 5 7 8 both 3 5

a[i] = 9 > b[j] = 8 → 8 is only in b: added to the union, j++.

a 1 3 5 7 9 ▲ i = 4 b 3 4 5 8 ▲ j = 4 (done) uni 1 3 4 5 7 8 both 3 5

j = 4 = b.length → b is exhausted and the main loop exits. The intersection is complete: no more common elements are possible.

a 1 3 5 7 9 ▲ i = 5 (done) b 3 4 5 8 ▲ j = 4 (done) uni 1 3 4 5 7 8 9 both 3 5

The 9 left in a goes straight into the union, i++.

a 1 3 5 7 9 ▲ i = 5 (done) b 3 4 5 8 ▲ j = 4 (done) uni 1 3 4 5 7 8 9 both 3 5

Nothing is left in b. Union [1, 3, 4, 5, 7, 8, 9], intersection [3, 5]. Each step advances one pointer → at most n + m steps: O(n + m).

Set Implementations

Comparing Set Implementations

Operation (|A| = n, |B| = m)Unsorted listSorted arrayHash tableBalanced BST
containsO(n)O(log n)O(1)*O(log n)
add / removeO(n)†O(n)O(1)*O(log n)
A ∪ B, A ∩ B, A − BO(n·m)O(n + m)O(n + m)*O(m log(n+m))
Sorted iterationO(n log n)O(n)O(n log n)O(n)
Min / floor / rangeO(n)O(log n)O(n)O(log n)

* average case (worst case O(n)). † a lookup is needed first to rule out duplicates. Union with a hash table: contains + add for each element of B.

Default choice: hashThe fastest membership test when order doesn't matter.
Need order? Use a treeEvery operation O(log n), and ordered queries come free.

Set Implementations

Sets as Bit Vectors (Bitsets)

7 6 5 4 3 2 1 0 A 0 0 0 1 1 0 1 0 = 26 → {1, 3, 4} B 0 0 1 0 1 0 0 0 = 40 → {3, 5} A | B 0 0 1 1 1 0 1 0 = 58 → {1, 3, 4, 5}
IdeaWith universe 0..U−1, bit i is 1 ⇔ i is in the set.
One element: O(1)Add, remove, membership = a single bit operation.
Set algebra: O(U / 64)∪ ∩ − word by word with | & &~.
Operationint mask (U ≤ 32)java.util.BitSet
add x / contains x?a |= 1 << x · (a >> x & 1) == 1set(x) · get(x)
union / intersectiona | b · a & bor(b) · and(b)
difference / sizea & ~b · Integer.bitCount(a)andNot(b) · cardinality()

Memory is U bits: a universe of 10⁶ elements ≈ 122 KB. If the universe is large but the set sparse (e.g. 3 elements, U = 10⁹), use a hash-based set.

Set Implementations · Step through the code

Code Trace: Set Algebra with Bit Operations

7 6 5 4 3 2 1 0 a 0 0 0 0 0 0 0 0 = {} b 0 0 0 0 0 0 0 0 = {}

Two empty sets: all bits 0.

7 6 5 4 3 2 1 0 a 0 0 0 0 0 0 1 0 = {1} b 0 0 0 0 0 0 0 0 = {} 1 << x 0 0 0 0 0 0 1 0 mask = 2

1 << 1 = mask 2; a |= mask → bit 1 becomes 1: 1 is added to the set.

7 6 5 4 3 2 1 0 a 0 0 0 0 1 0 1 0 = {1, 3} b 0 0 0 0 0 0 0 0 = {} 1 << x 0 0 0 0 1 0 0 0 mask = 8

1 << 3 = mask 8; a |= mask → bit 3 becomes 1: 3 is added to the set.

7 6 5 4 3 2 1 0 a 0 0 0 1 1 0 1 0 = {1, 3, 4} b 0 0 0 0 0 0 0 0 = {} 1 << x 0 0 0 1 0 0 0 0 mask = 16

1 << 4 = mask 16; a |= mask → bit 4 becomes 1: 4 is added to the set.

7 6 5 4 3 2 1 0 a 0 0 0 1 1 0 1 0 = {1, 3, 4} b 0 0 0 0 1 0 0 0 = {3} 1 << x 0 0 0 0 1 0 0 0 mask = 8

Add 3 to set b: bit 3 turns on.

7 6 5 4 3 2 1 0 a 0 0 0 1 1 0 1 0 = {1, 3, 4} b 0 0 1 0 1 0 0 0 = {3, 5} 1 << x 0 0 1 0 0 0 0 0 mask = 32

Add 5 to set b: bit 5 turns on.

7 6 5 4 3 2 1 0 a 0 0 0 1 1 0 1 0 = {1, 3, 4} b 0 0 1 0 1 0 0 0 = {3, 5} a | b 0 0 1 1 1 0 1 0 = {1, 3, 4, 5}

OR: 1 if either bit is 1 → union {1, 3, 4, 5}. A single machine instruction.

7 6 5 4 3 2 1 0 a 0 0 0 1 1 0 1 0 = {1, 3, 4} b 0 0 1 0 1 0 0 0 = {3, 5} a & b 0 0 0 0 1 0 0 0 = {3}

AND: 1 if both bits are 1 → intersection {3}.

7 6 5 4 3 2 1 0 a 0 0 0 1 1 0 1 0 = {1, 3, 4} b 0 0 1 0 1 0 0 0 = {3, 5} a & ~b 0 0 0 1 0 0 1 0 = {1, 4}

~b flips the bits of b; a & ~b: in a but not in b → difference {1, 4}.

7 6 5 4 3 2 1 0 a 0 0 0 1 1 0 1 0 = {1, 3, 4} b 0 0 1 0 1 0 0 0 = {3, 5} a >> 3 0 0 0 0 0 0 1 1 & 1 → 1

Membership test: a >> 3 shifts bit 3 to the far right and & 1 isolates it → 1, so 3 ∈ a. O(1).

int a = 0, b = 0;
for (int x : new int[]{1, 3, 4})
  a |= 1 << x;          // set bit x
for (int x : new int[]{3, 5})
  b |= 1 << x;
int uni = a | b;        // union
int both = a & b;       // intersection
int diff = a & ~b;      // difference
boolean has3 = (a >> 3 & 1) == 1;

Multisets

Multiset (Bag)

  • A multiset (or bag) is a set in which the same element can appear more than once.
  • Order still doesn't matter; what matters is how many times each element occurs (its multiplicity).
  • Natural implementation: an element → count map.
  • Examples: word frequencies in a text, a shopping cart, a vote count.
ElementCount
a2
b1
c3

M = {a, a, b, c, c, c} → map {a:2, b:1, c:3}

Multiset ADT (Goodrich)MeaningWith hashing
add(e) / add(e, n)add e once / n times (count += n)O(1)*
count(e)how many times is e there? (0 if absent)O(1)*
remove(e) / remove(e, n)decrement the count; delete the entry at 0O(1)*
size()number of elements, repeats included (6 here)O(1)

Multisets

A Multiset in Java

class Multiset<E> {
    private final Map<E, Integer> counts = new HashMap<>();
    private int n = 0;                                   // number of elements, repeats included
    void add(E e) { counts.merge(e, 1, Integer::sum); n++; }  // 1 if absent, else +1
    int count(E e) { return counts.getOrDefault(e, 0); }
    boolean remove(E e) {
        Integer c = counts.get(e);
        if (c == null) return false;                     // e is absent
        if (c == 1) counts.remove(e);                       // counter would hit 0: delete entry
        else counts.put(e, c - 1);
        n--;
        return true;
    }
    int size() { return n; }
}
Usageadd("a"); add("a");
count("a") → 2
Unlike a setIn a Set the second add("a") has no effect; here the count goes up.
Sorted multisetUse TreeMap<E, Integer> for sorted iteration.

Multisets · Step through the code

Code Trace: Multiset Counters

void add(E e) {
  counts.merge(e, 1, Integer::sum);  n++;
}
int count(E e) {
  return counts.getOrDefault(e, 0);
}
boolean remove(E e) {
  Integer c = counts.get(e);
  if (c == null) return false;
  if (c == 1) counts.remove(e);
  else counts.put(e, c - 1);
  n--;
  return true;
}
counts (element → count) element count "a" 1 n (repeats included) 1 M = {a}

merge: "a" was absent → added with count 1; n = 1.

counts (element → count) element count "a" 1 "b" 1 n (repeats included) 2 M = {a, b}

merge: "b" was absent → added with count 1; n = 2.

counts (element → count) element count "a" 2 "b" 1 n (repeats included) 3 M = {a, a, b}

merge: "a" was present → Integer::sum gives 1 + 1 = 2; n = 3. A Set would not have changed.

counts (element → count) element count "a" 2 "b" 1 "c" 1 n (repeats included) 4 M = {a, a, b, c}

merge: "c" was absent → added with count 1; n = 4.

counts (element → count) element count "a" 2 "b" 1 "c" 1 n (repeats included) 4 M = {a, a, b, c}

count("a"): getOrDefault → 2. A single hash lookup: O(1).

counts (element → count) element count "a" 2 "b" 1 "c" 1 n (repeats included) 4 M = {a, a, b, c}

remove("a"): c = counts.get(e) = 2.

counts (element → count) element count "a" 1 "b" 1 "c" 1 n (repeats included) 3 M = {a, b, c}

c = 2 > 1 → the count drops to 1 (the entry stays); n = 3.

counts (element → count) element count "a" 1 "b" 1 "c" 1 n (repeats included) 3 M = {a, b, c}

remove("b"): c = counts.get(e) = 1.

counts (element → count) element count "a" 1 "c" 1 "b"0 (deleted) n (repeats included) 2 M = {a, c}

c == 1 → the count would reach 0, so the entry is deleted entirely (zero counts are not stored); n = 2.

counts (element → count) element count "a" 1 "c" 1 "x" not in the map n (repeats included) 2 M = {a, c}

remove("x"): c = counts.get(e) = null.

counts (element → count) element count "a" 1 "c" 1 "x" not in the map n (repeats included) 2 M = {a, c}

c == null → the element is absent: return false, n is unchanged.

counts (element → count) element count "a" 1 "c" 1 "b" not in the map n (repeats included) 2 M = {a, c}

count("b"): getOrDefault → 0. A single hash lookup: O(1).

counts (element → count) element count "a" 1 "c" 1 n (repeats included) 2 M = {a, c}

Result: M = {a, c}, n = 2. Every operation is one hash access → O(1) on average.

Multisets

Multimap

  • In a multimap, one key can map to several values (course → students).
  • Goodrich ADT: get(k) returns all values; put(k, v), remove(k, v), removeAll(k); size() counts all (k, v) pairs.
  • Java has no built-in class: build one from Map<K, List<V>>.
CS201 Ann Dana Ben CS203 Dana MATH101 Ben Carol
Map<String, List<String>> m = new HashMap<>();
void put(String k, String v) {   // create an empty list if absent
  m.computeIfAbsent(k, x -> new ArrayList<>()).add(v);
}
List<String> get(String k) {
  return m.getOrDefault(k, List.of());
}

Unlike a multiset, it stores the values themselves, not just how many. With hashing, put / get are O(1)* and remove(k, v) is O(1 + list length).

Disjoint Sets (Union-Find)

Disjoint Sets and Union-Find

  • Disjoint sets: a collection of sets that share no elements. Every element belongs to exactly one set.
  • Union-Find (Disjoint Set Union, DSU) is the data structure that maintains them. It has two main operations:
find(x)Returns the representative (root) of the set containing x.find(x) == find(y) ⇔ same set
union(x, y)Merges the sets of x and y into one.{1,2} ∪ {3} → {1,2,3}
0 1 2 5 6 3 9 4 8 7

Each set is a tree: every node points to its parent; the root points to itself and represents the set.

Disjoint Sets (Union-Find)

Magnet Example

  • Picture strong magnets: once two touch, they never come apart.
  • As they merge, we want to keep track of which magnet is in which group.
  • First, each magnet gets a number.
  • A merge is a union; "same group?" compares two finds.
1 2 3 4 5 6 7 8 9 10 11 12 13 14

14 magnets, each in its own group: 14 disjoint sets. find(x) = x.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

union(6, 8): the closest pair, 6 and 8, merge; now find(6) = find(8).

1 2 3 4 5 6 7 8 9 10 11 12 13 14

union(3, 4): 3 and 4 attract, forming the new group {3, 4}.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

union(2, 3): 2 joins the group {3, 4} → {2, 3, 4}.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

union(9, 10), union(13, 14), union(9, 13) → {9, 10, 13, 14}.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

union(5, 7), union(7, 11), union(11, 12) → {5, 7, 11, 12}. Five groups remain (1 is alone).

1 2 3 4 5 6 7 8 9 10 11 12 13 14

Are 4 and 10 in the same group? find(4) ≠ find(10) → no. union(4, 10) merges the two groups.

1 2 3 4 5 6 7 8 9 10 11 12 13 14

The merging goes on until all magnets end up in one group: find(x) gives the same root for everyone.

Disjoint Sets (Union-Find)

The Find and Union Operations

  • find(x) returns the name (root) of the set containing x by following parent links until it reaches the root.
  • This is not the search of a search tree: no value is looked for; we simply climb to the root.
  • union(x, y) replaces two sets with their union by attaching one root below the other.
The sets are disjoint to begin with, so no duplicate check is needed: only one parent link changes.
find(x):
  if x.parent != x:
    x.parent = find(x.parent)  // path compression
  return x.parent

union(x, y):
  root1 = find(x)
  root2 = find(y)
  if root1 != root2:
    // make the root of one set
    // a child of the other's root
    root1.parent = root2

Disjoint Sets (Union-Find)

Mapping Objects to Indices

  • Union-Find is most efficient on an array, with elements numbered 0..n−1.
  • If the elements are letters, cities, people…, each object is first mapped to an index arbitrarily (but permanently), and the mapping is stored in a hash table.
  • Then parent[i] = i starts every element as the root of its own set (makeSet).
E → 0F → 1I → 2D → 3C → 4A → 5J → 6L → 7G → 8K → 9B → 10H → 11
Map<Character, Integer> index = new HashMap<>();   // 'E'→0, 'F'→1, …
int[] parent = new int[12];
for (int i = 0; i < 12; i++) parent[i] = i;              // 12 singleton sets

Disjoint Sets (Union-Find)

The Union Operation Step by Step

element index parent E 0 0 F 1 1 I 2 2 D 3 3 C 4 4 A 5 5 J 6 6 L 7 7 G 8 8 K 9 9 B 10 10 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

Start: 12 singleton sets, parent[i] = i. Rule: the root of the smaller tree goes under the root of the larger; on a tie, the second root goes under the first. No path compression.

element index parent E 0 0 F 1 1 I 2 2 D 3 3 C 4 4 A 5 5 J 6 6 L 7 7 G 8 8 K 9 4 B 10 10 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(C, K): find(C) = C, find(K) = K; sizes equal (1) → K goes under C (parent[9] = 4).

element index parent E 0 1 F 1 1 I 2 2 D 3 3 C 4 4 A 5 5 J 6 6 L 7 7 G 8 8 K 9 4 B 10 10 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(F, E): find(F) = F, find(E) = E; sizes equal (1) → E goes under F (parent[0] = 1).

element index parent E 0 1 F 1 1 I 2 2 D 3 3 C 4 4 A 5 5 J 6 5 L 7 7 G 8 8 K 9 4 B 10 10 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(A, J): find(A) = A, find(J) = J; sizes equal (1) → J goes under A (parent[6] = 5).

element index parent E 0 1 F 1 1 I 2 2 D 3 3 C 4 4 A 5 5 J 6 5 L 7 7 G 8 8 K 9 4 B 10 5 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(A, B): find(A) = A, find(B) = B; size 1 < 2 → B goes under A (parent[10] = 5).

element index parent E 0 1 F 1 1 I 2 2 D 3 4 C 4 4 A 5 5 J 6 5 L 7 7 G 8 8 K 9 4 B 10 5 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(C, D): find(C) = C, find(D) = D; size 1 < 2 → D goes under C (parent[3] = 4).

element index parent E 0 1 F 1 1 I 2 4 D 3 4 C 4 4 A 5 5 J 6 5 L 7 7 G 8 8 K 9 4 B 10 5 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(D, I): find(D) = C, find(I) = I; size 1 < 3 → I goes under C (parent[2] = 4).

element index parent E 0 1 F 1 1 I 2 4 D 3 4 C 4 4 A 5 5 J 6 5 L 7 1 G 8 8 K 9 4 B 10 5 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(L, F): find(L) = L, find(F) = F; size 1 < 2 → L goes under F (parent[7] = 1).

element index parent E 0 1 F 1 1 I 2 4 D 3 4 C 4 4 A 5 4 J 6 5 L 7 1 G 8 8 K 9 4 B 10 5 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(C, A): find(C) = C, find(A) = A; size 3 < 4 → A goes under C (parent[5] = 4).

element index parent E 0 1 F 1 1 I 2 4 D 3 4 C 4 4 A 5 4 J 6 5 L 7 1 G 8 11 K 9 4 B 10 5 H 11 11 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(A, B): find(A) = find(B) = C → already in the same set; nothing changes. union(H, G): find(H) = H, find(G) = G; sizes equal (1) → G goes under H (parent[8] = 11).

element index parent E 0 1 F 1 1 I 2 4 D 3 4 C 4 4 A 5 4 J 6 5 L 7 1 G 8 11 K 9 4 B 10 5 H 11 1 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(H, F): find(H) = H, find(F) = F; size 2 < 3 → H goes under F (parent[11] = 1).

element index parent E 0 1 F 1 4 I 2 4 D 3 4 C 4 4 A 5 4 J 6 5 L 7 1 G 8 11 K 9 4 B 10 5 H 11 1 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

union(H, B): find(H) = F, find(B) = C; size 5 < 7 → F goes under C (parent[1] = 4).

element index parent E 0 1 F 1 4 I 2 4 D 3 4 C 4 4 A 5 4 J 6 5 L 7 1 G 8 11 K 9 4 B 10 5 H 11 1 Operations union(C, K) union(F, E) union(A, J) union(A, B) union(C, D) union(D, I) union(L, F) union(C, A) union(A, B) union(H, G) union(H, F) union(H, B) E F I D C A J L G K B H

Result: one set with root C. Are H and B in the same set? find(H): H → F → C, find(B): B → A → C; same root → yes (4 steps in all).

Disjoint Sets (Union-Find)

Another Idea: Quick-Find

  • A flat array instead of a tree: id[i] = the set name of i; elements of the same set share an id. find(p) = id[p] → O(1).
  • union(p, q): set every entry equal to id[p] to id[q] → the whole array is scanned, O(n).
i id[i] 0 1 2 3 4 5 1 1 3 3 4 5 sets: {0, 1} {2, 3} {4} {5}

after union(0,1), union(2,3): 4 sets (same color = same set)

Method (Sedgewick)finduniontotal for n − 1 unions
Quick-Find (array, set name)O(1)O(n)O(n²)
Quick-Union (parent tree, naive)O(h), worst case O(n)O(h)worst case O(n²)
Rank/size + path compressionO(α(n)) amortized≈ O(n)

Disjoint Sets (Union-Find) · Step through the code

Code Trace: Quick-Find union

i id[i] 0 1 2 3 4 5 1 1 3 3 4 5 pid = 1 qid = 3 sets: {0, 1} {2, 3} {4} {5}

union(1, 3): p's set name is pid = id[1] = 1, q's is qid = id[3] = 3.

i id[i] 0 1 2 3 4 5 1 1 3 3 4 5 pid = 1 qid = 3 sets: {0, 1} {2, 3} {4} {5}

pid ≠ qid: different sets, so they must be merged. Now the whole array is scanned.

i id[i] 0 1 2 3 4 5 3 1 3 3 4 5 ▲ i pid = 1 qid = 3 sets: {0, 2, 3} {1} {4} {5}

id[0] = 1 = pid → set id[0] = 3.

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 1 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[1] = 1 = pid → set id[1] = 3.

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 1 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[2] = 3 ≠ pid: left alone (but still examined).

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 1 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[3] = 3 ≠ pid: left alone (but still examined).

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 1 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[4] = 4 ≠ pid: left alone (but still examined).

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 1 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[5] = 5 ≠ pid: left alone (but still examined).

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 pid = 4 qid = 3 sets: {0, 1, 2, 3} {4} {5}

union(4, 3): p's set name is pid = id[4] = 4, q's is qid = id[3] = 3.

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 pid = 4 qid = 3 sets: {0, 1, 2, 3} {4} {5}

pid ≠ qid: different sets, so they must be merged. Now the whole array is scanned.

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 4 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[0] = 3 ≠ pid: left alone (but still examined).

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 4 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[1] = 3 ≠ pid: left alone (but still examined).

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 4 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[2] = 3 ≠ pid: left alone (but still examined).

i id[i] 0 1 2 3 4 5 3 3 3 3 4 5 ▲ i pid = 4 qid = 3 sets: {0, 1, 2, 3} {4} {5}

id[3] = 3 ≠ pid: left alone (but still examined).

i id[i] 0 1 2 3 4 5 3 3 3 3 3 5 ▲ i pid = 4 qid = 3 sets: {0, 1, 2, 3, 4} {5}

id[4] = 4 = pid → set id[4] = 3.

i id[i] 0 1 2 3 4 5 3 3 3 3 3 5 ▲ i pid = 4 qid = 3 sets: {0, 1, 2, 3, 4} {5}

id[5] = 5 ≠ pid: left alone. Each union examined all 6 cells: O(n) for n elements; n − 1 unions → O(n²).

int find(int p) { return id[p]; }  // O(1)
void union(int p, int q) {
  int pid = id[p], qid = id[q];
  if (pid == qid) return;
  for (int i = 0; i < id.length; i++)
    if (id[i] == pid)
      id[i] = qid;           // O(n) scan
}

Disjoint Sets (Union-Find)

Why Do We Need Improvements?

  • find takes as many steps as the node's distance from the root. A bad sequence of unions turns the tree into a chain → O(n) in the worst case.
Naive: old root under new By size: smaller under larger 1 2 3 4 5 1 2 3 4 5 height 0 · find(1) = 0 steps height 0 · find(1) = 0 steps

Start: 5 singleton sets; each node is its own root. We will do union(1,2), union(2,3), union(3,4), union(4,5) in order.

Naive: old root under new By size: smaller under larger 1 2 3 4 5 1 2 3 4 5 height 1 · find(1) = 1 step height 1 · find(1) = 1 step

union(1, 2): two singleton sets; both methods put 1 under 2.

Naive: old root under new By size: smaller under larger 1 2 3 4 5 1 2 3 4 5 height 2 · find(1) = 2 steps height 1 · find(1) = 1 step

union(2, 3): the naive method puts root 2 (a 2-element tree) under the singleton 3 → the chain grows. By size: the singleton 3 goes under 2, the root of the larger tree.

Naive: old root under new By size: smaller under larger 1 2 3 4 5 1 2 3 4 5 height 3 · find(1) = 3 steps height 1 · find(1) = 1 step

union(3, 4): the naive chain grows one more level; by size, 4 again joins the root.

Naive: old root under new By size: smaller under larger 1 2 3 4 5 1 2 3 4 5 height 4 · find(1) = 4 steps height 1 · find(1) = 1 step

union(4, 5): the naive tree is now a chain (height n − 1 → find is O(n)); the by-size tree is a star (height 1).

Two simple ideas keep the trees flat: (1) union by size or rank and (2) path compression. Together they give nearly constant time per operation: O(α(n)).

Disjoint Sets (Union-Find)

Union by Size

  • Store each tree's size (number of elements) at its root.
  • When merging, attach the root of the smaller tree to the root of the larger; the new size is the sum of the two.
1 4 7 2 3 6 9size 3size 4

Two sets: {1, 4, 7} (size 3) and {2, 3, 6, 9} (size 4). Heights 1 and 2.

1 4 7 2 3 6 9

Naive union: if root 2 of the larger tree goes under the smaller one, the height rises to 3 and 9's find gets longer.

2 1 4 7 3 6 9

By size: root 1 of the smaller tree goes under root 2 of the larger. The height stays 2, and size[2] = 3 + 4 = 7.

Disjoint Sets (Union-Find)

Union by Rank

  • The rank is an upper bound on the height; a new set has rank 0.
  • The root of lower rank goes under the root of higher rank.
  • If the ranks are equal, the new root's rank goes up by one.
  • This rule keeps the height O(log n) even without path compression.
Path compression lowers the height but does not update the rank, so the rank is only an upper bound.
makeSet(x):
  parent[x] = x
  rank[x] = 0

find(x):   // with path compression
  if x != parent[x]:
    parent[x] = find(parent[x])
  return parent[x]

link(x, y):   // x, y are roots
  if rank[x] > rank[y]:
    parent[y] = x
  else:
    parent[x] = y
    if rank[x] == rank[y]:
      rank[y] = rank[y] + 1

union(x, y):
  link(find(x), find(y))

Disjoint Sets (Union-Find)

Path Compression

Path compression: during find, attach every node you pass directly to the root. Later finds finish in one step.

1 2 7 5 6 3 10 4 8 9

Start: 3's root is 7, but the path is 3 → 6 → 5 → 7 (3 steps).

1 2 7 5 6 3 10 4 8 9

find(3): the recursion climbs 3 → 6 → 5 → 7 and finds the root, 7.

1 2 7 3 6 10 5 4 8 9

On the way back, each node's parent becomes 7: parent[3] = parent[6] = 7 (5 was already on 7). Now find(3) and find(6) take one step, and 10's path is shorter too.

Disjoint Sets (Union-Find)

Union-Find Complexity

ImplementationfindunionNote
Naive (arbitrary linking)O(n)O(n)the tree can become a chain
By size or rankO(log n)O(log n)height ≤ log₂ n
Path compression onlyO(log n) amortizedfor m operations
Rank + path compressionO(α(n)) amortizedconstant in practice
α(n): the inverse Ackermann functionIt grows extremely slowly: α(n) ≤ 4 even for as many elements as there are atoms in the universe.
MemoryTwo integer arrays, parent[n] and rank[n] (or size[n]) → O(n).
Tarjan (1975): a sequence of m operations finishes in O(m · α(n)) time. Used in Kruskal's algorithm, connected components, image processing (region labeling), and network connectivity queries.

Disjoint Sets (Union-Find)

Java: Node-Based (Object) Union-Find

class SetNode {
  int data;
  int rank;   // bound on height
  SetNode parent;

  SetNode(int data) {
    this.data = data;
    this.parent = this;
    this.rank = 0;
  }
}

SetNode find(SetNode node) {
  if (node != node.parent)
    node.parent = find(node.parent); // compression
  return node.parent;
}
void union(SetNode x, SetNode y) {
  SetNode rootX = find(x);
  SetNode rootY = find(y);
  if (rootX == rootY) return;
  // union by rank
  if (rootX.rank < rootY.rank) {
    rootX.parent = rootY;
  } else if (rootX.rank > rootY.rank) {
    rootY.parent = rootX;
  } else { // equal: pick one
    rootX.parent = rootY;
    rootY.rank++;
  }
}

Disjoint Sets (Union-Find)

Java: Array-Based Union-Find

class UnionFind {
  private final int[] parent, rank;
  UnionFind(int n) {
    parent = new int[n];  rank = new int[n];
    for (int i = 0; i < n; i++) parent[i] = i;
  }
  int find(int x) {
    if (parent[x] != x) parent[x] = find(parent[x]);
    return parent[x];
  }
  boolean union(int x, int y) {
    int rx = find(x), ry = find(y);
    if (rx == ry) return false;
    if (rank[rx] < rank[ry]) parent[rx] = ry;
    else if (rank[rx] > rank[ry]) parent[ry] = rx;
    else { parent[rx] = ry; rank[ry]++; }
    return true;
  }
}
UsageUnionFind uf = new UnionFind(5);
uf.union(0, 1); uf.union(3, 4);
uf.find(1) == uf.find(0) → true
uf.find(1) == uf.find(3) → false
Why does union return boolean?It returns true if a merge happened: in Kruskal's algorithm, that answers "does this edge form a cycle?"
Deep recursionFor very large n, find can be written with a loop instead.

Disjoint Sets (Union-Find) · Step through the code

Code Trace: find + union

int find(int x) {
  if (parent[x] != x) parent[x] = find(parent[x]);
  return parent[x];
}
boolean union(int x, int y) {
  int rx = find(x), ry = find(y);
  if (rx == ry) return false;
  if (rank[rx] < rank[ry]) parent[rx] = ry;
  else if (rank[rx] > rank[ry]) parent[ry] = rx;
  else { parent[rx] = ry; rank[ry]++; }
  return true;
}
0 1 2 3 4 5 i 0 1 2 3 4 5 parent 0 1 2 3 4 5 rank 0 0 0 0 0 0

6 elements, parent[i] = i, rank[i] = 0: 6 singleton sets (each root points to itself).

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 0 1 2 3 4 5 rank 0 0 0 0 0 0

find(0): parent[0] == 0 → 0 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 0 1 2 3 4 5 rank 0 0 0 0 0 0

find(1): parent[1] == 1 → 1 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 2 3 4 5 rank 0 1 0 0 0 0

Equal ranks (0) → parent[0] = 1, and the new root's rank goes up: rank[1] = 1.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 2 3 4 5 rank 0 1 0 0 0 0

find(2): parent[2] == 2 → 2 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 2 3 4 5 rank 0 1 0 0 0 0

find(3): parent[3] == 3 → 3 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 3 3 4 5 rank 0 1 0 1 0 0

Equal ranks (0) → parent[2] = 3, and the new root's rank goes up: rank[3] = 1.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 3 3 4 5 rank 0 1 0 1 0 0

find(0): parent[0] = 1 ≠ 0 → not a root; climb with find(1).

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 3 3 4 5 rank 0 1 0 1 0 0

find(1): parent[1] == 1 → 1 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 3 3 4 5 rank 0 1 0 1 0 0

On return, parent[0] = 1 (already a child of the root, unchanged); find(0) returns 1.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 3 3 4 5 rank 0 1 0 1 0 0

find(2): parent[2] = 3 ≠ 2 → not a root; climb with find(3).

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 3 3 4 5 rank 0 1 0 1 0 0

find(3): parent[3] == 3 → 3 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 1 3 3 4 5 rank 0 1 0 1 0 0

On return, parent[2] = 3 (already a child of the root, unchanged); find(2) returns 3.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 3 3 3 4 5 rank 0 1 0 2 0 0

Equal ranks (1) → parent[1] = 3, and the new root's rank goes up: rank[3] = 2.

Disjoint Sets (Union-Find) · Step through the code

Code Trace: find + union (cont.)

int find(int x) {
  if (parent[x] != x) parent[x] = find(parent[x]);
  return parent[x];
}
boolean union(int x, int y) {
  int rx = find(x), ry = find(y);
  if (rx == ry) return false;
  if (rank[rx] < rank[ry]) parent[rx] = ry;
  else if (rank[rx] > rank[ry]) parent[ry] = rx;
  else { parent[rx] = ry; rank[ry]++; }
  return true;
}
0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 3 3 3 4 5 rank 0 1 0 2 0 0

find(4): parent[4] == 4 → 4 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 3 3 3 4 5 rank 0 1 0 2 0 0

find(0): parent[0] = 1 ≠ 0 → not a root; climb with find(1).

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 3 3 3 4 5 rank 0 1 0 2 0 0

find(1): parent[1] = 3 ≠ 1 → not a root; climb with find(3).

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 3 3 3 4 5 rank 0 1 0 2 0 0

find(3): parent[3] == 3 → 3 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 1 3 3 3 4 5 rank 0 1 0 2 0 0

On return, parent[1] = 3 (already a child of the root, unchanged); find(1) returns 3.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 4 5 rank 0 1 0 2 0 0

Path compression: on return, parent[0] changes from 1 to 3; 0 now links directly to the root. find(0) = 3.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

rank[4] = 0 < rank[3] = 2 → 4, the root of the shorter tree, goes under 3: parent[4] = 3. The height does not grow.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

find(1): parent[1] = 3 ≠ 1 → not a root; climb with find(3).

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

find(3): parent[3] == 3 → 3 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

On return, parent[1] = 3 (already a child of the root, unchanged); find(1) returns 3.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

find(4): parent[4] = 3 ≠ 4 → not a root; climb with find(3).

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

find(3): parent[3] == 3 → 3 is a root and is returned.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

On return, parent[4] = 3 (already a child of the root, unchanged); find(4) returns 3.

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

rx = ry = 3 → 1 and 4 are already in the same set: return false (in Kruskal: a cycle).

0 1 2 3 4 5 i 0 1 2 3 4 5 parent 3 3 3 3 3 5 rank 0 1 0 2 0 0

One set, root 3. Path compression moved 0 up to the root; rank[3] = 2 is only an upper bound. Together: ≈ O(α(n)) per operation.

Disjoint Sets (Union-Find) · Step through the code

Code Trace: Iterative find with Path Compression

0 1 2 3 4 x, root i parent 0 0 0 1 1 2 2 3 3 4

The chain 4 → 3 → 2 → 1 → 0 (the result of bad unions). find(4) first looks for the root: root = 4.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 2 3 3 4

Loop 1: parent[4] = 3 ≠ 4 → move up, root = 3.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 2 3 3 4

Loop 1: parent[3] = 2 ≠ 3 → move up, root = 2.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 2 3 3 4

Loop 1: parent[2] = 1 ≠ 2 → move up, root = 1.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 2 3 3 4

parent[1] = 0 → root = 0; since parent[0] = 0, the loop ends: root found (4 steps).

0 1 2 3 4 x root i parent 0 0 0 1 1 2 2 3 3 4

Loop 2: parent[4] = 3 is not the root → this node will be compressed.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 2 3 0 4

First the old parent is saved (next = 3), then parent[4] = 0: 4 now links directly to the root.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 2 3 0 4

One step up the path: x = 3.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 2 3 0 4

Loop 2: parent[3] = 2 is not the root → this node will be compressed.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 0 3 0 4

First the old parent is saved (next = 2), then parent[3] = 0: 3 now links directly to the root.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 0 3 0 4

One step up the path: x = 2.

0 1 2 3 4 x root i parent 0 0 0 1 1 2 0 3 0 4

Loop 2: parent[2] = 1 is not the root → this node will be compressed.

0 1 2 3 4 x root i parent 0 0 0 1 0 2 0 3 0 4

First the old parent is saved (next = 1), then parent[2] = 0: 2 now links directly to the root.

0 1 2 3 4 x root i parent 0 0 0 1 0 2 0 3 0 4

One step up the path: x = 1.

0 1 2 3 4 x root i parent 0 0 0 1 0 2 0 3 0 4

parent[1] = 0 = root: 1 already links to the root, so the loop ends.

0 1 2 3 4 root i parent 0 0 0 1 0 2 0 3 0 4

The root 0 is returned. Every node on the path now links to the root, so later finds take 1 step. No recursion → no risk of stack overflow either.

int find(int x) {
  int root = x;
  while (parent[root] != root) root = parent[root];
  while (parent[x] != root) {    // path compression
    int next = parent[x];
    parent[x] = root;
    x = next;
  }
  return root;
}

Disjoint Sets (Union-Find) · Application · Step through the code

Code Trace: Counting Connected Components with Union-Find

0 1 2 3 4 5 6 i parent rank 0 0 1 1 2 2 3 3 4 4 5 5 6 6 0 0 0 0 0 0 0 count = 7

7 nodes, 5 edges. UnionFind(7): each node is the root of its own set (parent[i] = i).

0 1 2 3 4 5 6 i parent rank 0 0 1 1 2 2 3 3 4 4 5 5 6 6 0 0 0 0 0 0 0 count = 7

No edges yet: 7 components.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 2 2 3 3 4 4 5 5 6 6 0 1 0 0 0 0 0 count = 7

Edge (0, 1): different roots → union merges the sets and returns true.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 2 2 3 3 4 4 5 5 6 6 0 1 0 0 0 0 0 count = 6

Two components became one: count = 6.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 1 2 3 3 4 4 5 5 6 6 0 1 0 0 0 0 0 count = 6

Edge (1, 2): different roots → union merges the sets and returns true.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 1 2 3 3 4 4 5 5 6 6 0 1 0 0 0 0 0 count = 5

Two components became one: count = 5.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 1 2 4 3 4 4 5 5 6 6 0 1 0 0 1 0 0 count = 5

Edge (3, 4): different roots → union merges the sets and returns true.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 1 2 4 3 4 4 5 5 6 6 0 1 0 0 1 0 0 count = 4

Two components became one: count = 4.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 1 2 4 3 4 4 5 5 6 6 0 1 0 0 1 0 0 count = 4

Edge (0, 2): find(0) = find(2) → already in the same component; union returns false and count is unchanged (this edge closes a cycle).

0 1 2 3 4 5 6 i parent rank 1 0 1 1 1 2 4 3 4 4 6 5 6 6 0 1 0 0 1 0 1 count = 4

Edge (5, 6): different roots → union merges the sets and returns true.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 1 2 4 3 4 4 6 5 6 6 0 1 0 0 1 0 1 count = 3

Two components became one: count = 3.

0 1 2 3 4 5 6 i parent rank 1 0 1 1 1 2 4 3 4 4 6 5 6 6 0 1 0 0 1 0 1 count = 3

Result: 3 components: {0, 1, 2}, {3, 4}, {5, 6}. Total O(n + m·α(n)); even if edges arrive later, the count is always current (dynamic connectivity).

int countComponents(int n, int[][] edges) {
  UnionFind uf = new UnionFind(n);
  int count = n;        // n singleton sets
  for (int[] e : edges)
    if (uf.union(e[0], e[1]))
      count--;          // two components merged
  return count;
}

Summary

Common Mistakes

1. Changing an element inside a setAn object whose hashCode changes after insertion stays in the wrong bucket, so contains returns false.p.x = 5; set.contains(p) → false
2. equals without hashCodeTwo "equal" objects will probably land in different buckets, so the HashSet holds them twice. Override both together.
3. Removing while iteratingset.remove(x) inside for-each → ConcurrentModificationException.set.removeIf(x -> x % 2 == 0);
4. Linking elements instead of rootsparent[x] = y is wrong: it splits x's tree. Correct:parent[find(x)] = find(y);
5. Forgetting to bump equal ranksWithout rank[ry]++, rank no longer tracks height and the O(log n) guarantee is lost.
6. A compare inconsistent with TreeSet's needsOf distinct objects with compare(a, b) == 0, only one is kept (e.g. when sorting by surname alone).

Lab · Java code step by step

Lab: Set Operations

Summary

Summary

StructureCore operationsCostWhen?
HashSetadd, remove, containsO(1)*order not needed
TreeSet+ first, floor, rangeO(log n)sorted access needed
Multisetsadd, count, removeO(1)*repeat counts matter
Union-Findfind, unionO(α(n))disjoint sets, connectivity queries

* average case. Union-Find cost is amortized, with rank + path compression.

Choice: HashSet for membership, TreeSet for sorted access, Union-Find for dynamic connectivity.

Summary

Interactive Simulators

Set ADTAdding and removing elements, membership tests, union, intersection, and difference.
Disjoint Set Union (Union-Find)find and union, union by rank, and path compression, shown on trees.

Summary

Sample Problems

  1. For A = {2, 4, 6, 8} and B = {1, 2, 3, 4}, what are A ∪ B, A ∩ B, and B − A?
  2. Which Set implementation would you choose to print the words of a dictionary in alphabetical order, without duplicates?
  3. Store the letters of "kayak" in a multiset. What do count('a') and size() return?
  4. On 0..5 we do union(0,1), union(2,3), union(1,3), union(4,5) in order (by rank; on a tie, parent[x] = y). How many sets remain? What is find(0)?
  5. On 0..4 we do union(0,1), union(1,2), union(3,4), union(2,0) in order. Which union returns false? How many connected components remain?
Show answers
  1. A ∪ B = {1, 2, 3, 4, 6, 8}, A ∩ B = {2, 4}, B − A = {1, 3}.
  2. TreeSet (sorted, no duplicates, O(log n)).
  3. {k:2, a:2, y:1} → count('a') = 2, size() = 5.
  4. union(0,1): parent[0]=1, rank[1]=1 · union(2,3): parent[2]=3, rank[3]=1 · union(1,3): tie → parent[1]=3, rank[3]=2 · union(4,5): parent[4]=5. Two sets: {0,1,2,3} and {4,5}; find(0) = 3.
  5. union(2,0) returns false: 0 and 2 are already in the same set (the edge closes a cycle). Components {0, 1, 2} and {3, 4} → 2.

Summary

Exercises

  1. 12, 28, 44, 7 are added to an empty HashSet (16 buckets, bucket = x & 15). Which buckets? What does add(28) return?
  2. U = {0..7}: A = 1011 0010₂, B = 0111 0100₂. What sets are A | B, A & B, and A & ~B?
  3. With Quick-Find, n − 1 unions on n elements: how many cells are examined in total?
  4. With parent = [1, 2, 3, 3, 3], what is parent after an iterative find(0)?
  5. In union by rank, how large can a rank get? Why?
  6. Edges arrive one at a time; after each one: "is the graph connected?" Which structure, at what cost?
Answers / hints
  1. 12, 28, 44 → bucket 12 (chain), 7 → bucket 7. 28 is a new element: true.
  2. A = {1, 4, 5, 7}, B = {2, 4, 5, 6} → ∪ {1, 2, 4, 5, 6, 7} · ∩ {4, 5} · A − B {1, 7}.
  3. Each union, n cells: (n − 1) · n → O(n²).
  4. Root 3; parent = [3, 3, 3, 3, 3].
  5. ⌊log₂ n⌋: a tree of rank r has at least 2r elements.
  6. Union-Find + a counter (count−− on each successful union; count == 1 ⇔ connected): O(m·α(n)).

Summary

Further Reading

  • Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
  • Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, 4th ed., 2022.
  • Sedgewick and Wayne, Algorithms, 4th ed., 2011.
  • Oracle, Java SE 17 API documentation for Set, HashSet, and TreeSet.

The End

Chapter 13: Sets and Disjoint Sets
Next deck: Chapter 14: Graphs · Study questions · Simulators: Set ADT Simulator, Disjoint Sets Simulator · Glossary
1 / 1 Sercan KÜLCÜ. All rights reserved.