Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.thealgorithms.datastructures.disjointsetunion;

/**
* Disjoint Set Union (DSU) with Union by Size.
* This data structure tracks a set of elements partitioned into disjoint (non-overlapping) subsets.
* It supports two primary operations efficiently:
*
* <ul>
* <li>Find: Determine which subset a particular element belongs to.</li>
* <li>Union: Merge two subsets into a single subset using union by size.</li>
* </ul>
*
* Union by size always attaches the smaller tree under the root of the larger tree.
* This helps keep the tree shallow, improving the efficiency of find operations.
*
* @see <a href="https://en.wikipedia.org/wiki/Disjoint-set_data_structure">Disjoint Set Union (Wikipedia)</a>
*/
public class DisjointSetUnionBySize<T> {
/**
* Node class for DSU by size.
* Each node keeps track of its parent and the size of the set it represents.
*/
public static class Node<T> {
public T value;
public Node<T> parent;
public int size; // size of the set

public Node(T value) {
this.value = value;
this.parent = this;
this.size = 1; // initially, the set size is 1
}
}

/**
* Creates a new disjoint set containing the single specified element.
* @param value the element to be placed in a new singleton set
* @return a node representing the new set
*/
public Node<T> makeSet(final T value) {
return new Node<>(value);
}

/**
* Finds and returns the representative (root) of the set containing the given node.
* This method applies path compression to flatten the tree structure for future efficiency.
* @param node the node whose set representative is to be found
* @return the representative (root) node of the set
*/
public Node<T> findSet(Node<T> node) {
if (node != node.parent) {
node.parent = findSet(node.parent); // path compression
}
return node.parent;
}

/**
* Merges the sets containing the two given nodes using union by size.
* The root of the smaller set is attached to the root of the larger set.
* @param x a node in the first set
* @param y a node in the second set
*/
public void unionSets(Node<T> x, Node<T> y) {
Node<T> rootX = findSet(x);
Node<T> rootY = findSet(y);

if (rootX == rootY) {
return; // They are already in the same set
}
// Union by size: attach smaller tree under the larger one
if (rootX.size < rootY.size) {
rootX.parent = rootY;
rootY.size += rootX.size; // update size
} else {
rootY.parent = rootX;
rootX.size += rootY.size; // update size
}
}
}
// This implementation uses union by size instead of union by rank.
// The size field tracks the number of elements in each set.
// When two sets are merged, the smaller set is always attached to the larger set's root.
// This helps keep the tree shallow and improves the efficiency of find operations.
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.thealgorithms.datastructures.disjointsetunion;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;

public class DisjointSetUnionBySizeTest {

@Test
public void testMakeSet() {
DisjointSetUnionBySize<Integer> dsu = new DisjointSetUnionBySize<>();
DisjointSetUnionBySize.Node<Integer> node = dsu.makeSet(1);
assertNotNull(node);
assertEquals(node, node.parent);
assertEquals(1, node.size);
}

@Test
public void testUnionFindSet() {
DisjointSetUnionBySize<Integer> dsu = new DisjointSetUnionBySize<>();
DisjointSetUnionBySize.Node<Integer> node1 = dsu.makeSet(1);
DisjointSetUnionBySize.Node<Integer> node2 = dsu.makeSet(2);
DisjointSetUnionBySize.Node<Integer> node3 = dsu.makeSet(3);
DisjointSetUnionBySize.Node<Integer> node4 = dsu.makeSet(4);

dsu.unionSets(node1, node2);
dsu.unionSets(node3, node2);
dsu.unionSets(node3, node4);
dsu.unionSets(node1, node3);

DisjointSetUnionBySize.Node<Integer> root1 = dsu.findSet(node1);
DisjointSetUnionBySize.Node<Integer> root2 = dsu.findSet(node2);
DisjointSetUnionBySize.Node<Integer> root3 = dsu.findSet(node3);
DisjointSetUnionBySize.Node<Integer> root4 = dsu.findSet(node4);

assertEquals(root1, root2);
assertEquals(root1, root3);
assertEquals(root1, root4);
assertEquals(4, root1.size);
}

@Test
public void testFindSetOnSingleNode() {
DisjointSetUnionBySize<String> dsu = new DisjointSetUnionBySize<>();
DisjointSetUnionBySize.Node<String> node = dsu.makeSet("A");
assertEquals(node, dsu.findSet(node));
}

@Test
public void testUnionAlreadyConnectedNodes() {
DisjointSetUnionBySize<Integer> dsu = new DisjointSetUnionBySize<>();
DisjointSetUnionBySize.Node<Integer> node1 = dsu.makeSet(1);
DisjointSetUnionBySize.Node<Integer> node2 = dsu.makeSet(2);
DisjointSetUnionBySize.Node<Integer> node3 = dsu.makeSet(3);

dsu.unionSets(node1, node2);
dsu.unionSets(node2, node3);

// Union nodes that are already connected
dsu.unionSets(node1, node3);

// All should have the same root
DisjointSetUnionBySize.Node<Integer> root = dsu.findSet(node1);
assertEquals(root, dsu.findSet(node2));
assertEquals(root, dsu.findSet(node3));
assertEquals(3, root.size);
}

@Test
public void testMultipleMakeSets() {
DisjointSetUnionBySize<Integer> dsu = new DisjointSetUnionBySize<>();
DisjointSetUnionBySize.Node<Integer> node1 = dsu.makeSet(1);
DisjointSetUnionBySize.Node<Integer> node2 = dsu.makeSet(2);
DisjointSetUnionBySize.Node<Integer> node3 = dsu.makeSet(3);

assertNotEquals(node1, node2);
assertNotEquals(node2, node3);
assertNotEquals(node1, node3);

assertEquals(node1, node1.parent);
assertEquals(node2, node2.parent);
assertEquals(node3, node3.parent);
assertEquals(1, node1.size);
assertEquals(1, node2.size);
assertEquals(1, node3.size);
}

@Test
public void testPathCompression() {
DisjointSetUnionBySize<Integer> dsu = new DisjointSetUnionBySize<>();
DisjointSetUnionBySize.Node<Integer> node1 = dsu.makeSet(1);
DisjointSetUnionBySize.Node<Integer> node2 = dsu.makeSet(2);
DisjointSetUnionBySize.Node<Integer> node3 = dsu.makeSet(3);

dsu.unionSets(node1, node2);
dsu.unionSets(node2, node3);

// After findSet, path compression should update parent to root directly
DisjointSetUnionBySize.Node<Integer> root = dsu.findSet(node3);
assertEquals(root, node1);
assertEquals(node1, node3.parent);
assertEquals(3, root.size);
}

@Test
public void testMultipleDisjointSets() {
DisjointSetUnionBySize<Integer> dsu = new DisjointSetUnionBySize<>();
DisjointSetUnionBySize.Node<Integer> node1 = dsu.makeSet(1);
DisjointSetUnionBySize.Node<Integer> node2 = dsu.makeSet(2);
DisjointSetUnionBySize.Node<Integer> node3 = dsu.makeSet(3);
DisjointSetUnionBySize.Node<Integer> node4 = dsu.makeSet(4);
DisjointSetUnionBySize.Node<Integer> node5 = dsu.makeSet(5);
DisjointSetUnionBySize.Node<Integer> node6 = dsu.makeSet(6);

// Create two separate components
dsu.unionSets(node1, node2);
dsu.unionSets(node2, node3);

dsu.unionSets(node4, node5);
dsu.unionSets(node5, node6);

// Verify they are separate
assertEquals(dsu.findSet(node1), dsu.findSet(node2));
assertEquals(dsu.findSet(node2), dsu.findSet(node3));
assertEquals(dsu.findSet(node4), dsu.findSet(node5));
assertEquals(dsu.findSet(node5), dsu.findSet(node6));

assertNotEquals(dsu.findSet(node1), dsu.findSet(node4));
assertNotEquals(dsu.findSet(node3), dsu.findSet(node6));
}

@Test
public void testEmptyValues() {
DisjointSetUnionBySize<String> dsu = new DisjointSetUnionBySize<>();
DisjointSetUnionBySize.Node<String> emptyNode = dsu.makeSet("");
DisjointSetUnionBySize.Node<String> nullNode = dsu.makeSet(null);

assertEquals(emptyNode, dsu.findSet(emptyNode));
assertEquals(nullNode, dsu.findSet(nullNode));

dsu.unionSets(emptyNode, nullNode);
assertEquals(dsu.findSet(emptyNode), dsu.findSet(nullNode));
}
}