@@ -294,33 +294,75 @@ public int getDistinctDistanceCount() {
294294 /**
295295 * Generates a human-readable text report of the distance distribution.
296296 *
297+ * <p><b>Performance:</b> Computes all aggregate statistics (average path
298+ * length, Wiener index, harmonic mean, histogram, separation ratio, and
299+ * percentiles) in a single O(V²) pass over the distance matrix, then
300+ * sorts the collected distances once for all percentile queries. The
301+ * previous implementation made 8+ separate O(V²) passes and sorted the
302+ * distance list 3 times independently.</p>
303+ *
297304 * @return multi-line report string
298305 */
299306 public String generateReport () {
300307 ensureComputed ();
301308 StringBuilder sb = new StringBuilder ();
302309 int n = graph .getVertexCount ();
303- int e = graph .getEdgeCount ();
310+ int edgeCount = graph .getEdgeCount ();
311+ List <String > vertices = new ArrayList <>(graph .getVertices ());
312+
313+ // Single pass over the upper triangle of the distance matrix to
314+ // compute all aggregate metrics simultaneously.
315+ long distSum = 0 ;
316+ long reachableCount = 0 ;
317+ double reciprocalSum = 0.0 ;
318+ long totalPairs = (long ) n * (n - 1 ) / 2 ;
319+ Map <Integer , Integer > hist = new TreeMap <>();
320+ List <Integer > finiteDistances = new ArrayList <>();
321+
322+ for (int i = 0 ; i < vertices .size (); i ++) {
323+ Map <String , Integer > row = distanceMatrix .get (vertices .get (i ));
324+ for (int j = i + 1 ; j < vertices .size (); j ++) {
325+ Integer d = row .get (vertices .get (j ));
326+ if (d != null && d > 0 ) {
327+ distSum += d ;
328+ reachableCount ++;
329+ reciprocalSum += 1.0 / d ;
330+ hist .merge (d , 1 , Integer ::sum );
331+ finiteDistances .add (d );
332+ }
333+ }
334+ }
335+
336+ double avgPath = reachableCount == 0 ? 0.0 : (double ) distSum / reachableCount ;
337+ double harmonicMean = reciprocalSum == 0 ? Double .POSITIVE_INFINITY
338+ : (double ) totalPairs / reciprocalSum ;
339+ double separation = n <= 1 ? 0.0 : 1.0 - (double ) reachableCount / totalPairs ;
340+
341+ // Sort once for all percentile queries
342+ Collections .sort (finiteDistances );
304343
305344 sb .append ("=== Distance Distribution Report ===\n \n " );
306- sb .append (String .format ("Vertices: %d | Edges: %d\n " , n , e ));
307- sb .append (String .format ("Average path length: %.4f\n " , getAveragePathLength () ));
308- sb .append (String .format ("Wiener index: %d\n " , getWienerIndex () ));
309- sb .append (String .format ("Harmonic mean distance: %.4f\n " , getHarmonicMeanDistance () ));
310- sb .append (String .format ("Median distance: %d\n " , getMedianDistance ( )));
311- sb .append (String .format ("90th percentile: %d\n " , getDistancePercentile ( 90 )));
312- sb .append (String .format ("95th percentile: %d\n " , getDistancePercentile ( 95 )));
313- sb .append (String .format ("Separation ratio: %.4f\n " , getSeparationRatio () ));
314- sb .append (String .format ("Distinct distances: %d\n \n " , getDistinctDistanceCount ()));
345+ sb .append (String .format ("Vertices: %d | Edges: %d\n " , n , edgeCount ));
346+ sb .append (String .format ("Average path length: %.4f\n " , avgPath ));
347+ sb .append (String .format ("Wiener index: %d\n " , distSum ));
348+ sb .append (String .format ("Harmonic mean distance: %.4f\n " , harmonicMean ));
349+ sb .append (String .format ("Median distance: %d\n " , percentileFromSorted ( finiteDistances , 50 )));
350+ sb .append (String .format ("90th percentile: %d\n " , percentileFromSorted ( finiteDistances , 90 )));
351+ sb .append (String .format ("95th percentile: %d\n " , percentileFromSorted ( finiteDistances , 95 )));
352+ sb .append (String .format ("Separation ratio: %.4f\n " , separation ));
353+ sb .append (String .format ("Distinct distances: %d\n \n " , hist . size ()));
315354
316- Map <Integer , Integer > hist = getDistanceHistogram ();
317355 if (!hist .isEmpty ()) {
318356 sb .append ("Distance Histogram:\n " );
319- int maxCount = hist .values ().stream ().max (Integer ::compareTo ).orElse (1 );
357+ int maxCount = 0 ;
358+ for (int c : hist .values ()) {
359+ if (c > maxCount ) maxCount = c ;
360+ }
320361 for (Map .Entry <Integer , Integer > entry : hist .entrySet ()) {
321362 int barLen = (int ) Math .ceil (40.0 * entry .getValue () / maxCount );
322- String bar = String .join ("" , Collections .nCopies (barLen , "█" ));
323- sb .append (String .format (" d=%d: %s %d\n " , entry .getKey (), bar , entry .getValue ()));
363+ StringBuilder bar = new StringBuilder (barLen );
364+ for (int b = 0 ; b < barLen ; b ++) bar .append ('\u2588' );
365+ sb .append (String .format (" d=%d: %s %d\n " , entry .getKey (), bar .toString (), entry .getValue ()));
324366 }
325367 }
326368
@@ -335,6 +377,20 @@ public String generateReport() {
335377 return sb .toString ();
336378 }
337379
380+ /**
381+ * Extracts a percentile from a pre-sorted list of distances.
382+ *
383+ * @param sorted sorted list of finite pairwise distances
384+ * @param percentile the percentile (0–100)
385+ * @return the distance at the given percentile, or -1 if list is empty
386+ */
387+ private static int percentileFromSorted (List <Integer > sorted , double percentile ) {
388+ if (sorted .isEmpty ()) return -1 ;
389+ int index = (int ) Math .ceil (percentile / 100.0 * sorted .size ()) - 1 ;
390+ if (index < 0 ) index = 0 ;
391+ return sorted .get (index );
392+ }
393+
338394 /**
339395 * Exports the distance matrix as a CSV string.
340396 *
0 commit comments