Skip to content

Commit fde018a

Browse files
committed
Add K-Means clustering algorithm
1 parent 7c934ad commit fde018a

2 files changed

Lines changed: 518 additions & 0 deletions

File tree

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
package com.thealgorithms.machinelearning;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
import java.util.Objects;
7+
import java.util.Random;
8+
9+
public final class Clustering {
10+
11+
private Clustering() {
12+
13+
}
14+
/**
15+
* Runs K-Means using explicit, caller-supplied initial centroids. Deterministic — the
16+
* recommended entry point for reproducible results and tests.
17+
*
18+
* @param points the dataset to cluster; non-empty, consistent dimensionality
19+
* @param initialCentroids exactly {@code k} initial centroids, matching {@code points}'
20+
* dimensionality
21+
* @param maxIterations maximum number of iterations; must be positive
22+
* @param tolerance convergence tolerance on center movement; must be non-negative
23+
* @return the clustering result
24+
*/
25+
26+
public static ClusteringResult kMeans(double[][] points, double[][] initialCentroids, int maxIterations, double tolerance) {
27+
validateParameters(maxIterations, tolerance);
28+
double[][] centroids = validateAndCopyCentroids(points, initialCentroids);
29+
return run(points, centroids, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean);
30+
}
31+
public static ClusteringResult kMeans(double[][] points, int k, long seed, int maxIterations, double tolerance) {
32+
validateParameters(maxIterations, tolerance);
33+
double[][] centroids = randomInitialCentroids(points, k, seed);
34+
return run(points, centroids, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean);
35+
}
36+
37+
/**
38+
* Runs K-Medians using explicit, caller-supplied initial centers. Deterministic — the
39+
* recommended entry point for reproducible results and tests.
40+
*
41+
* @param points the dataset to cluster; non-empty, consistent dimensionality
42+
* @param initialCenters exactly {@code k} initial centers, matching {@code points}'
43+
* dimensionality
44+
* @param maxIterations maximum number of iterations; must be positive
45+
* @param tolerance convergence tolerance on center movement; must be non-negative
46+
* @return the clustering result
47+
*/
48+
49+
public static ClusteringResult kMedians(double[][] points, double[][] initialCenters, int maxIterations, double tolerance) {
50+
validateParameters(maxIterations, tolerance);
51+
double[][] centers = validateAndCopyCentroids(points, initialCenters);
52+
return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median);
53+
}
54+
55+
/**
56+
* Runs K-Medians, sampling {@code k} distinct points from the dataset (via a seeded
57+
* {@link Random}) as initial centers. Reproducible across runs given the same seed.
58+
*
59+
* @param points the dataset to cluster; non-empty, at least {@code k} points
60+
* @param k the number of clusters; must be positive and ≤ number of points
61+
* @param seed seed used to pick initial centers
62+
* @param maxIterations maximum number of iterations; must be positive
63+
* @param tolerance convergence tolerance on center movement; must be non-negative
64+
* @return the clustering result
65+
*/
66+
67+
public static ClusteringResult kMedians(double[][] points, int k, long seed, int maxIterations, double tolerance) {
68+
validateParameters(maxIterations, tolerance);
69+
double[][] centers = randomInitialCentroids(points, k, seed);
70+
return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median);
71+
}
72+
73+
74+
@FunctionalInterface
75+
private interface DistanceFunction {
76+
double distance(double[] a, double[] b);
77+
}
78+
79+
@FunctionalInterface
80+
private interface CenterFunction {
81+
double[] center(List<double[]> clusterPoints, int dimension);
82+
}
83+
84+
private static ClusteringResult run(double[][] points, double[][] initialCenters, int maxIterations, double tolerance, DistanceFunction assignmentDistance, CenterFunction centerFunction) {
85+
int n = points.length;
86+
int k = initialCenters.length;
87+
int dimension = points[0].length;
88+
double[][] centers = initialCenters;
89+
int[] labels = new int[n];
90+
Arrays.fill(labels, -1);
91+
92+
int iteration = 0;
93+
boolean converged = false;
94+
95+
while (iteration < maxIterations && !converged) {
96+
boolean anyAssignmentChanged = assign(points, centers, labels, assignmentDistance);
97+
double[][] newCenters = updateCenters(points, labels, centers, k, dimension, centerFunction);
98+
double maxShift = maxCenterShift(centers, newCenters);
99+
centers = newCenters;
100+
iteration++;
101+
converged = !anyAssignmentChanged || maxShift < tolerance;
102+
}
103+
104+
return new ClusteringResult(centers, labels, iteration, converged);
105+
}
106+
107+
private static boolean assign(double[][] points, double[][] centers, int[] labels, DistanceFunction distanceFunction) {
108+
boolean changed = false;
109+
for (int i = 0; i < points.length; i++) {
110+
int best = 0;
111+
double bestDist = distanceFunction.distance(points[i], centers[0]);
112+
for (int c = 1; c < centers.length; c++) {
113+
double dist = distanceFunction.distance(points[i], centers[c]);
114+
if (dist < bestDist) {
115+
bestDist = dist;
116+
best = c;
117+
}
118+
}
119+
if (labels[i] != best) {
120+
labels[i] = best;
121+
changed = true;
122+
}
123+
}
124+
return changed;
125+
}
126+
127+
private static double[][] updateCenters(double[][] points, int[] labels, double[][] oldCenters, int k, int dimension, CenterFunction centerFunction) {
128+
List<List<double[]>> groups = new ArrayList<>();
129+
for (int c = 0; c < k; c++) {
130+
groups.add(new ArrayList<>());
131+
}
132+
for (int i = 0; i < points.length; i++) {
133+
groups.get(labels[i]).add(points[i]);
134+
}
135+
double[][] newCenters = new double[k][];
136+
for (int c = 0; c < k; c++) {
137+
if (groups.get(c).isEmpty()) {
138+
// Keep the previous center if the cluster lost all its points.
139+
newCenters[c] = Arrays.copyOf(oldCenters[c], dimension);
140+
} else {
141+
newCenters[c] = centerFunction.center(groups.get(c), dimension);
142+
}
143+
}
144+
return newCenters;
145+
}
146+
147+
private static double maxCenterShift(double[][] oldCenters, double[][] newCenters) {
148+
double max = 0.0;
149+
for (int c = 0; c < oldCenters.length; c++) {
150+
max = Math.max(max, euclideanDistance(oldCenters[c], newCenters[c]));
151+
}
152+
return max;
153+
}
154+
155+
private static double squaredEuclideanDistance(double[] a, double[] b) {
156+
double sum = 0.0;
157+
for (int d = 0; d < a.length; d++) {
158+
double diff = a[d] - b[d];
159+
sum += diff * diff;
160+
}
161+
return sum;
162+
}
163+
164+
private static double euclideanDistance(double[] a, double[] b) {
165+
return Math.sqrt(squaredEuclideanDistance(a, b));
166+
}
167+
168+
private static double manhattanDistance(double[] a, double[] b) {
169+
double sum = 0.0;
170+
for (int d = 0; d < a.length; d++) {
171+
sum += Math.abs(a[d] - b[d]);
172+
}
173+
return sum;
174+
}
175+
176+
private static double[] mean(List<double[]> clusterPoints, int dimension) {
177+
double[] result = new double[dimension];
178+
for (double[] p : clusterPoints) {
179+
for (int d = 0; d < dimension; d++) {
180+
result[d] += p[d];
181+
}
182+
}
183+
for (int d = 0; d < dimension; d++) {
184+
result[d] /= clusterPoints.size();
185+
}
186+
return result;
187+
}
188+
189+
private static double[] median(List<double[]> clusterPoints, int dimension) {
190+
int n = clusterPoints.size();
191+
double[] result = new double[dimension];
192+
double[] values = new double[n];
193+
for (int d = 0; d < dimension; d++) {
194+
for (int i = 0; i < n; i++) {
195+
values[i] = clusterPoints.get(i)[d];
196+
}
197+
Arrays.sort(values);
198+
if (n % 2 == 1) {
199+
result[d] = values[n / 2];
200+
} else {
201+
result[d] = (values[n / 2 - 1] + values[n / 2]) / 2.0;
202+
}
203+
}
204+
return result;
205+
}
206+
207+
private static void validateParameters(int maxIterations, double tolerance) {
208+
if (maxIterations <= 0) {
209+
throw new IllegalArgumentException("maxIterations must be positive, got " + maxIterations);
210+
}
211+
if (tolerance < 0) {
212+
throw new IllegalArgumentException("tolerance must be non-negative, got " + tolerance);
213+
}
214+
}
215+
216+
private static void validatePoints(double[][] points, int k) {
217+
if (points == null || points.length == 0) {
218+
throw new IllegalArgumentException("Dataset must not be empty");
219+
}
220+
if (k <= 0) {
221+
throw new IllegalArgumentException("k must be positive, got " + k);
222+
}
223+
if (k > points.length) {
224+
throw new IllegalArgumentException("k (" + k + ") cannot exceed the number of points (" + points.length + ")");
225+
}
226+
int dimension = points[0].length;
227+
if (dimension == 0) {
228+
throw new IllegalArgumentException("Points must have at least one dimension");
229+
}
230+
for (int i = 0; i < points.length; i++) {
231+
if (points[i] == null || points[i].length != dimension) {
232+
throw new IllegalArgumentException("All points must share the same dimensionality; point " + i + " does not match");
233+
}
234+
}
235+
}
236+
237+
private static double[][] validateAndCopyCentroids(double[][] points, double[][] initialCenters) {
238+
Objects.requireNonNull(initialCenters, "initial centers must not be null");
239+
validatePoints(points, initialCenters.length);
240+
int dimension = points[0].length;
241+
double[][] centers = new double[initialCenters.length][];
242+
for (int i = 0; i < initialCenters.length; i++) {
243+
if (initialCenters[i] == null || initialCenters[i].length != dimension) {
244+
throw new IllegalArgumentException("Initial center " + i + " has inconsistent dimensionality");
245+
}
246+
centers[i] = Arrays.copyOf(initialCenters[i], dimension);
247+
}
248+
return centers;
249+
}
250+
251+
private static double[][] randomInitialCentroids(double[][] points, int k, long seed) {
252+
validatePoints(points, k);
253+
int[] indices = new int[points.length];
254+
for (int i = 0; i < indices.length; i++) {
255+
indices[i] = i;
256+
}
257+
Random random = new Random(seed);
258+
for (int i = indices.length - 1; i > 0; i--) {
259+
int j = random.nextInt(i + 1);
260+
int tmp = indices[i];
261+
indices[i] = indices[j];
262+
indices[j] = tmp;
263+
}
264+
double[][] centers = new double[k][];
265+
for (int i = 0; i < k; i++) {
266+
centers[i] = Arrays.copyOf(points[indices[i]], points[indices[i]].length);
267+
}
268+
return centers;
269+
}
270+
271+
public static final class ClusteringResult {
272+
private final double[][] centers;
273+
private final int[] labels;
274+
private final int iterations;
275+
private final boolean converged;
276+
277+
ClusteringResult(double[][] centers, int[] labels, int iterations, boolean converged) {
278+
this.centers = centers;
279+
this.labels = labels;
280+
this.iterations = iterations;
281+
this.converged = converged;
282+
}
283+
284+
public double[][] getCenters() {
285+
double[][] copy = new double[centers.length][];
286+
for (int i = 0; i < centers.length; i++) {
287+
copy[i] = Arrays.copyOf(centers[i], centers[i].length);
288+
}
289+
return copy;
290+
}
291+
292+
public int[] getLabels() {
293+
return Arrays.copyOf(labels, labels.length);
294+
}
295+
296+
public int getIterations() {
297+
return iterations;
298+
}
299+
300+
public boolean hasConverged() {
301+
return converged;
302+
}
303+
}
304+
}

0 commit comments

Comments
 (0)