diff --git a/src/main/java/com/thealgorithms/greedyalgorithms/KruskalAlgorithm.java b/src/main/java/com/thealgorithms/greedyalgorithms/KruskalAlgorithm.java new file mode 100644 index 000000000000..e0e64071e097 --- /dev/null +++ b/src/main/java/com/thealgorithms/greedyalgorithms/KruskalAlgorithm.java @@ -0,0 +1,167 @@ +package com.thealgorithms.greedyalgorithms; + +/** + * An encapsulated, self-contained implementation of Kruskal's algorithm + * for computing the Minimum Spanning Tree (MST) of a weighted, undirected graph. + * You can find more about this algorithm in the following link: + * Kruskal algorithm - Geeks for Geeks + *

+ * To avoid namespace conflicts and maintain isolation within larger projects, + * all collaborators (Edge, Graph, DisjointSet) are implemented as private + * static nested classes. This ensures no type leakage outside this file while + * preserving clean internal architecture. + *

+ * + *

Usage

+ *
+ *     KruskalAlgorithm algo = new KruskalAlgorithm();
+ *     KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(4);
+ *     graph.addEdge(0,1,10);
+ *     graph.addEdge(1,2,5);
+ *     List<KruskalAlgorithm.Edge> mst = algo.computeMST(graph);
+ * 
+ * + *

Design Notes

+ * + */ +public class KruskalAlgorithm { + + /** + * Computes the Minimum Spanning Tree (or Minimum Spanning Forest if the graph + * is disconnected) using Kruskal’s greedy strategy. + * + * @param graph the graph instance to process + * @return a list of edges forming the MST + */ + public java.util.List computeMST(Graph graph) { + java.util.List mst = new java.util.ArrayList<>(); + java.util.List edges = new java.util.ArrayList<>(graph.edges); + + // Sort edges by ascending weight + java.util.Collections.sort(edges); + + DisjointSet ds = new DisjointSet(graph.numberOfVertices); + + for (Edge e : edges) { + int rootA = ds.find(e.source); + int rootB = ds.find(e.target); + + if (rootA != rootB) { + mst.add(e); + ds.union(rootA, rootB); + + if (mst.size() == graph.numberOfVertices - 1) { + break; + } + } + } + + return mst; + } + + /** + * Represents an immutable weighted edge between two vertices. + */ + public static final class Edge implements Comparable { + private final int source; + private final int target; + private final int weight; + + public Edge(int source, int target, int weight) { + if (weight < 0) { + throw new IllegalArgumentException("Weight cannot be negative."); + } + this.source = source; + this.target = target; + this.weight = weight; + } + + public int getSource() { + return source; + } + + public int getTarget() { + return target; + } + + public int getWeight() { + return weight; + } + + @Override + public int compareTo(Edge o) { + return Integer.compare(this.weight, o.weight); + } + } + + /** + * Lightweight graph representation consisting solely of vertices and edges. + * All algorithmic behavior is delegated to higher-level components. + */ + public static final class Graph { + private final int numberOfVertices; + private final java.util.List edges = new java.util.ArrayList<>(); + + public Graph(int numberOfVertices) { + if (numberOfVertices <= 0) { + throw new IllegalArgumentException("Graph must have at least one vertex."); + } + this.numberOfVertices = numberOfVertices; + } + + /** + * Adds an undirected edge to the graph. + */ + public void addEdge(int source, int target, int weight) { + if (source < 0 || source >= numberOfVertices || target < 0 || target >= numberOfVertices) { + throw new IndexOutOfBoundsException("Vertex index out of range."); + } + + edges.add(new Edge(source, target, weight)); + } + } + + /** + * Disjoint Set Union data structure supporting path compression + * and union-by-rank — essential for cycle detection in Kruskal's algorithm. + */ + private static final class DisjointSet { + private final int[] parent; + private final int[] rank; + + DisjointSet(int size) { + parent = new int[size]; + rank = new int[size]; + for (int i = 0; i < size; i++) { + parent[i] = i; + } + } + + public int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); // Path compression + } + return parent[x]; + } + + public void union(int a, int b) { + int ra = find(a); + int rb = find(b); + if (ra == rb) { + return; + } + if (rank[ra] < rank[rb]) { + parent[ra] = rb; + } else if (rank[ra] > rank[rb]) { + parent[rb] = ra; + } else { + parent[rb] = ra; + rank[ra]++; + } + } + } +} diff --git a/src/test/java/com/thealgorithms/greedyalgorithms/KruskalAlgorithmTest.java b/src/test/java/com/thealgorithms/greedyalgorithms/KruskalAlgorithmTest.java new file mode 100644 index 000000000000..bffeca4fd5c8 --- /dev/null +++ b/src/test/java/com/thealgorithms/greedyalgorithms/KruskalAlgorithmTest.java @@ -0,0 +1,212 @@ +package com.thealgorithms.greedyalgorithms; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test suite for the KruskalAlgorithm implementation. + * Ensures correctness, stability, and coverage of all internal logic. + */ +public class KruskalAlgorithmTest { + + // ------------------------------------------------------------- + // BASIC ALGORITHM CORRECTNESS + // ------------------------------------------------------------- + + @Test + @DisplayName("MST for a normal connected graph is computed correctly") + void testBasicMST() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(4); + + graph.addEdge(0, 1, 10); + graph.addEdge(0, 2, 6); + graph.addEdge(0, 3, 5); + graph.addEdge(2, 3, 4); + + KruskalAlgorithm algo = new KruskalAlgorithm(); + List mst = algo.computeMST(graph); + + assertEquals(3, mst.size()); + + int weight = mst.stream().mapToInt(KruskalAlgorithm.Edge::getWeight).sum(); + assertEquals(19, weight); + } + + @Test + @DisplayName("Single-vertex graph must return empty MST") + void testSingleVertexGraph() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(1); + + KruskalAlgorithm algo = new KruskalAlgorithm(); + assertTrue(algo.computeMST(graph).isEmpty()); + } + + @Test + @DisplayName("Graph with no edges returns empty MST") + void testGraphWithNoEdges() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(5); + KruskalAlgorithm algo = new KruskalAlgorithm(); + + assertTrue(algo.computeMST(graph).isEmpty()); + } + + @Test + @DisplayName("Disconnected graph produces a forest") + void testDisconnectedGraph() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(4); + + graph.addEdge(0, 1, 3); + graph.addEdge(2, 3, 1); + + KruskalAlgorithm algo = new KruskalAlgorithm(); + List mst = algo.computeMST(graph); + + assertEquals(2, mst.size()); + } + + // ------------------------------------------------------------- + // GRAPH CONSTRUCTOR & EDGE VALIDATION + // ------------------------------------------------------------- + + @Test + @DisplayName("Graph constructor rejects invalid vertex counts") + void testInvalidGraphSize() { + assertThrows(IllegalArgumentException.class, () -> new KruskalAlgorithm.Graph(0)); + assertThrows(IllegalArgumentException.class, () -> new KruskalAlgorithm.Graph(-3)); + } + + @Test + @DisplayName("Invalid edge indices throw exceptions") + void testInvalidEdgeVertices() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(3); + + assertThrows(IndexOutOfBoundsException.class, () -> graph.addEdge(-1, 1, 2)); + assertThrows(IndexOutOfBoundsException.class, () -> graph.addEdge(0, 3, 2)); + } + + @Test + @DisplayName("Negative weight edge must throw exception") + void testNegativeWeightEdge() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(3); + assertThrows(IllegalArgumentException.class, () -> graph.addEdge(0, 1, -5)); + } + + @Test + @DisplayName("Zero-weight edges are accepted") + void testZeroWeightEdge() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(2); + assertDoesNotThrow(() -> graph.addEdge(0, 1, 0)); + } + + // ------------------------------------------------------------- + // EDGE COMPARISON & SORTING BEHAVIOR + // ------------------------------------------------------------- + + @Test + @DisplayName("Edges are sorted correctly when weights are equal") + void testEdgeSortingTies() { + KruskalAlgorithm.Edge e1 = new KruskalAlgorithm.Edge(0, 1, 5); + KruskalAlgorithm.Edge e2 = new KruskalAlgorithm.Edge(1, 2, 5); + + assertEquals(0, e1.compareTo(e2)); + } + + @Test + @DisplayName("Algorithm chooses cheapest among parallel edges") + void testParallelEdges() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(3); + + graph.addEdge(0, 1, 10); + graph.addEdge(0, 1, 3); + graph.addEdge(1, 2, 4); + + List mst = new KruskalAlgorithm().computeMST(graph); + + int weight = mst.stream().mapToInt(KruskalAlgorithm.Edge::getWeight).sum(); + assertEquals(7, weight); + } + + // ------------------------------------------------------------- + // CYCLE & UNION-FIND BEHAVIOR + // ------------------------------------------------------------- + + @Test + @DisplayName("Graph containing cycles still produces correct MST") + void testCycleHeavyGraph() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(4); + + graph.addEdge(0, 1, 1); + graph.addEdge(1, 2, 2); + graph.addEdge(2, 3, 3); + + // Creating cycles + graph.addEdge(0, 2, 10); + graph.addEdge(1, 3, 10); + + List mst = new KruskalAlgorithm().computeMST(graph); + + assertEquals(3, mst.size()); + assertEquals(6, mst.stream().mapToInt(KruskalAlgorithm.Edge::getWeight).sum()); + } + + @Test + @DisplayName("Union-Find path compression works (indirect test via MST)") + void testPathCompression() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(3); + + graph.addEdge(0, 1, 1); + graph.addEdge(1, 2, 2); + + // Forces multiple find() calls + new KruskalAlgorithm().computeMST(graph); + + // Indirect validation: + // If path compression failed, algorithm would still work, + // but we can ensure no exception occurs (behavioral guarantee). + assertTrue(true); + } + + @Test + @DisplayName("Union-by-rank is stable (indirect coverage)") + void testUnionByRank() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(4); + + graph.addEdge(0, 1, 1); + graph.addEdge(2, 3, 1); + graph.addEdge(1, 2, 1); + + List mst = new KruskalAlgorithm().computeMST(graph); + + assertEquals(3, mst.size()); + } + + // ------------------------------------------------------------- + // EARLY EXIT CONDITION + // ------------------------------------------------------------- + + @Test + @DisplayName("Algorithm stops early when MST is complete") + void testEarlyExit() { + KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(100); + + // Only 99 edges needed, so extra edges should be ignored + for (int i = 0; i < 99; i++) { + graph.addEdge(i, i + 1, 1); + } + + // Add a bunch of useless heavy edges + for (int i = 0; i < 500; i++) { + graph.addEdge(0, 1, 9999); + } + + List mst = new KruskalAlgorithm().computeMST(graph); + + assertEquals(99, mst.size()); // ensures early break + } +}