Skip to content

Commit 08ce5d2

Browse files
feature: adds Kruskal's algorithm to greedy section
1 parent a008cc2 commit 08ce5d2

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.thealgorithms.greedyalgorithms;
2+
3+
/**
4+
* An encapsulated, self-contained implementation of Kruskal's algorithm
5+
* for computing the Minimum Spanning Tree (MST) of a weighted, undirected graph.
6+
* <p>
7+
* To avoid namespace conflicts and maintain isolation within larger projects,
8+
* all collaborators (Edge, Graph, DisjointSet) are implemented as private
9+
* static nested classes. This ensures no type leakage outside this file while
10+
* preserving clean internal architecture.
11+
* </p>
12+
*
13+
* <h2>Usage</h2>
14+
* <pre>
15+
* KruskalAlgorithm algo = new KruskalAlgorithm();
16+
* KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(4);
17+
* graph.addEdge(0,1,10);
18+
* graph.addEdge(1,2,5);
19+
* List&lt;KruskalAlgorithm.Edge&gt; mst = algo.computeMST(graph);
20+
* </pre>
21+
*
22+
* <h2>Design Notes</h2>
23+
* <ul>
24+
* <li>Implements a fully isolated module without risk of polluting global scope.</li>
25+
* <li>Inner classes preserve encapsulation but keep responsibilities separate.</li>
26+
* <li>Algorithm complexity: O(e log e), dominated by edge sorting.</li>
27+
* </ul>
28+
*/
29+
public class KruskalAlgorithm {
30+
31+
/**
32+
* Computes the Minimum Spanning Tree (or Minimum Spanning Forest if the graph
33+
* is disconnected) using Kruskal’s greedy strategy.
34+
*
35+
* @param graph the graph instance to process
36+
* @return a list of edges forming the MST
37+
*/
38+
public java.util.List<Edge> computeMST(Graph graph) {
39+
java.util.List<Edge> mst = new java.util.ArrayList<>();
40+
java.util.List<Edge> edges = new java.util.ArrayList<>(graph.edges);
41+
42+
// Sort edges by ascending weight
43+
java.util.Collections.sort(edges);
44+
45+
DisjointSet ds = new DisjointSet(graph.numberOfVertices);
46+
47+
for (Edge e : edges) {
48+
int rootA = ds.find(e.source);
49+
int rootB = ds.find(e.target);
50+
51+
if (rootA != rootB) {
52+
mst.add(e);
53+
ds.union(rootA, rootB);
54+
55+
if (mst.size() == graph.numberOfVertices - 1) break;
56+
}
57+
}
58+
59+
return mst;
60+
}
61+
62+
/**
63+
* Represents an immutable weighted edge between two vertices.
64+
*/
65+
public static final class Edge implements Comparable<Edge> {
66+
private final int source;
67+
private final int target;
68+
private final int weight;
69+
70+
public Edge(int source, int target, int weight) {
71+
if (weight < 0) {
72+
throw new IllegalArgumentException("Weight cannot be negative.");
73+
}
74+
this.source = source;
75+
this.target = target;
76+
this.weight = weight;
77+
}
78+
79+
public int getSource() { return source; }
80+
public int getTarget() { return target; }
81+
public int getWeight() { return weight; }
82+
83+
@Override
84+
public int compareTo(Edge o) {
85+
return Integer.compare(this.weight, o.weight);
86+
}
87+
}
88+
89+
/**
90+
* Lightweight graph representation consisting solely of vertices and edges.
91+
* All algorithmic behavior is delegated to higher-level components.
92+
*/
93+
public static final class Graph {
94+
private final int numberOfVertices;
95+
private final java.util.List<Edge> edges = new java.util.ArrayList<>();
96+
97+
public Graph(int numberOfVertices) {
98+
if (numberOfVertices <= 0) {
99+
throw new IllegalArgumentException("Graph must have at least one vertex.");
100+
}
101+
this.numberOfVertices = numberOfVertices;
102+
}
103+
104+
/**
105+
* Adds an undirected edge to the graph.
106+
*/
107+
public void addEdge(int source, int target, int weight) {
108+
if (source < 0 || source >= numberOfVertices ||
109+
target < 0 || target >= numberOfVertices) {
110+
throw new IndexOutOfBoundsException("Vertex index out of range.");
111+
}
112+
113+
edges.add(new Edge(source, target, weight));
114+
}
115+
}
116+
117+
/**
118+
* Disjoint Set Union data structure supporting path compression
119+
* and union-by-rank — essential for cycle detection in Kruskal's algorithm.
120+
*/
121+
private static final class DisjointSet {
122+
private final int[] parent;
123+
private final int[] rank;
124+
125+
public DisjointSet(int size) {
126+
parent = new int[size];
127+
rank = new int[size];
128+
for (int i = 0; i < size; i++) parent[i] = i;
129+
}
130+
131+
public int find(int x) {
132+
if (parent[x] != x) {
133+
parent[x] = find(parent[x]); // Path compression
134+
}
135+
return parent[x];
136+
}
137+
138+
public void union(int a, int b) {
139+
int ra = find(a);
140+
int rb = find(b);
141+
if (ra == rb) return;
142+
143+
if (rank[ra] < rank[rb]) {
144+
parent[ra] = rb;
145+
} else if (rank[ra] > rank[rb]) {
146+
parent[rb] = ra;
147+
} else {
148+
parent[rb] = ra;
149+
rank[ra]++;
150+
}
151+
}
152+
}
153+
}
154+
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package com.thealgorithms.greedyalgorithms;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.DisplayName;
5+
6+
import java.util.List;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
/**
11+
* Unit tests for the KruskalAlgorithm implementation.
12+
*/
13+
public class KruskalAlgorithmTest {
14+
15+
@Test
16+
@DisplayName("Computes MST for a standard connected graph")
17+
void testMSTCorrectness() {
18+
KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(4);
19+
20+
graph.addEdge(0, 1, 10);
21+
graph.addEdge(0, 2, 6);
22+
graph.addEdge(0, 3, 5);
23+
graph.addEdge(2, 3, 4);
24+
25+
KruskalAlgorithm algo = new KruskalAlgorithm();
26+
List<KruskalAlgorithm.Edge> mst = algo.computeMST(graph);
27+
28+
assertEquals(3, mst.size());
29+
int totalWeight = mst.stream().mapToInt(KruskalAlgorithm.Edge::getWeight).sum();
30+
31+
assertEquals(19, totalWeight); // Correct MST weight
32+
}
33+
34+
@Test
35+
@DisplayName("Graph with a single vertex produces an empty MST")
36+
void testSingleVertexGraph() {
37+
KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(1);
38+
39+
KruskalAlgorithm algo = new KruskalAlgorithm();
40+
List<KruskalAlgorithm.Edge> mst = algo.computeMST(graph);
41+
42+
assertTrue(mst.isEmpty());
43+
}
44+
45+
@Test
46+
@DisplayName("Disconnected graph yields a minimum spanning forest")
47+
void testDisconnectedGraph() {
48+
KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(4);
49+
50+
graph.addEdge(0, 1, 3);
51+
graph.addEdge(2, 3, 1);
52+
53+
KruskalAlgorithm algo = new KruskalAlgorithm();
54+
List<KruskalAlgorithm.Edge> mst = algo.computeMST(graph);
55+
56+
assertEquals(2, mst.size());
57+
}
58+
59+
@Test
60+
@DisplayName("Adding an edge with negative weight should throw an exception")
61+
void testNegativeWeightThrowsException() {
62+
KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(3);
63+
64+
assertThrows(IllegalArgumentException.class, () -> graph.addEdge(0, 1, -5));
65+
}
66+
67+
@Test
68+
@DisplayName("Parallel edges: algorithm should choose the cheaper one")
69+
void testParallelEdges() {
70+
KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(3);
71+
72+
graph.addEdge(0, 1, 10);
73+
graph.addEdge(0, 1, 3); // cheaper parallel edge
74+
graph.addEdge(1, 2, 4);
75+
76+
KruskalAlgorithm algo = new KruskalAlgorithm();
77+
List<KruskalAlgorithm.Edge> mst = algo.computeMST(graph);
78+
79+
int totalWeight = mst.stream().mapToInt(KruskalAlgorithm.Edge::getWeight).sum();
80+
81+
assertEquals(7, totalWeight);
82+
}
83+
84+
@Test
85+
@DisplayName("Graph with no edges must produce an empty MST")
86+
void testEmptyGraph() {
87+
KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(5);
88+
89+
KruskalAlgorithm algo = new KruskalAlgorithm();
90+
List<KruskalAlgorithm.Edge> mst = algo.computeMST(graph);
91+
92+
assertTrue(mst.isEmpty());
93+
}
94+
95+
// ---------------------------
96+
// ADDITIONAL ROBUSTNESS TESTS
97+
// ---------------------------
98+
99+
@Test
100+
@DisplayName("Edge with invalid vertex index should throw exception")
101+
void testOutOfBoundsVertexIndex() {
102+
KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(3);
103+
104+
assertThrows(IndexOutOfBoundsException.class, () -> graph.addEdge(0, 5, 10));
105+
assertThrows(IndexOutOfBoundsException.class, () -> graph.addEdge(-1, 1, 2));
106+
}
107+
108+
@Test
109+
@DisplayName("Zero-weight edges are allowed and handled correctly")
110+
void testZeroWeightEdges() {
111+
KruskalAlgorithm.Graph graph = new KruskalAlgorithm.Graph(3);
112+
113+
graph.addEdge(0, 1, 0);
114+
graph.addEdge(1, 2, 1);
115+
116+
KruskalAlgorithm algo = new KruskalAlgorithm();
117+
118+
assertDoesNotThrow(() -> algo.computeMST(graph));
119+
List<KruskalAlgorithm.Edge> mst = algo.computeMST(graph);
120+
121+
int totalWeight = mst.stream().mapToInt(KruskalAlgorithm.Edge::getWeight).sum();
122+
assertEquals(1, totalWeight);
123+
}
124+
}

0 commit comments

Comments
 (0)