@@ -252,30 +252,42 @@ public double similarity(String u, String v, Metric metric) {
252252 * Uses a min-heap to efficiently track the top-k pairs without
253253 * storing all O(n²) scores.
254254 *
255+ * <p><b>Performance:</b> For JACCARD, OVERLAP, ADAMIC_ADAR, and COSINE
256+ * metrics, similarity is always 0 when two nodes share no common
257+ * neighbors. This method exploits that by enumerating only <b>2-hop
258+ * pairs</b> (neighbors-of-neighbors) instead of all O(V²) vertex
259+ * pairs — reducing work to O(V·Δ²) where Δ is max degree. On sparse
260+ * graphs (Δ ≪ V) this is orders of magnitude faster.
261+ * STRUCTURAL_EQUIVALENCE can be non-zero for any pair, so it still
262+ * uses the O(V²) sweep.</p>
263+ *
255264 * @param metric the similarity metric to use
256265 * @param k maximum number of pairs to return
257266 * @return list of scored pairs, sorted by score descending
258267 */
259268 public List <ScoredPair > mostSimilar (Metric metric , int k ) {
260269 if (k <= 0 ) return Collections .emptyList ();
261270
262- List <String > vertices = new ArrayList <String >(graph .getVertices ());
263- Collections .sort (vertices ); // deterministic ordering
264271 PriorityQueue <ScoredPair > minHeap = new PriorityQueue <ScoredPair >(k + 1 ,
265272 (ScoredPair a , ScoredPair b ) -> {
266273 return Double .compare (a .score , b .score ); // min-heap by score
267274 }
268275 );
269276
270- for (int i = 0 ; i < vertices .size (); i ++) {
271- for (int j = i + 1 ; j < vertices .size (); j ++) {
272- double score = similarity (vertices .get (i ), vertices .get (j ), metric );
273- ScoredPair sp = new ScoredPair (vertices .get (i ), vertices .get (j ), score );
274- minHeap .offer (sp );
275- if (minHeap .size () > k ) {
276- minHeap .poll ();
277+ if (metric == Metric .STRUCTURAL_EQUIVALENCE ) {
278+ // SE can be non-zero for any pair — full O(V²) sweep required
279+ List <String > vertices = new ArrayList <String >(graph .getVertices ());
280+ Collections .sort (vertices );
281+ for (int i = 0 ; i < vertices .size (); i ++) {
282+ for (int j = i + 1 ; j < vertices .size (); j ++) {
283+ double score = structuralEquivalence (vertices .get (i ), vertices .get (j ));
284+ offerToHeap (minHeap , vertices .get (i ), vertices .get (j ), score , k );
277285 }
278286 }
287+ } else {
288+ // JACCARD, OVERLAP, ADAMIC_ADAR, COSINE: score > 0 only when
289+ // nodes share ≥1 common neighbor. Enumerate 2-hop pairs only.
290+ enumerate2HopPairs (metric , minHeap , k );
279291 }
280292
281293 List <ScoredPair > result = new ArrayList <ScoredPair >(minHeap );
@@ -354,20 +366,52 @@ public Map<String, Double> similarityMatrix(Metric metric) {
354366 /**
355367 * Find all node pairs with similarity above a threshold.
356368 *
369+ * <p><b>Performance:</b> For JACCARD, OVERLAP, ADAMIC_ADAR, and COSINE
370+ * metrics (which are 0 when nodes share no common neighbor), only
371+ * 2-hop pairs are evaluated — O(V·Δ²) instead of O(V²). When
372+ * threshold > 0 this is both correct and complete since unreachable
373+ * pairs always score 0.</p>
374+ *
357375 * @param metric the similarity metric to use
358376 * @param threshold minimum score (inclusive)
359377 * @return list of scored pairs above threshold, sorted descending
360378 */
361379 public List <ScoredPair > similarPairsAboveThreshold (Metric metric , double threshold ) {
362- List <String > vertices = new ArrayList <String >(graph .getVertices ());
363- Collections .sort (vertices );
364380 List <ScoredPair > result = new ArrayList <ScoredPair >();
365381
366- for (int i = 0 ; i < vertices .size (); i ++) {
367- for (int j = i + 1 ; j < vertices .size (); j ++) {
368- double score = similarity (vertices .get (i ), vertices .get (j ), metric );
369- if (score >= threshold ) {
370- result .add (new ScoredPair (vertices .get (i ), vertices .get (j ), score ));
382+ if (metric != Metric .STRUCTURAL_EQUIVALENCE && threshold > 0 ) {
383+ // 2-hop fast path: only pairs sharing ≥1 common neighbor can score > 0
384+ List <String > vertices = new ArrayList <String >(graph .getVertices ());
385+ Collections .sort (vertices );
386+ Map <String , Integer > vertexOrd = new HashMap <String , Integer >(vertices .size () * 2 );
387+ for (int i = 0 ; i < vertices .size (); i ++) vertexOrd .put (vertices .get (i ), i );
388+
389+ for (String u : vertices ) {
390+ Set <String > nu = neighbors (u );
391+ if (nu .isEmpty ()) continue ;
392+ int uOrd = vertexOrd .get (u );
393+ Set <String > seen = new HashSet <String >();
394+ for (String w : nu ) {
395+ for (String v : neighbors (w )) {
396+ if (vertexOrd .get (v ) > uOrd && seen .add (v )) {
397+ double score = similarity (u , v , metric );
398+ if (score >= threshold ) {
399+ result .add (new ScoredPair (u , v , score ));
400+ }
401+ }
402+ }
403+ }
404+ }
405+ } else {
406+ // Full O(V²) sweep needed for SE or threshold <= 0
407+ List <String > vertices = new ArrayList <String >(graph .getVertices ());
408+ Collections .sort (vertices );
409+ for (int i = 0 ; i < vertices .size (); i ++) {
410+ for (int j = i + 1 ; j < vertices .size (); j ++) {
411+ double score = similarity (vertices .get (i ), vertices .get (j ), metric );
412+ if (score >= threshold ) {
413+ result .add (new ScoredPair (vertices .get (i ), vertices .get (j ), score ));
414+ }
371415 }
372416 }
373417 }
@@ -403,6 +447,52 @@ public String report(String target, int k) {
403447 return sb .toString ();
404448 }
405449
450+ // ── 2-hop enumeration ────────────────────────────────────────
451+
452+ /**
453+ * Enumerates only vertex pairs that share at least one common neighbor
454+ * (2-hop reachable pairs). For each such pair, computes the given
455+ * similarity metric and inserts into the min-heap.
456+ *
457+ * <p>Complexity: O(V·Δ²) where Δ is max degree — compared to O(V²)
458+ * for the full enumeration. On sparse graphs where Δ ≪ V this gives
459+ * orders-of-magnitude speedup. A per-source seen set deduplicates
460+ * pairs, and lexicographic ordering (v > u) prevents evaluating
461+ * each pair twice.</p>
462+ */
463+ private void enumerate2HopPairs (Metric metric , PriorityQueue <ScoredPair > minHeap , int k ) {
464+ List <String > vertices = new ArrayList <String >(graph .getVertices ());
465+ Collections .sort (vertices );
466+ Map <String , Integer > vertexOrd = new HashMap <String , Integer >(vertices .size () * 2 );
467+ for (int i = 0 ; i < vertices .size (); i ++) vertexOrd .put (vertices .get (i ), i );
468+
469+ for (String u : vertices ) {
470+ Set <String > nu = neighbors (u );
471+ if (nu .isEmpty ()) continue ;
472+ int uOrd = vertexOrd .get (u );
473+
474+ // Walk 2-hop: u -> w -> v where v > u (lexicographic) and v ∉ seen
475+ Set <String > seen = new HashSet <String >();
476+ for (String w : nu ) {
477+ for (String v : neighbors (w )) {
478+ if (vertexOrd .get (v ) > uOrd && seen .add (v )) {
479+ double score = similarity (u , v , metric );
480+ offerToHeap (minHeap , u , v , score , k );
481+ }
482+ }
483+ }
484+ }
485+ }
486+
487+ /** Offers a scored pair to a bounded min-heap. */
488+ private static void offerToHeap (PriorityQueue <ScoredPair > heap ,
489+ String u , String v , double score , int k ) {
490+ if (heap .size () < k || score > heap .peek ().score ) {
491+ heap .offer (new ScoredPair (u , v , score ));
492+ if (heap .size () > k ) heap .poll ();
493+ }
494+ }
495+
406496 // ── Internal helpers ────────────────────────────────────────
407497
408498 private Set <String > neighbors (String node ) {
0 commit comments