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..f67b20e46c6e
--- /dev/null
+++ b/src/main/java/com/thealgorithms/machinelearning/Clustering.java
@@ -0,0 +1,388 @@
+package com.thealgorithms.machinelearning;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+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:
+ *
+ * - Start from a set of {@code k} centers (supplied explicitly, or sampled from the
+ * dataset using a seeded {@link Random} for reproducibility).
+ * - Assignment step: assign every point to its nearest center.
+ * - Update step: recompute each center from the points assigned to it.
+ * - 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.
+ *
+ *
+ * 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.
+ }
+
+ // ------------------------------------------------------------------
+ // K-Means
+ // ------------------------------------------------------------------
+
+ /**
+ * 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[][] 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[][] centers = randomInitialCenters(points, k, seed);
+ return run(points, centers, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean);
+ }
+
+ // ------------------------------------------------------------------
+ // K-Medians
+ // ------------------------------------------------------------------
+
+ /**
+ * 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 = validateAndCopyCenters(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 = randomInitialCenters(points, k, seed);
+ return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median);
+ }
+
+ // ------------------------------------------------------------------
+ // Shared iterative core
+ // ------------------------------------------------------------------
+
+ @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;
+ }
+
+ // ------------------------------------------------------------------
+ // Distance functions
+ // ------------------------------------------------------------------
+
+ 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;
+ }
+
+ // ------------------------------------------------------------------
+ // Center functions
+ // ------------------------------------------------------------------
+
+ private static double[] mean(Collection 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;
+ }
+
+ // ------------------------------------------------------------------
+ // Validation & initialization helpers
+ // ------------------------------------------------------------------
+
+ 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[][] validateAndCopyCenters(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[][] randomInitialCenters(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;
+ }
+
+ // ------------------------------------------------------------------
+ // 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;
+ 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;
+ }
+
+ /** Returns whether the algorithm converged before hitting {@code maxIterations}. */
+ public boolean hasConverged() {
+ return converged;
+ }
+ }
+}
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..85fd6d7befb3
--- /dev/null
+++ b/src/test/java/com/thealgorithms/machinelearning/ClusteringTest.java
@@ -0,0 +1,236 @@
+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;
+
+class ClusteringTest {
+
+ // ------------------------------------------------------------------
+ // K-Means
+ // ------------------------------------------------------------------
+
+ @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]);
+ Assertions.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);
+ }
+ }
+
+ @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()) {
+ Assertions.assertNotEquals(2, label);
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // 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]);
+ Assertions.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));
+ }
+}