Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 584f8fb

Browse files
perf: O(V·Δ²) 2-hop pair enumeration in NodeSimilarityAnalyzer
mostSimilar() and similarPairsAboveThreshold() previously iterated all O(V²) vertex pairs for every metric. For JACCARD, OVERLAP, ADAMIC_ADAR, and COSINE, similarity is always 0 when two nodes share no common neighbor — so only 2-hop reachable pairs need evaluation. New enumerate2HopPairs() walks neighbors-of-neighbors with a per-source seen set and lexicographic ordering to deduplicate. On sparse graphs (Δ ≪ V) this reduces work by orders of magnitude. STRUCTURAL_EQUIVALENCE retains the O(V²) sweep since non-adjacent pairs can still have non-zero structural equivalence.
1 parent 467366c commit 584f8fb

1 file changed

Lines changed: 106 additions & 16 deletions

File tree

Gvisual/src/gvisual/NodeSimilarityAnalyzer.java

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)