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