Skip to content

Commit f934d71

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

2 files changed

Lines changed: 188 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package com.thealgorithms.machinelearning;
2+
3+
/**
4+
* Implements the K-Means clustering algorithm using Lloyd's algorithm.
5+
*/
6+
public final class KMeans {
7+
8+
private KMeans() {
9+
// Utility class
10+
}
11+
12+
private static double squaredDistance(double[] point1, double[] point2) {
13+
double sum = 0.0;
14+
for (int i = 0; i < point1.length; i++) {
15+
double diff = point1[i] - point2[i];
16+
sum += diff * diff;
17+
}
18+
return sum;
19+
}
20+
21+
private static int nearestCentroid(double[] point, double[][] centroids) {
22+
int nearest = 0;
23+
double minimumDistance = squaredDistance(point, centroids[0]);
24+
25+
for (int i = 1; i < centroids.length; i++) {
26+
double distance = squaredDistance(point, centroids[i]);
27+
if (distance < minimumDistance) {
28+
minimumDistance = distance;
29+
nearest = i;
30+
}
31+
}
32+
33+
return nearest;
34+
}
35+
36+
/**
37+
* Clusters the given points using K-Means.
38+
*
39+
* @param points input data points
40+
* @param initialCentroids initial centroid positions
41+
* @param maxIterations maximum number of iterations
42+
* @param tolerance convergence tolerance
43+
* @return cluster assignment for each point
44+
*/
45+
public static int[] cluster(
46+
double[][] points,
47+
double[][] initialCentroids,
48+
int maxIterations,
49+
double tolerance) {
50+
51+
if (points == null || initialCentroids == null) {
52+
throw new IllegalArgumentException("Input arrays cannot be null.");
53+
}
54+
55+
if (points.length == 0) {
56+
throw new IllegalArgumentException("Dataset cannot be empty.");
57+
}
58+
59+
if (initialCentroids.length == 0) {
60+
throw new IllegalArgumentException("At least one centroid is required.");
61+
}
62+
63+
if (initialCentroids.length > points.length) {
64+
throw new IllegalArgumentException("Number of centroids cannot exceed number of points.");
65+
}
66+
67+
if (maxIterations <= 0) {
68+
throw new IllegalArgumentException("Maximum iterations must be positive.");
69+
}
70+
71+
if (tolerance < 0) {
72+
throw new IllegalArgumentException("Tolerance cannot be negative.");
73+
}
74+
75+
int dimensions = points[0].length;
76+
if (dimensions == 0) {
77+
throw new IllegalArgumentException("Points must have at least one dimension.");
78+
}
79+
80+
for (double[] point : points) {
81+
if (point.length != dimensions) {
82+
throw new IllegalArgumentException("All points must have the same dimension.");
83+
}
84+
}
85+
86+
for (double[] centroid : initialCentroids) {
87+
if (centroid.length != dimensions) {
88+
throw new IllegalArgumentException("Centroid dimensions must match point dimensions.");
89+
}
90+
}
91+
92+
int k = initialCentroids.length;
93+
int[] assignments = new int[points.length];
94+
double[][] centroids = new double[k][dimensions];
95+
96+
for (int i = 0; i < k; i++) {
97+
System.arraycopy(initialCentroids[i], 0, centroids[i], 0, dimensions);
98+
}
99+
100+
boolean changed = true;
101+
int iterations = 0;
102+
103+
while (changed && iterations < maxIterations) {
104+
changed = false;
105+
iterations++;
106+
107+
// Assign points to nearest centroid
108+
for (int i = 0; i < points.length; i++) {
109+
int nearest = nearestCentroid(points[i], centroids);
110+
if (assignments[i] != nearest) {
111+
assignments[i] = nearest;
112+
changed = true;
113+
}
114+
}
115+
116+
// Compute new centroids
117+
double[][] newCentroids = new double[k][dimensions];
118+
int[] clusterSizes = new int[k];
119+
120+
for (int i = 0; i < points.length; i++) {
121+
int cluster = assignments[i];
122+
clusterSizes[cluster]++;
123+
124+
for (int j = 0; j < dimensions; j++) {
125+
newCentroids[cluster][j] += points[i][j];
126+
}
127+
}
128+
129+
for (int i = 0; i < k; i++) {
130+
if (clusterSizes[i] == 0) {
131+
System.arraycopy(centroids[i], 0, newCentroids[i], 0, dimensions);
132+
continue;
133+
}
134+
135+
for (int j = 0; j < dimensions; j++) {
136+
newCentroids[i][j] /= clusterSizes[i];
137+
}
138+
}
139+
140+
double maxShift = 0.0;
141+
142+
for (int i = 0; i < k; i++) {
143+
double shift = squaredDistance(centroids[i], newCentroids[i]);
144+
if (shift > maxShift) {
145+
maxShift = shift;
146+
}
147+
}
148+
149+
centroids = newCentroids;
150+
151+
if (maxShift <= tolerance * tolerance) {
152+
break;
153+
}
154+
}
155+
156+
return assignments;
157+
}
158+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.thealgorithms.machinelearning;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import org.junit.jupiter.api.Test;
5+
6+
class KMeansTest {
7+
8+
@Test
9+
void testSimpleClustering() {
10+
11+
double[][] points = {
12+
{1.0, 1.0},
13+
{1.2, 1.1},
14+
{8.0, 8.0},
15+
{8.2, 8.1}
16+
};
17+
18+
double[][] centroids = {
19+
{1.0, 1.0},
20+
{8.0, 8.0}
21+
};
22+
23+
int[] expected = {0, 0, 1, 1};
24+
25+
assertArrayEquals(
26+
expected,
27+
KMeans.cluster(points, centroids, 100, 0.0001)
28+
);
29+
}
30+
}

0 commit comments

Comments
 (0)