Chapter 13: Sets and Disjoint Sets
Introduction
Learning Outcomes
Explain the set ADT and Java's Set interface.
Use a multiset for problems where items repeat.
Implement path compression and union by rank.
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.
Set ADT
Set Operations
in A or in B
in both A and B
in A but not in 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
| Method | Description |
|---|---|
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
We create an empty HashSet: 16 empty buckets. Each element goes to bucket hash & 15.
The loop takes the next element: x = 4.
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).
The loop takes the next element: x = 7.
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).
The loop takes the next element: x = 2.
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).
The loop takes the next element: x = 7.
add(7): 7 is already in bucket 7; the set is unchanged and false comes back → the condition holds.
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
| Class | Internal structure | Iteration order | add / contains / remove |
|---|---|---|---|
| HashSet | hash table | depends on hash codes, unpredictable | O(1) average |
| LinkedHashSet | hash table + doubly linked list | insertion order | O(1) average |
| TreeSet | red-black tree | sorted (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)
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
HashSetuses aHashMapinternally.
Start: an empty table with 7 buckets. Position = hashCode(x) mod 7.
add(12): 12 mod 7 = 5 → bucket 5 is empty, so 12 goes in.
add(25): 25 mod 7 = 4 → into bucket 4.
add(7): 7 mod 7 = 0 → into bucket 0.
add(19): 19 mod 7 = 5 → collision! Bucket 5 holds 12; 19 goes at the end of the chain.
add(12) again: bucket 5 is scanned and 12 is found → not added, false is returned. This is what makes a set a set.
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; }
add(12): bucket no. = 12 % 7 = 5. The bucket is empty, so the scan ends at once.
12 is not in the bucket → appended to the chain, n = 1, return true.
add(25): bucket no. = 25 % 7 = 4. The bucket is empty, so the scan ends at once.
25 is not in the bucket → appended to the chain, n = 2, return true.
add(7): bucket no. = 7 % 7 = 0. The bucket is empty, so the scan ends at once.
7 is not in the bucket → appended to the chain, n = 3, return true.
add(19): bucket no. = 19 % 7 = 5. The chain in the bucket must be scanned: 12.
y = 12 ≠ 19 → move along the chain.
19 is not in the bucket → appended to the chain, n = 4, return true. (Collision: bucket 5 now holds 2 elements.)
add(12): bucket no. = 12 % 7 = 5. The chain in the bucket must be scanned: 12, 19.
y = 12 == 12 → the element is already in the set: return false, nothing is added. This is what makes a set a set.
contains(26): 26 % 7 = 5 → only bucket 5 is scanned; the other buckets are never examined.
y = 12 ≠ 26 → keep going.
y = 19 ≠ 26 → keep going.
End of chain, no 26 → return false. The average chain length n/m is constant → O(1).
contains(19): 19 % 7 = 5 → only bucket 5 is scanned; the other buckets are never examined.
y = 12 ≠ 19 → keep going.
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 (
ComparableorComparator). TreeSettreats two elements as the same whencompare(a, b) == 0, so the ordering must be consistent withequals.
Extra operations a sorted structure provides (S = {5, 10, 15, 20, 25, 30, 40}):
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++]); }
The sets are in sorted arrays. Like merge in merge sort, two pointers advance in a single pass (Goodrich: generic merge).
a[i] = 1 < b[j] = 3 → 1 is only in a: added to the union, i++.
a[i] = b[j] = 3 → in both: added once to the union and to the intersection; both pointers advance.
a[i] = 5 > b[j] = 4 → 4 is only in b: added to the union, j++.
a[i] = b[j] = 5 → in both: added once to the union and to the intersection; both pointers advance.
a[i] = 7 < b[j] = 8 → 7 is only in a: added to the union, i++.
a[i] = 9 > b[j] = 8 → 8 is only in b: added to the union, j++.
j = 4 = b.length → b is exhausted and the main loop exits. The intersection is complete: no more common elements are possible.
The 9 left in a goes straight into the union, i++.
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 list | Sorted array | Hash table | Balanced BST |
|---|---|---|---|---|
contains | O(n) | O(log n) | O(1)* | O(log n) |
add / remove | O(n)† | O(n) | O(1)* | O(log n) |
| A ∪ B, A ∩ B, A − B | O(n·m) | O(n + m) | O(n + m)* | O(m log(n+m)) |
| Sorted iteration | O(n log n) | O(n) | O(n log n) | O(n) |
| Min / floor / range | O(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.
Set Implementations
Sets as Bit Vectors (Bitsets)
| & &~.| Operation | int mask (U ≤ 32) | java.util.BitSet |
|---|---|---|
| add x / contains x? | a |= 1 << x · (a >> x & 1) == 1 | set(x) · get(x) |
| union / intersection | a | b · a & b | or(b) · and(b) |
| difference / size | a & ~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
Two empty sets: all bits 0.
1 << 1 = mask 2; a |= mask → bit 1 becomes 1: 1 is added to the set.
1 << 3 = mask 8; a |= mask → bit 3 becomes 1: 3 is added to the set.
1 << 4 = mask 16; a |= mask → bit 4 becomes 1: 4 is added to the set.
Add 3 to set b: bit 3 turns on.
Add 5 to set b: bit 5 turns on.
OR: 1 if either bit is 1 → union {1, 3, 4, 5}. A single machine instruction.
AND: 1 if both bits are 1 → intersection {3}.
~b flips the bits of b; a & ~b: in a but not in b → difference {1, 4}.
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.
| Element | Count |
|---|---|
| a | 2 |
| b | 1 |
| c | 3 |
M = {a, a, b, c, c, c} → map {a:2, b:1, c:3}
| Multiset ADT (Goodrich) | Meaning | With 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 0 | O(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; } }
count("a") → 2
add("a") has no effect; here the count goes up.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; }
merge: "a" was absent → added with count 1; n = 1.
merge: "b" was absent → added with count 1; n = 2.
merge: "a" was present → Integer::sum gives 1 + 1 = 2; n = 3. A Set would not have changed.
merge: "c" was absent → added with count 1; n = 4.
count("a"): getOrDefault → 2. A single hash lookup: O(1).
remove("a"): c = counts.get(e) = 2.
c = 2 > 1 → the count drops to 1 (the entry stays); n = 3.
remove("b"): c = counts.get(e) = 1.
c == 1 → the count would reach 0, so the entry is deleted entirely (zero counts are not stored); n = 2.
remove("x"): c = counts.get(e) = null.
c == null → the element is absent: return false, n is unchanged.
count("b"): getOrDefault → 0. A single hash lookup: O(1).
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>>.
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:
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 twofinds.
14 magnets, each in its own group: 14 disjoint sets. find(x) = x.
union(6, 8): the closest pair, 6 and 8, merge; now find(6) = find(8).
union(3, 4): 3 and 4 attract, forming the new group {3, 4}.
union(2, 3): 2 joins the group {3, 4} → {2, 3, 4}.
union(9, 10), union(13, 14), union(9, 13) → {9, 10, 13, 14}.
union(5, 7), union(7, 11), union(11, 12) → {5, 7, 11, 12}. Five groups remain (1 is alone).
Are 4 and 10 in the same group? find(4) ≠ find(10) → no. union(4, 10) merges the two groups.
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.
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] = istarts every element as the root of its own set (makeSet).
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
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.
union(C, K): find(C) = C, find(K) = K; sizes equal (1) → K goes under C (parent[9] = 4).
union(F, E): find(F) = F, find(E) = E; sizes equal (1) → E goes under F (parent[0] = 1).
union(A, J): find(A) = A, find(J) = J; sizes equal (1) → J goes under A (parent[6] = 5).
union(A, B): find(A) = A, find(B) = B; size 1 < 2 → B goes under A (parent[10] = 5).
union(C, D): find(C) = C, find(D) = D; size 1 < 2 → D goes under C (parent[3] = 4).
union(D, I): find(D) = C, find(I) = I; size 1 < 3 → I goes under C (parent[2] = 4).
union(L, F): find(L) = L, find(F) = F; size 1 < 2 → L goes under F (parent[7] = 1).
union(C, A): find(C) = C, find(A) = A; size 3 < 4 → A goes under C (parent[5] = 4).
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).
union(H, F): find(H) = H, find(F) = F; size 2 < 3 → H goes under F (parent[11] = 1).
union(H, B): find(H) = F, find(B) = C; size 5 < 7 → F goes under C (parent[1] = 4).
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]toid[q]→ the whole array is scanned, O(n).
after union(0,1), union(2,3): 4 sets (same color = same set)
| Method (Sedgewick) | find | union | total 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 compression | O(α(n)) amortized | ≈ O(n) | |
Disjoint Sets (Union-Find) · Step through the code
Code Trace: Quick-Find union
union(1, 3): p's set name is pid = id[1] = 1, q's is qid = id[3] = 3.
pid ≠ qid: different sets, so they must be merged. Now the whole array is scanned.
id[0] = 1 = pid → set id[0] = 3.
id[1] = 1 = pid → set id[1] = 3.
id[2] = 3 ≠ pid: left alone (but still examined).
id[3] = 3 ≠ pid: left alone (but still examined).
id[4] = 4 ≠ pid: left alone (but still examined).
id[5] = 5 ≠ pid: left alone (but still examined).
union(4, 3): p's set name is pid = id[4] = 4, q's is qid = id[3] = 3.
pid ≠ qid: different sets, so they must be merged. Now the whole array is scanned.
id[0] = 3 ≠ pid: left alone (but still examined).
id[1] = 3 ≠ pid: left alone (but still examined).
id[2] = 3 ≠ pid: left alone (but still examined).
id[3] = 3 ≠ pid: left alone (but still examined).
id[4] = 4 = pid → set id[4] = 3.
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.
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.
union(1, 2): two singleton sets; both methods put 1 under 2.
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.
union(3, 4): the naive chain grows one more level; by size, 4 again joins the root.
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).
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.
Two sets: {1, 4, 7} (size 3) and {2, 3, 6, 9} (size 4). Heights 1 and 2.
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.
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.
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.
Start: 3's root is 7, but the path is 3 → 6 → 5 → 7 (3 steps).
find(3): the recursion climbs 3 → 6 → 5 → 7 and finds the root, 7.
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
| Implementation | find | union | Note |
|---|---|---|---|
| Naive (arbitrary linking) | O(n) | O(n) | the tree can become a chain |
| By size or rank | O(log n) | O(log n) | height ≤ log₂ n |
| Path compression only | O(log n) amortized | for m operations | |
| Rank + path compression | O(α(n)) amortized | constant in practice | |
parent[n] and rank[n] (or size[n]) → O(n).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; } }
uf.union(0, 1); uf.union(3, 4);
uf.find(1) == uf.find(0) → true
uf.find(1) == uf.find(3) → false
union return boolean?It returns true if a merge happened: in Kruskal's algorithm, that answers "does this edge form a cycle?"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; }
6 elements, parent[i] = i, rank[i] = 0: 6 singleton sets (each root points to itself).
find(0): parent[0] == 0 → 0 is a root and is returned.
find(1): parent[1] == 1 → 1 is a root and is returned.
Equal ranks (0) → parent[0] = 1, and the new root's rank goes up: rank[1] = 1.
find(2): parent[2] == 2 → 2 is a root and is returned.
find(3): parent[3] == 3 → 3 is a root and is returned.
Equal ranks (0) → parent[2] = 3, and the new root's rank goes up: rank[3] = 1.
find(0): parent[0] = 1 ≠ 0 → not a root; climb with find(1).
find(1): parent[1] == 1 → 1 is a root and is returned.
On return, parent[0] = 1 (already a child of the root, unchanged); find(0) returns 1.
find(2): parent[2] = 3 ≠ 2 → not a root; climb with find(3).
find(3): parent[3] == 3 → 3 is a root and is returned.
On return, parent[2] = 3 (already a child of the root, unchanged); find(2) returns 3.
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; }
find(4): parent[4] == 4 → 4 is a root and is returned.
find(0): parent[0] = 1 ≠ 0 → not a root; climb with find(1).
find(1): parent[1] = 3 ≠ 1 → not a root; climb with find(3).
find(3): parent[3] == 3 → 3 is a root and is returned.
On return, parent[1] = 3 (already a child of the root, unchanged); find(1) returns 3.
Path compression: on return, parent[0] changes from 1 to 3; 0 now links directly to the root. find(0) = 3.
rank[4] = 0 < rank[3] = 2 → 4, the root of the shorter tree, goes under 3: parent[4] = 3. The height does not grow.
find(1): parent[1] = 3 ≠ 1 → not a root; climb with find(3).
find(3): parent[3] == 3 → 3 is a root and is returned.
On return, parent[1] = 3 (already a child of the root, unchanged); find(1) returns 3.
find(4): parent[4] = 3 ≠ 4 → not a root; climb with find(3).
find(3): parent[3] == 3 → 3 is a root and is returned.
On return, parent[4] = 3 (already a child of the root, unchanged); find(4) returns 3.
rx = ry = 3 → 1 and 4 are already in the same set: return false (in Kruskal: a cycle).
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
The chain 4 → 3 → 2 → 1 → 0 (the result of bad unions). find(4) first looks for the root: root = 4.
Loop 1: parent[4] = 3 ≠ 4 → move up, root = 3.
Loop 1: parent[3] = 2 ≠ 3 → move up, root = 2.
Loop 1: parent[2] = 1 ≠ 2 → move up, root = 1.
parent[1] = 0 → root = 0; since parent[0] = 0, the loop ends: root found (4 steps).
Loop 2: parent[4] = 3 is not the root → this node will be compressed.
First the old parent is saved (next = 3), then parent[4] = 0: 4 now links directly to the root.
One step up the path: x = 3.
Loop 2: parent[3] = 2 is not the root → this node will be compressed.
First the old parent is saved (next = 2), then parent[3] = 0: 3 now links directly to the root.
One step up the path: x = 2.
Loop 2: parent[2] = 1 is not the root → this node will be compressed.
First the old parent is saved (next = 1), then parent[2] = 0: 2 now links directly to the root.
One step up the path: x = 1.
parent[1] = 0 = root: 1 already links to the root, so the loop ends.
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
7 nodes, 5 edges. UnionFind(7): each node is the root of its own set (parent[i] = i).
No edges yet: 7 components.
Edge (0, 1): different roots → union merges the sets and returns true.
Two components became one: count = 6.
Edge (1, 2): different roots → union merges the sets and returns true.
Two components became one: count = 5.
Edge (3, 4): different roots → union merges the sets and returns true.
Two components became one: 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).
Edge (5, 6): different roots → union merges the sets and returns true.
Two components became one: 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
contains returns false.p.x = 5; set.contains(p) → falseset.remove(x) inside for-each → ConcurrentModificationException.set.removeIf(x -> x % 2 == 0);parent[x] = y is wrong: it splits x's tree. Correct:parent[find(x)] = find(y);rank[ry]++, rank no longer tracks height and the O(log n) guarantee is lost.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
| Structure | Core operations | Cost | When? |
|---|---|---|---|
| HashSet | add, remove, contains | O(1)* | order not needed |
| TreeSet | + first, floor, range | O(log n) | sorted access needed |
| Multisets | add, count, remove | O(1)* | repeat counts matter |
| Union-Find | find, union | O(α(n)) | disjoint sets, connectivity queries |
* average case. Union-Find cost is amortized, with rank + path compression.
HashSet for membership, TreeSet for sorted access, Union-Find for dynamic connectivity.Summary
Interactive Simulators
Summary
Sample Problems
- For A = {2, 4, 6, 8} and B = {1, 2, 3, 4}, what are A ∪ B, A ∩ B, and B − A?
- Which Set implementation would you choose to print the words of a dictionary in alphabetical order, without duplicates?
- Store the letters of "kayak" in a multiset. What do count('a') and size() return?
- 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)?
- 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
- A ∪ B = {1, 2, 3, 4, 6, 8}, A ∩ B = {2, 4}, B − A = {1, 3}.
- TreeSet (sorted, no duplicates, O(log n)).
- {k:2, a:2, y:1} → count('a') = 2, size() = 5.
- 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.
- 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
- 12, 28, 44, 7 are added to an empty HashSet (16 buckets, bucket = x & 15). Which buckets? What does
add(28)return? - U = {0..7}: A = 1011 0010₂, B = 0111 0100₂. What sets are A | B, A & B, and A & ~B?
- With Quick-Find, n − 1 unions on n elements: how many cells are examined in total?
- With
parent = [1, 2, 3, 3, 3], what isparentafter an iterativefind(0)? - In union by rank, how large can a rank get? Why?
- Edges arrive one at a time; after each one: "is the graph connected?" Which structure, at what cost?
Answers / hints
- 12, 28, 44 → bucket 12 (chain), 7 → bucket 7. 28 is a new element:
true. - A = {1, 4, 5, 7}, B = {2, 4, 5, 6} → ∪ {1, 2, 4, 5, 6, 7} · ∩ {4, 5} · A − B {1, 7}.
- Each union, n cells: (n − 1) · n → O(n²).
- Root 3;
parent = [3, 3, 3, 3, 3]. - ⌊log₂ n⌋: a tree of rank r has at least 2r elements.
- 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, andTreeSet.