Skip to content

Commit ded88c0

Browse files
feat(machinelearning): add K-Means clustering algorithm
1 parent f934d71 commit ded88c0

2 files changed

Lines changed: 250 additions & 0 deletions

File tree

src/main/java/com/thealgorithms/machinelearning/KMeans.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,26 @@
22

33
/**
44
* Implements the K-Means clustering algorithm using Lloyd's algorithm.
5+
*
6+
* <p>
7+
* K-Means partitions observations into k clusters by iteratively assigning each
8+
* point to its nearest centroid and recomputing centroid positions until
9+
* convergence or the maximum number of iterations is reached.
510
*/
611
public final class KMeans {
712

813
private KMeans() {
14+
915
// Utility class
1016
}
1117

18+
/**
19+
* Computes the squared Euclidean distance between two points.
20+
*
21+
* @param point1 first point
22+
* @param point2 second point
23+
* @return squared Euclidean distance
24+
*/
1225
private static double squaredDistance(double[] point1, double[] point2) {
1326
double sum = 0.0;
1427
for (int i = 0; i < point1.length; i++) {
@@ -18,6 +31,13 @@ private static double squaredDistance(double[] point1, double[] point2) {
1831
return sum;
1932
}
2033

34+
/**
35+
* Finds the nearest centroid for the given point.
36+
*
37+
* @param point point to classify
38+
* @param centroids current centroids
39+
* @return index of the nearest centroid
40+
*/
2141
private static int nearestCentroid(double[] point, double[][] centroids) {
2242
int nearest = 0;
2343
double minimumDistance = squaredDistance(point, centroids[0]);
@@ -41,6 +61,7 @@ private static int nearestCentroid(double[] point, double[][] centroids) {
4161
* @param maxIterations maximum number of iterations
4262
* @param tolerance convergence tolerance
4363
* @return cluster assignment for each point
64+
* @throws IllegalArgumentException if the input is invalid
4465
*/
4566
public static int[] cluster(
4667
double[][] points,
@@ -72,18 +93,31 @@ public static int[] cluster(
7293
throw new IllegalArgumentException("Tolerance cannot be negative.");
7394
}
7495

96+
if (points[0] == null) {
97+
throw new IllegalArgumentException("Points cannot contain null rows.");
98+
}
99+
75100
int dimensions = points[0].length;
101+
76102
if (dimensions == 0) {
77103
throw new IllegalArgumentException("Points must have at least one dimension.");
78104
}
79105

80106
for (double[] point : points) {
107+
if (point == null) {
108+
throw new IllegalArgumentException("Points cannot contain null rows.");
109+
}
110+
81111
if (point.length != dimensions) {
82112
throw new IllegalArgumentException("All points must have the same dimension.");
83113
}
84114
}
85115

86116
for (double[] centroid : initialCentroids) {
117+
if (centroid == null) {
118+
throw new IllegalArgumentException("Centroids cannot contain null rows.");
119+
}
120+
87121
if (centroid.length != dimensions) {
88122
throw new IllegalArgumentException("Centroid dimensions must match point dimensions.");
89123
}

src/test/java/com/thealgorithms/machinelearning/KMeansTest.java

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.thealgorithms.machinelearning;
22

33
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
45
import org.junit.jupiter.api.Test;
56

67
class KMeansTest {
@@ -27,4 +28,219 @@ void testSimpleClustering() {
2728
KMeans.cluster(points, centroids, 100, 0.0001)
2829
);
2930
}
31+
32+
@Test
33+
void testNullCentroids() {
34+
double[][] points = {
35+
{1.0, 1.0}
36+
};
37+
38+
assertThrows(
39+
IllegalArgumentException.class,
40+
() -> KMeans.cluster(points, null, 100, 0.0001)
41+
);
42+
43+
}
44+
45+
@Test
46+
void testEmptyDataset() {
47+
double[][] points = {};
48+
double[][] centroids = {
49+
{1.0, 1.0}
50+
};
51+
52+
assertThrows(
53+
IllegalArgumentException.class,
54+
() -> KMeans.cluster(points, centroids, 100, 0.0001)
55+
);
56+
}
57+
58+
@Test
59+
void testEmptyPoints() {
60+
double[][] points = {};
61+
double[][] centroids = {
62+
{1.0, 1.0}
63+
};
64+
65+
assertThrows(
66+
IllegalArgumentException.class,
67+
() -> KMeans.cluster(points, centroids, 100, 0.0001)
68+
);
69+
}
70+
71+
@Test
72+
void testNoCentroids() {
73+
double[][] points = {
74+
{1.0, 1.0}
75+
};
76+
77+
double[][] centroids = {};
78+
79+
assertThrows(
80+
IllegalArgumentException.class,
81+
() -> KMeans.cluster(points, centroids, 100, 0.0001)
82+
);
83+
}
84+
85+
@Test
86+
void testNonPositiveMaxIterations() {
87+
double[][] points = {
88+
{1.0, 1.0}
89+
};
90+
91+
double[][] centroids = {
92+
{1.0, 1.0}
93+
};
94+
95+
assertThrows(
96+
IllegalArgumentException.class,
97+
() -> KMeans.cluster(points, centroids, 0, 0.0001)
98+
);
99+
}
100+
101+
@Test
102+
void testNegativeTolerance() {
103+
double[][] points = {
104+
{1.0, 1.0}
105+
};
106+
double[][] centroids = {
107+
{1.0, 1.0}
108+
};
109+
110+
assertThrows(
111+
IllegalArgumentException.class,
112+
() -> KMeans.cluster(points, centroids, 100, -1.0)
113+
);
114+
}
115+
116+
@Test
117+
void testTooManyCentroids() {
118+
double[][] points = {
119+
{1.0, 1.0}
120+
};
121+
122+
double[][] centroids = {
123+
{1.0, 1.0},
124+
{2.0, 2.0}
125+
};
126+
127+
assertThrows(
128+
IllegalArgumentException.class,
129+
() -> KMeans.cluster(points, centroids, 100, 0.0001)
130+
);
131+
}
132+
133+
@Test
134+
void testDimensionMismatchInPoints() {
135+
double[][] points = {
136+
{1.0, 1.0},
137+
{2.0}
138+
};
139+
140+
double[][] centroids = {
141+
{1.0, 1.0}
142+
};
143+
144+
assertThrows(
145+
IllegalArgumentException.class,
146+
() -> KMeans.cluster(points, centroids, 100, 0.0001)
147+
);
148+
}
149+
150+
@Test
151+
void testDimensionMismatchInCentroids() {
152+
double[][] points = {
153+
{1.0, 1.0}
154+
};
155+
156+
double[][] centroids = {
157+
{1.0}
158+
};
159+
160+
assertThrows(
161+
IllegalArgumentException.class,
162+
() -> KMeans.cluster(points, centroids, 100, 0.0001)
163+
);
164+
}
165+
166+
@Test
167+
void testZeroDimensionPoints() {
168+
double[][] points = {
169+
{}
170+
};
171+
172+
double[][] centroids = {
173+
{}
174+
};
175+
176+
assertThrows(
177+
IllegalArgumentException.class,
178+
() -> KMeans.cluster(points, centroids, 100, 0.0001)
179+
);
180+
}
181+
182+
@Test
183+
void testSingleCluster() {
184+
double[][] points = {
185+
{1.0, 1.0},
186+
{2.0, 2.0},
187+
{3.0, 3.0}
188+
};
189+
190+
double[][] centroids = {
191+
{2.0, 2.0}
192+
};
193+
194+
int[] expected = {
195+
0, 0, 0
196+
};
197+
198+
assertArrayEquals(
199+
expected,
200+
KMeans.cluster(points, centroids, 100, 0.0001)
201+
);
202+
}
203+
204+
@Test
205+
void testEmptyClusterHandling() {
206+
double[][] points = {
207+
{0.0, 0.0},
208+
{0.1, 0.1},
209+
{10.0, 10.0}
210+
};
211+
212+
double[][] centroids = {
213+
{0.0, 0.0},
214+
{100.0, 100.0}
215+
};
216+
217+
int[] result = KMeans.cluster(points, centroids, 100, 0.0001);
218+
219+
assertArrayEquals(
220+
new int[]{0, 0, 0},
221+
result
222+
);
223+
}
224+
225+
@Test
226+
void testImmediateConvergence() {
227+
double[][] points = {
228+
{1.0, 1.0},
229+
{9.0, 9.0}
230+
};
231+
232+
double[][] centroids = {
233+
{1.0, 1.0},
234+
{9.0, 9.0}
235+
};
236+
237+
int[] expected = {
238+
0, 1
239+
};
240+
241+
assertArrayEquals(
242+
expected,
243+
KMeans.cluster(points, centroids, 100, 0.000001)
244+
);
245+
}
30246
}

0 commit comments

Comments
 (0)