|
| 1 | +// Union-Find (Disjoint Set Union) implementation with path compression and union by rank |
| 2 | + |
| 3 | +// Detect cycle in undirected graph using Union-Find |
| 4 | +// Algorithm steps: |
| 5 | +// 1. Initialize each element to be its own parent (self root) and rank 0. |
| 6 | +// 2. For each union operation, find the roots of the elements. |
| 7 | +// 3. If roots are different, union them by rank. |
| 8 | +// 4. If roots are the same, a cycle is detected. |
| 9 | + |
| 10 | +// Time Complexity: O(α(N)) per operation, where α is the inverse Ackermann function |
| 11 | +// which grows very slowly, effectively constant for all practical N. |
| 12 | +// For interviews, we can consider it O(1) amortized time per operation. |
| 13 | +// Space Complexity: O(N) for storing parent and rank |
| 14 | + |
| 15 | +// usage of Union-Find data structure: |
| 16 | +// 1. Cycle detection in undirected graphs |
| 17 | +// 2. Connected components in graphs |
| 18 | +// 3. Kruskal's algorithm for Minimum Spanning Tree |
| 19 | +// 4. Network connectivity problems |
| 20 | + |
| 21 | +class UnionFind { |
| 22 | + constructor() { |
| 23 | + this.parent = new Map(); |
| 24 | + this.rank = new Map(); |
| 25 | + } |
| 26 | + |
| 27 | + find(x) { |
| 28 | + // Step 1: Initialize new element if not seen before |
| 29 | + if (!this.parent.has(x)) { |
| 30 | + this.parent.set(x, x); // Element is its own parent (root) |
| 31 | + this.rank.set(x, 0); // Initial rank is 0 |
| 32 | + } |
| 33 | + |
| 34 | + // Step 2: Path compression optimization |
| 35 | + if (this.parent.get(x) !== x) { |
| 36 | + this.parent.set(x, this.find(this.parent.get(x))); // Recursive call |
| 37 | + } |
| 38 | + |
| 39 | + // Step 3: Return the root |
| 40 | + return this.parent.get(x); |
| 41 | + } |
| 42 | + |
| 43 | + union(x, y) { |
| 44 | + const rootX = this.find(x) |
| 45 | + const rootY = this.find(y) |
| 46 | + |
| 47 | + if (rootX === rootY) { |
| 48 | + return false // Already in the same set |
| 49 | + } |
| 50 | + |
| 51 | + // Union by rank |
| 52 | + // Case 1: rootX has higher rank |
| 53 | + if (this.rank.get(rootX) > this.rank.get(rootY)) { |
| 54 | + this.parent.set(rootY, rootX) |
| 55 | + // No rank change needed - rootX's rank stays the same |
| 56 | + } |
| 57 | + |
| 58 | + // Case 2: rootY has higher rank |
| 59 | + else if (this.rank.get(rootX) < this.rank.get(rootY)) { |
| 60 | + this.parent.set(rootX, rootY) |
| 61 | + // No rank change needed - rootY's rank stays the same |
| 62 | + } |
| 63 | + |
| 64 | + // Case 3: Both have equal rank |
| 65 | + else { |
| 66 | + this.parent.set(rootY, rootX) // rootX becomes the new root |
| 67 | + this.rank.set(rootX, this.rank.get(rootX) + 1) // Only rootX rank increases |
| 68 | + } |
| 69 | + return true |
| 70 | + } |
| 71 | + |
| 72 | + connected(x, y) { |
| 73 | + return this.find(x) === this.find(y); |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | + |
| 78 | +// Example usage: |
| 79 | +const uf = new UnionFind(); |
| 80 | +uf.union(1, 2); |
| 81 | +uf.union(2, 3); |
| 82 | +uf.union(4, 5); |
| 83 | +uf.union(5, 6); |
| 84 | + |
| 85 | +console.log(uf.connected(1, 3)); // true |
| 86 | +console.log(uf.connected(1, 4)); // false |
| 87 | +uf.union(3, 4); |
| 88 | +console.log(uf.connected(1, 4)); // true |
| 89 | + |
| 90 | + |
| 91 | +// Questions for practice: |
| 92 | +// - Checking Existence of Edge Length Limited Paths |
0 commit comments