From fde018af38de5c88bbb45cc6dcabfe6d3f874890 Mon Sep 17 00:00:00 2001 From: Ashish2343 Date: Mon, 3 Aug 2026 17:46:20 +0530 Subject: [PATCH 1/7] Add K-Means clustering algorithm --- .../machinelearning/Clustering.java | 304 ++++++++++++++++++ .../machinelearning/ClusteringTest.java | 214 ++++++++++++ 2 files changed, 518 insertions(+) create mode 100644 src/main/java/com/thealgorithms/machinelearning/Clustering.java create mode 100644 src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java diff --git a/src/main/java/com/thealgorithms/machinelearning/Clustering.java b/src/main/java/com/thealgorithms/machinelearning/Clustering.java new file mode 100644 index 000000000000..6d9eacda775e --- /dev/null +++ b/src/main/java/com/thealgorithms/machinelearning/Clustering.java @@ -0,0 +1,304 @@ +package com.thealgorithms.machinelearning; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Random; + +public final class Clustering { + + private Clustering() { + + } + /** + * Runs K-Means using explicit, caller-supplied initial centroids. Deterministic — the + * recommended entry point for reproducible results and tests. + * + * @param points the dataset to cluster; non-empty, consistent dimensionality + * @param initialCentroids exactly {@code k} initial centroids, matching {@code points}' + * dimensionality + * @param maxIterations maximum number of iterations; must be positive + * @param tolerance convergence tolerance on center movement; must be non-negative + * @return the clustering result + */ + + public static ClusteringResult kMeans(double[][] points, double[][] initialCentroids, int maxIterations, double tolerance) { + validateParameters(maxIterations, tolerance); + double[][] centroids = validateAndCopyCentroids(points, initialCentroids); + return run(points, centroids, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean); + } + public static ClusteringResult kMeans(double[][] points, int k, long seed, int maxIterations, double tolerance) { + validateParameters(maxIterations, tolerance); + double[][] centroids = randomInitialCentroids(points, k, seed); + return run(points, centroids, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean); + } + + /** + * Runs K-Medians using explicit, caller-supplied initial centers. Deterministic — the + * recommended entry point for reproducible results and tests. + * + * @param points the dataset to cluster; non-empty, consistent dimensionality + * @param initialCenters exactly {@code k} initial centers, matching {@code points}' + * dimensionality + * @param maxIterations maximum number of iterations; must be positive + * @param tolerance convergence tolerance on center movement; must be non-negative + * @return the clustering result + */ + + public static ClusteringResult kMedians(double[][] points, double[][] initialCenters, int maxIterations, double tolerance) { + validateParameters(maxIterations, tolerance); + double[][] centers = validateAndCopyCentroids(points, initialCenters); + return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median); + } + + /** + * Runs K-Medians, sampling {@code k} distinct points from the dataset (via a seeded + * {@link Random}) as initial centers. Reproducible across runs given the same seed. + * + * @param points the dataset to cluster; non-empty, at least {@code k} points + * @param k the number of clusters; must be positive and ≤ number of points + * @param seed seed used to pick initial centers + * @param maxIterations maximum number of iterations; must be positive + * @param tolerance convergence tolerance on center movement; must be non-negative + * @return the clustering result + */ + + public static ClusteringResult kMedians(double[][] points, int k, long seed, int maxIterations, double tolerance) { + validateParameters(maxIterations, tolerance); + double[][] centers = randomInitialCentroids(points, k, seed); + return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median); + } + + + @FunctionalInterface + private interface DistanceFunction { + double distance(double[] a, double[] b); + } + + @FunctionalInterface + private interface CenterFunction { + double[] center(List clusterPoints, int dimension); + } + + private static ClusteringResult run(double[][] points, double[][] initialCenters, int maxIterations, double tolerance, DistanceFunction assignmentDistance, CenterFunction centerFunction) { + int n = points.length; + int k = initialCenters.length; + int dimension = points[0].length; + double[][] centers = initialCenters; + int[] labels = new int[n]; + Arrays.fill(labels, -1); + + int iteration = 0; + boolean converged = false; + + while (iteration < maxIterations && !converged) { + boolean anyAssignmentChanged = assign(points, centers, labels, assignmentDistance); + double[][] newCenters = updateCenters(points, labels, centers, k, dimension, centerFunction); + double maxShift = maxCenterShift(centers, newCenters); + centers = newCenters; + iteration++; + converged = !anyAssignmentChanged || maxShift < tolerance; + } + + return new ClusteringResult(centers, labels, iteration, converged); + } + + private static boolean assign(double[][] points, double[][] centers, int[] labels, DistanceFunction distanceFunction) { + boolean changed = false; + for (int i = 0; i < points.length; i++) { + int best = 0; + double bestDist = distanceFunction.distance(points[i], centers[0]); + for (int c = 1; c < centers.length; c++) { + double dist = distanceFunction.distance(points[i], centers[c]); + if (dist < bestDist) { + bestDist = dist; + best = c; + } + } + if (labels[i] != best) { + labels[i] = best; + changed = true; + } + } + return changed; + } + + private static double[][] updateCenters(double[][] points, int[] labels, double[][] oldCenters, int k, int dimension, CenterFunction centerFunction) { + List> groups = new ArrayList<>(); + for (int c = 0; c < k; c++) { + groups.add(new ArrayList<>()); + } + for (int i = 0; i < points.length; i++) { + groups.get(labels[i]).add(points[i]); + } + double[][] newCenters = new double[k][]; + for (int c = 0; c < k; c++) { + if (groups.get(c).isEmpty()) { + // Keep the previous center if the cluster lost all its points. + newCenters[c] = Arrays.copyOf(oldCenters[c], dimension); + } else { + newCenters[c] = centerFunction.center(groups.get(c), dimension); + } + } + return newCenters; + } + + private static double maxCenterShift(double[][] oldCenters, double[][] newCenters) { + double max = 0.0; + for (int c = 0; c < oldCenters.length; c++) { + max = Math.max(max, euclideanDistance(oldCenters[c], newCenters[c])); + } + return max; + } + + private static double squaredEuclideanDistance(double[] a, double[] b) { + double sum = 0.0; + for (int d = 0; d < a.length; d++) { + double diff = a[d] - b[d]; + sum += diff * diff; + } + return sum; + } + + private static double euclideanDistance(double[] a, double[] b) { + return Math.sqrt(squaredEuclideanDistance(a, b)); + } + + private static double manhattanDistance(double[] a, double[] b) { + double sum = 0.0; + for (int d = 0; d < a.length; d++) { + sum += Math.abs(a[d] - b[d]); + } + return sum; + } + + private static double[] mean(List clusterPoints, int dimension) { + double[] result = new double[dimension]; + for (double[] p : clusterPoints) { + for (int d = 0; d < dimension; d++) { + result[d] += p[d]; + } + } + for (int d = 0; d < dimension; d++) { + result[d] /= clusterPoints.size(); + } + return result; + } + + private static double[] median(List clusterPoints, int dimension) { + int n = clusterPoints.size(); + double[] result = new double[dimension]; + double[] values = new double[n]; + for (int d = 0; d < dimension; d++) { + for (int i = 0; i < n; i++) { + values[i] = clusterPoints.get(i)[d]; + } + Arrays.sort(values); + if (n % 2 == 1) { + result[d] = values[n / 2]; + } else { + result[d] = (values[n / 2 - 1] + values[n / 2]) / 2.0; + } + } + return result; + } + + private static void validateParameters(int maxIterations, double tolerance) { + if (maxIterations <= 0) { + throw new IllegalArgumentException("maxIterations must be positive, got " + maxIterations); + } + if (tolerance < 0) { + throw new IllegalArgumentException("tolerance must be non-negative, got " + tolerance); + } + } + + private static void validatePoints(double[][] points, int k) { + if (points == null || points.length == 0) { + throw new IllegalArgumentException("Dataset must not be empty"); + } + if (k <= 0) { + throw new IllegalArgumentException("k must be positive, got " + k); + } + if (k > points.length) { + throw new IllegalArgumentException("k (" + k + ") cannot exceed the number of points (" + points.length + ")"); + } + int dimension = points[0].length; + if (dimension == 0) { + throw new IllegalArgumentException("Points must have at least one dimension"); + } + for (int i = 0; i < points.length; i++) { + if (points[i] == null || points[i].length != dimension) { + throw new IllegalArgumentException("All points must share the same dimensionality; point " + i + " does not match"); + } + } + } + + private static double[][] validateAndCopyCentroids(double[][] points, double[][] initialCenters) { + Objects.requireNonNull(initialCenters, "initial centers must not be null"); + validatePoints(points, initialCenters.length); + int dimension = points[0].length; + double[][] centers = new double[initialCenters.length][]; + for (int i = 0; i < initialCenters.length; i++) { + if (initialCenters[i] == null || initialCenters[i].length != dimension) { + throw new IllegalArgumentException("Initial center " + i + " has inconsistent dimensionality"); + } + centers[i] = Arrays.copyOf(initialCenters[i], dimension); + } + return centers; + } + + private static double[][] randomInitialCentroids(double[][] points, int k, long seed) { + validatePoints(points, k); + int[] indices = new int[points.length]; + for (int i = 0; i < indices.length; i++) { + indices[i] = i; + } + Random random = new Random(seed); + for (int i = indices.length - 1; i > 0; i--) { + int j = random.nextInt(i + 1); + int tmp = indices[i]; + indices[i] = indices[j]; + indices[j] = tmp; + } + double[][] centers = new double[k][]; + for (int i = 0; i < k; i++) { + centers[i] = Arrays.copyOf(points[indices[i]], points[indices[i]].length); + } + return centers; + } + + public static final class ClusteringResult { + private final double[][] centers; + private final int[] labels; + private final int iterations; + private final boolean converged; + + ClusteringResult(double[][] centers, int[] labels, int iterations, boolean converged) { + this.centers = centers; + this.labels = labels; + this.iterations = iterations; + this.converged = converged; + } + + public double[][] getCenters() { + double[][] copy = new double[centers.length][]; + for (int i = 0; i < centers.length; i++) { + copy[i] = Arrays.copyOf(centers[i], centers[i].length); + } + return copy; + } + + public int[] getLabels() { + return Arrays.copyOf(labels, labels.length); + } + + public int getIterations() { + return iterations; + } + + public boolean hasConverged() { + return converged; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java new file mode 100644 index 000000000000..f7b82796bab0 --- /dev/null +++ b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java @@ -0,0 +1,214 @@ +package com.thealgorithms.machinelearning; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.thealgorithms.machinelearning.Clustering.ClusteringResult; +import org.junit.jupiter.api.Test; + +class ClustringTest { + + // K-mean + + @Test + void kMeansClustersTwoWellSeparatedGroups() { + double[][] points = { + {0.0, 0.0}, + {0.5, 0.5}, + {1.0, 0.0}, + {10.0, 10.0}, + {10.5, 10.5}, + {11.0, 10.0}, + }; + double[][] initialCentroids = {{0.0, 0.0}, {10.0, 10.0}}; + + ClusteringResult result = Clustering.kMeans(points, initialCentroids, 100, 1e-9); + int[] labels = result.getLabels(); + + assertEquals(labels[0], labels[1]); + assertEquals(labels[0], labels[2]); + assertEquals(labels[3], labels[4]); + assertEquals(labels[3], labels[5]); + assertNotEquals(labels[0], labels[3]); + assertTrue(result.hasConverged()); + } + + @Test + void kMeansWithKEqualsOneReturnsMean() { + double[][] points = {{0.0, 0.0}, {2.0, 0.0}, {1.0, 3.0}}; + double[][] initialCentroids = {{0.0, 0.0}}; + + ClusteringResult result = Clustering.kMeans(points, initialCentroids, 50, 1e-9); + + assertArrayEquals(new int[] {0, 0, 0}, result.getLabels()); + assertArrayEquals(new double[] {1.0, 1.0}, result.getCenters()[0], 1e-9); + } + + @Test + void kMeansWithKEqualsNKeepsEveryPointItsOwnCluster() { + double[][] points = {{0.0, 0.0}, {5.0, 5.0}, {10.0, 10.0}}; + double[][] initialCentroids = {{0.0, 0.0}, {5.0, 5.0}, {10.0, 10.0}}; + + ClusteringResult result = Clustering.kMeans(points, initialCentroids, 50, 1e-9); + + assertArrayEquals(new int[] {0, 1, 2}, result.getLabels()); + assertEquals(1, result.getIterations()); + assertTrue(result.hasConverged()); + } + + @Test + void kMeansSeededRandomInitializationIsReproducible() { + double[][] points = { + {0.0, 0.0}, + {0.1, 0.2}, + {8.0, 8.0}, + {8.2, 7.9}, + {4.0, 0.0}, + {4.1, 0.1}, + }; + + ClusteringResult r1 = Clustering.kMeans(points, 3, 7L, 100, 1e-9); + ClusteringResult r2 = Clustering.kMeans(points, 3, 7L, 100, 1e-9); + + assertArrayEquals(r1.getLabels(), r2.getLabels()); + for (int c = 0; c < r1.getCenters().length; c++) { + assertArrayEquals(r1.getCenters()[c], r2.getCenters()[c], 1e-9); + } + } + + // K-Medians + + @Test + void kMediansClustersTwoWellSeparatedGroups() { + double[][] points = { + {0.0, 0.0}, + {0.5, 0.5}, + {1.0, 0.0}, + {10.0, 10.0}, + {10.5, 10.5}, + {11.0, 10.0}, + }; + double[][] initialCenters = {{0.0, 0.0}, {10.0, 10.0}}; + + ClusteringResult result = Clustering.kMedians(points, initialCenters, 100, 1e-9); + int[] labels = result.getLabels(); + + assertEquals(labels[0], labels[1]); + assertEquals(labels[0], labels[2]); + assertEquals(labels[3], labels[4]); + assertEquals(labels[3], labels[5]); + assertNotEquals(labels[0], labels[3]); + assertTrue(result.hasConverged()); + } + + @Test + void kMediansIsMoreRobustToOutliersThanKMeans() { + // One tight group plus a single extreme outlier attached to it. + double[][] points = { + {1.0, 1.0}, + {1.1, 0.9}, + {0.9, 1.1}, + {1.0, 1.0}, + {100.0, 100.0}, // outlier + }; + double[][] initialCenter = {{1.0, 1.0}}; + + ClusteringResult meansResult = Clustering.kMeans(points, initialCenter, 50, 1e-9); + ClusteringResult mediansResult = Clustering.kMedians(points, initialCenter, 50, 1e-9); + + // The mean is dragged noticeably toward the outlier; the median is not. + double meanX = meansResult.getCenters()[0][0]; + double medianX = mediansResult.getCenters()[0][0]; + + assertTrue(meanX > medianX); + assertEquals(1.0, medianX, 1e-9); + } + + @Test + void kMediansWithKEqualsNKeepsEveryPointItsOwnCluster() { + double[][] points = {{0.0, 0.0}, {5.0, 5.0}, {10.0, 10.0}}; + double[][] initialCenters = {{0.0, 0.0}, {5.0, 5.0}, {10.0, 10.0}}; + + ClusteringResult result = Clustering.kMedians(points, initialCenters, 50, 1e-9); + + assertArrayEquals(new int[] {0, 1, 2}, result.getLabels()); + assertTrue(result.hasConverged()); + } + + @Test + void kMediansSeededRandomInitializationIsReproducible() { + double[][] points = { + {0.0, 0.0}, + {0.1, 0.2}, + {8.0, 8.0}, + {8.2, 7.9}, + {4.0, 0.0}, + {4.1, 0.1}, + }; + + ClusteringResult r1 = Clustering.kMedians(points, 3, 11L, 100, 1e-9); + ClusteringResult r2 = Clustering.kMedians(points, 3, 11L, 100, 1e-9); + + assertArrayEquals(r1.getLabels(), r2.getLabels()); + for (int c = 0; c < r1.getCenters().length; c++) { + assertArrayEquals(r1.getCenters()[c], r2.getCenters()[c], 1e-9); + } + } + + // ------------------------------------------------------------------ + // Shared validation (exercised through kMeans; identical path for kMedians) + // ------------------------------------------------------------------ + + @Test + void rejectsNonPositiveMaxIterations() { + double[][] points = {{0.0, 0.0}, {1.0, 1.0}}; + double[][] centers = {{0.0, 0.0}}; + assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, centers, 0, 1e-9)); + } + + @Test + void rejectsNegativeTolerance() { + double[][] points = {{0.0, 0.0}, {1.0, 1.0}}; + double[][] centers = {{0.0, 0.0}}; + assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, centers, 10, -1.0)); + } + + @Test + void rejectsKGreaterThanNumberOfPoints() { + double[][] points = {{0.0, 0.0}, {1.0, 1.0}}; + assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, 3, 42L, 10, 1e-9)); + assertThrows(IllegalArgumentException.class, () -> Clustering.kMedians(points, 3, 42L, 10, 1e-9)); + } + + @Test + void rejectsEmptyDataset() { + double[][] points = {}; + assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, 1, 42L, 10, 1e-9)); + } + + @Test + void rejectsInconsistentDimensions() { + double[][] points = {{0.0, 0.0}, {1.0, 1.0, 1.0}}; + assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, 1, 42L, 10, 1e-9)); + } + + @Test + void rejectsEmptyInitialCenters() { + // k is derived from initialCenters.length, so a zero-length array means k = 0. + double[][] points = {{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}}; + double[][] initialCenters = {}; + assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, initialCenters, 10, 1e-9)); + assertThrows(IllegalArgumentException.class, () -> Clustering.kMedians(points, initialCenters, 10, 1e-9)); + } + + @Test + void rejectsInitialCenterWithMismatchedDimension() { + double[][] points = {{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}}; + double[][] initialCenters = {{0.0, 0.0}, {1.0, 1.0, 1.0}}; + assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, initialCenters, 10, 1e-9)); + assertThrows(IllegalArgumentException.class, () -> Clustering.kMedians(points, initialCenters, 10, 1e-9)); + } +} \ No newline at end of file From 1066afa70920fa7de776b262681d62b012f0e2ef Mon Sep 17 00:00:00 2001 From: Ashish2343 Date: Sat, 8 Aug 2026 18:04:31 +0530 Subject: [PATCH 2/7] Typo Fixed --- .../machinelearning/Clustering.java | 113 ++++++++++++++++-- .../machinelearning/ClusteringTest.java | 30 ++++- 2 files changed, 128 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/thealgorithms/machinelearning/Clustering.java b/src/main/java/com/thealgorithms/machinelearning/Clustering.java index 6d9eacda775e..127a854ea644 100644 --- a/src/main/java/com/thealgorithms/machinelearning/Clustering.java +++ b/src/main/java/com/thealgorithms/machinelearning/Clustering.java @@ -6,11 +6,57 @@ import java.util.Objects; import java.util.Random; +/** + * Centroid-based partitional clustering algorithms. + * + *

This class currently provides two Lloyd-style iterative clustering algorithms that share + * the same assign/update/converge loop and differ only in their distance metric and how a + * cluster's center is recomputed: + * + *

    + *
  • K-Means — minimizes squared Euclidean distance; each center is the + * coordinate-wise mean of its cluster. Fast and simple, but sensitive to + * outliers.
  • + *
  • K-Medians — minimizes Manhattan (L1) distance; each center is the + * coordinate-wise median of its cluster. More robust to outliers than K-Means, + * at the cost of an O(n log n) sort per dimension during each update step.
  • + *
+ * + *

Both algorithms: + *

    + *
  1. Start from a set of {@code k} centers (supplied explicitly, or sampled from the + * dataset using a seeded {@link Random} for reproducibility).
  2. + *
  3. Assignment step: assign every point to its nearest center.
  4. + *
  5. Update step: recompute each center from the points assigned to it.
  6. + *
  7. Repeat steps 2-3 until no point changes cluster, every center moves less than a given + * tolerance, or a maximum number of iterations is reached.
  8. + *
+ * + *

Time complexity: O(n * k * d * iterations) for K-Means; + * O(n * k * d * iterations + k * d * n log n) for K-Medians (due to the per-dimension sort + * used to compute the median). + * + *

Limitations (both algorithms): + *

    + *
  • Sensitive to the initial choice of centers; poor initialization can converge to a + * suboptimal local minimum (see k-means++ for a smarter seeding strategy).
  • + *
  • The number of clusters {@code k} must be chosen in advance.
  • + *
  • Assume clusters are roughly convex and similarly sized/dense.
  • + *
+ * + * @see K-means clustering (Wikipedia) + * @see K-medians clustering (Wikipedia) + */ public final class Clustering { private Clustering() { - + // Utility class: only static entry points are exposed. } + + // ------------------------------------------------------------------ + // Public API — K-Means + // ------------------------------------------------------------------ + /** * Runs K-Means using explicit, caller-supplied initial centroids. Deterministic — the * recommended entry point for reproducible results and tests. @@ -22,18 +68,33 @@ private Clustering() { * @param tolerance convergence tolerance on center movement; must be non-negative * @return the clustering result */ - public static ClusteringResult kMeans(double[][] points, double[][] initialCentroids, int maxIterations, double tolerance) { validateParameters(maxIterations, tolerance); - double[][] centroids = validateAndCopyCentroids(points, initialCentroids); - return run(points, centroids, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean); + double[][] centers = validateAndCopyCenters(points, initialCentroids); + return run(points, centers, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean); } + + /** + * Runs K-Means, sampling {@code k} distinct points from the dataset (via a seeded + * {@link Random}) as initial centroids. Reproducible across runs given the same seed. + * + * @param points the dataset to cluster; non-empty, at least {@code k} points + * @param k the number of clusters; must be positive and ≤ number of points + * @param seed seed used to pick initial centroids + * @param maxIterations maximum number of iterations; must be positive + * @param tolerance convergence tolerance on center movement; must be non-negative + * @return the clustering result + */ public static ClusteringResult kMeans(double[][] points, int k, long seed, int maxIterations, double tolerance) { validateParameters(maxIterations, tolerance); - double[][] centroids = randomInitialCentroids(points, k, seed); - return run(points, centroids, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean); + double[][] centers = randomInitialCenters(points, k, seed); + return run(points, centers, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean); } + // ------------------------------------------------------------------ + // Public API — K-Medians + // ------------------------------------------------------------------ + /** * Runs K-Medians using explicit, caller-supplied initial centers. Deterministic — the * recommended entry point for reproducible results and tests. @@ -45,10 +106,9 @@ public static ClusteringResult kMeans(double[][] points, int k, long seed, int m * @param tolerance convergence tolerance on center movement; must be non-negative * @return the clustering result */ - public static ClusteringResult kMedians(double[][] points, double[][] initialCenters, int maxIterations, double tolerance) { validateParameters(maxIterations, tolerance); - double[][] centers = validateAndCopyCentroids(points, initialCenters); + double[][] centers = validateAndCopyCenters(points, initialCenters); return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median); } @@ -63,13 +123,15 @@ public static ClusteringResult kMedians(double[][] points, double[][] initialCen * @param tolerance convergence tolerance on center movement; must be non-negative * @return the clustering result */ - public static ClusteringResult kMedians(double[][] points, int k, long seed, int maxIterations, double tolerance) { validateParameters(maxIterations, tolerance); - double[][] centers = randomInitialCentroids(points, k, seed); + double[][] centers = randomInitialCenters(points, k, seed); return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median); } + // ------------------------------------------------------------------ + // Shared iterative core + // ------------------------------------------------------------------ @FunctionalInterface private interface DistanceFunction { @@ -152,6 +214,10 @@ private static double maxCenterShift(double[][] oldCenters, double[][] newCenter return max; } + // ------------------------------------------------------------------ + // Distance functions + // ------------------------------------------------------------------ + private static double squaredEuclideanDistance(double[] a, double[] b) { double sum = 0.0; for (int d = 0; d < a.length; d++) { @@ -173,6 +239,10 @@ private static double manhattanDistance(double[] a, double[] b) { return sum; } + // ------------------------------------------------------------------ + // Center functions + // ------------------------------------------------------------------ + private static double[] mean(List clusterPoints, int dimension) { double[] result = new double[dimension]; for (double[] p : clusterPoints) { @@ -204,6 +274,10 @@ private static double[] median(List clusterPoints, int dimension) { return result; } + // ------------------------------------------------------------------ + // Validation & initialization helpers + // ------------------------------------------------------------------ + private static void validateParameters(int maxIterations, double tolerance) { if (maxIterations <= 0) { throw new IllegalArgumentException("maxIterations must be positive, got " + maxIterations); @@ -234,7 +308,7 @@ private static void validatePoints(double[][] points, int k) { } } - private static double[][] validateAndCopyCentroids(double[][] points, double[][] initialCenters) { + private static double[][] validateAndCopyCenters(double[][] points, double[][] initialCenters) { Objects.requireNonNull(initialCenters, "initial centers must not be null"); validatePoints(points, initialCenters.length); int dimension = points[0].length; @@ -248,7 +322,7 @@ private static double[][] validateAndCopyCentroids(double[][] points, double[][] return centers; } - private static double[][] randomInitialCentroids(double[][] points, int k, long seed) { + private static double[][] randomInitialCenters(double[][] points, int k, long seed) { validatePoints(points, k); int[] indices = new int[points.length]; for (int i = 0; i < indices.length; i++) { @@ -268,6 +342,14 @@ private static double[][] randomInitialCentroids(double[][] points, int k, long return centers; } + // ------------------------------------------------------------------ + // Result holder + // ------------------------------------------------------------------ + + /** + * The outcome of a clustering run: final centers, per-point cluster labels, and metadata + * about how the run terminated. + */ public static final class ClusteringResult { private final double[][] centers; private final int[] labels; @@ -281,6 +363,7 @@ public static final class ClusteringResult { this.converged = converged; } + /** Returns the final cluster centers, one row per cluster. */ public double[][] getCenters() { double[][] copy = new double[centers.length][]; for (int i = 0; i < centers.length; i++) { @@ -289,16 +372,20 @@ public double[][] getCenters() { return copy; } + /** Returns the cluster index assigned to each input point, in input order. */ public int[] getLabels() { return Arrays.copyOf(labels, labels.length); } + /** Returns the number of iterations actually performed. */ public int getIterations() { return iterations; } + /** Returns whether the algorithm converged before hitting {@code maxIterations}. */ public boolean hasConverged() { return converged; } } -} \ No newline at end of file +} + diff --git a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java index f7b82796bab0..960033e0014e 100644 --- a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java +++ b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java @@ -9,9 +9,11 @@ import com.thealgorithms.machinelearning.Clustering.ClusteringResult; import org.junit.jupiter.api.Test; -class ClustringTest { +class ClusteringTest { - // K-mean + // ------------------------------------------------------------------ + // K-Means + // ------------------------------------------------------------------ @Test void kMeansClustersTwoWellSeparatedGroups() { @@ -79,7 +81,31 @@ void kMeansSeededRandomInitializationIsReproducible() { } } + @Test + void emptyClusterKeepsItsPreviousCenterUnchanged() { + + double[][] points = { + {0.0, 0.0}, + {0.1, 0.1}, + {0.2, 0.0}, + {10.0, 10.0}, + {10.1, 10.1}, + {10.2, 10.0}, + }; + double[][] initialCenters = {{0.0, 0.0}, {10.0, 10.0}, {10.0, 10.0}}; + + ClusteringResult result = Clustering.kMeans(points, initialCenters, 1, 1e-9); + + assertEquals(1, result.getIterations()); + assertArrayEquals(new double[] {10.0, 10.0}, result.getCenters()[2], 1e-9); + for (int label : result.getLabels()) { + assertNotEquals(2, label); + } + } + + // ------------------------------------------------------------------ // K-Medians + // ------------------------------------------------------------------ @Test void kMediansClustersTwoWellSeparatedGroups() { From 85d578c478647208dc09e268c6e5d7e98b7f4fa9 Mon Sep 17 00:00:00 2001 From: Ashish2343 Date: Sat, 8 Aug 2026 19:04:34 +0530 Subject: [PATCH 3/7] Clint Error Fixed --- .../com/thealgorithms/machinelearning/Clustering.java | 4 ---- .../com/thealgorithms/machinelearning/ClusteringTest.java | 8 ++------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/thealgorithms/machinelearning/Clustering.java b/src/main/java/com/thealgorithms/machinelearning/Clustering.java index 127a854ea644..0d60d019f2f8 100644 --- a/src/main/java/com/thealgorithms/machinelearning/Clustering.java +++ b/src/main/java/com/thealgorithms/machinelearning/Clustering.java @@ -363,7 +363,6 @@ public static final class ClusteringResult { this.converged = converged; } - /** Returns the final cluster centers, one row per cluster. */ public double[][] getCenters() { double[][] copy = new double[centers.length][]; for (int i = 0; i < centers.length; i++) { @@ -372,12 +371,10 @@ public double[][] getCenters() { return copy; } - /** Returns the cluster index assigned to each input point, in input order. */ public int[] getLabels() { return Arrays.copyOf(labels, labels.length); } - /** Returns the number of iterations actually performed. */ public int getIterations() { return iterations; } @@ -388,4 +385,3 @@ public boolean hasConverged() { } } } - diff --git a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java index 960033e0014e..36899ae6b7f7 100644 --- a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java +++ b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java @@ -134,11 +134,7 @@ void kMediansClustersTwoWellSeparatedGroups() { void kMediansIsMoreRobustToOutliersThanKMeans() { // One tight group plus a single extreme outlier attached to it. double[][] points = { - {1.0, 1.0}, - {1.1, 0.9}, - {0.9, 1.1}, - {1.0, 1.0}, - {100.0, 100.0}, // outlier + {1.0, 1.0}, {1.1, 0.9}, {0.9, 1.1}, {1.0, 1.0}, {100.0, 100.0}, // outlier }; double[][] initialCenter = {{1.0, 1.0}}; @@ -237,4 +233,4 @@ void rejectsInitialCenterWithMismatchedDimension() { assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, initialCenters, 10, 1e-9)); assertThrows(IllegalArgumentException.class, () -> Clustering.kMedians(points, initialCenters, 10, 1e-9)); } -} \ No newline at end of file +} From 61632a08ab235760a0e4347bc2af594602529a70 Mon Sep 17 00:00:00 2001 From: Ashish2343 Date: Sun, 9 Aug 2026 07:59:14 +0530 Subject: [PATCH 4/7] mean method fixed as per clint format --- .../java/com/thealgorithms/machinelearning/Clustering.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/thealgorithms/machinelearning/Clustering.java b/src/main/java/com/thealgorithms/machinelearning/Clustering.java index 0d60d019f2f8..f67b20e46c6e 100644 --- a/src/main/java/com/thealgorithms/machinelearning/Clustering.java +++ b/src/main/java/com/thealgorithms/machinelearning/Clustering.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Random; @@ -54,7 +55,7 @@ private Clustering() { } // ------------------------------------------------------------------ - // Public API — K-Means + // K-Means // ------------------------------------------------------------------ /** @@ -92,7 +93,7 @@ public static ClusteringResult kMeans(double[][] points, int k, long seed, int m } // ------------------------------------------------------------------ - // Public API — K-Medians + // K-Medians // ------------------------------------------------------------------ /** @@ -243,7 +244,7 @@ private static double manhattanDistance(double[] a, double[] b) { // Center functions // ------------------------------------------------------------------ - private static double[] mean(List clusterPoints, int dimension) { + private static double[] mean(Collection clusterPoints, int dimension) { double[] result = new double[dimension]; for (double[] p : clusterPoints) { for (int d = 0; d < dimension; d++) { From 929c92dc7c49fedd76d471038c6031bb780982a8 Mon Sep 17 00:00:00 2001 From: Ashish2343 Date: Sun, 9 Aug 2026 10:35:25 +0530 Subject: [PATCH 5/7] Fixed static imports --- .../thealgorithms/machinelearning/ClusteringTest.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java index 36899ae6b7f7..27744f20be9d 100644 --- a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java +++ b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java @@ -1,14 +1,10 @@ package com.thealgorithms.machinelearning; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - import com.thealgorithms.machinelearning.Clustering.ClusteringResult; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + class ClusteringTest { // ------------------------------------------------------------------ @@ -233,4 +229,4 @@ void rejectsInitialCenterWithMismatchedDimension() { assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, initialCenters, 10, 1e-9)); assertThrows(IllegalArgumentException.class, () -> Clustering.kMedians(points, initialCenters, 10, 1e-9)); } -} +} \ No newline at end of file From 2ad6926135b4a2cef5b7ad4ffeedae15d3588c64 Mon Sep 17 00:00:00 2001 From: Ashish2343 Date: Sun, 9 Aug 2026 11:14:53 +0530 Subject: [PATCH 6/7] Fixed static imports, clint formatting --- .../machinelearning/ClusteringTest.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java index 27744f20be9d..146cd1380f01 100644 --- a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java +++ b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java @@ -1,10 +1,14 @@ package com.thealgorithms.machinelearning; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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 com.thealgorithms.machinelearning.Clustering.ClusteringResult; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class ClusteringTest { // ------------------------------------------------------------------ @@ -30,7 +34,7 @@ void kMeansClustersTwoWellSeparatedGroups() { assertEquals(labels[0], labels[2]); assertEquals(labels[3], labels[4]); assertEquals(labels[3], labels[5]); - assertNotEquals(labels[0], labels[3]); + Assertions.assertNotEquals(labels[0], labels[3]); assertTrue(result.hasConverged()); } @@ -95,7 +99,7 @@ void emptyClusterKeepsItsPreviousCenterUnchanged() { assertEquals(1, result.getIterations()); assertArrayEquals(new double[] {10.0, 10.0}, result.getCenters()[2], 1e-9); for (int label : result.getLabels()) { - assertNotEquals(2, label); + Assertions.assertNotEquals(2, label); } } @@ -122,7 +126,7 @@ void kMediansClustersTwoWellSeparatedGroups() { assertEquals(labels[0], labels[2]); assertEquals(labels[3], labels[4]); assertEquals(labels[3], labels[5]); - assertNotEquals(labels[0], labels[3]); + Assertions.assertNotEquals(labels[0], labels[3]); assertTrue(result.hasConverged()); } From ef0e011dc2a599acb864d5930e2e5f851e45e25f Mon Sep 17 00:00:00 2001 From: Ashish2343 Date: Sun, 9 Aug 2026 11:21:51 +0530 Subject: [PATCH 7/7] new line --- .../java/com/thealgorithms/machinelearning/ClusteringTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java index 146cd1380f01..85fd6d7befb3 100644 --- a/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java +++ b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java @@ -233,4 +233,4 @@ void rejectsInitialCenterWithMismatchedDimension() { assertThrows(IllegalArgumentException.class, () -> Clustering.kMeans(points, initialCenters, 10, 1e-9)); assertThrows(IllegalArgumentException.class, () -> Clustering.kMedians(points, initialCenters, 10, 1e-9)); } -} \ No newline at end of file +}