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

Commit ba7e65a

Browse files
perf: fuse betweenness+closeness BFS into single pass; add 21 link prediction tests
NodeCentralityAnalyzer: previously ran two separate O(V*E) BFS sweeps -- one for betweenness (Brandes) and one for closeness centrality. Fused into a single computeBetweennessAndCloseness() method that does both metrics in one BFS per source vertex, halving total graph traversal. - Betweenness: Brandes back-propagation on shared BFS stack - Closeness: Wasserman-Faust normalization from shared distance data - Removed redundant computeBetweennessCentrality() and computeClosenessCentrality() private methods - 445 -> 423 lines (22 lines saved) LinkPredictionAnalyzerTest: expanded from 10 to 31 tests (+21): - Single vertex, two vertices with no edge - Isolated vertices in larger graph - Disconnected components (cross-component predictions) - topK=0 boundary, topK limits results - Results sorted by score descending - Jaccard with different-sized neighborhoods (0.25 score) - Adamic-Adar exact score (1/log(2)), multiple common neighbors - Preferential attachment exact score (deg product) - PredictionResult metadata (vertices, edges, possible, density) - Density 0 for no edges, 1 for complete graph - PredictedLink toString format - Ensemble normalized scores in [0,1], ensemble on empty graph - Summary contains method name, vertex/edge counts - Common neighbors set correctness (2 shared) - CommonNeighbors set unmodifiable - Predictions list unmodifiable
1 parent 035110c commit ba7e65a

2 files changed

Lines changed: 500 additions & 81 deletions

File tree

Gvisual/src/gvisual/NodeCentralityAnalyzer.java

Lines changed: 54 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,16 @@ public int hashCode() {
116116
/**
117117
* Computes all centrality metrics. Must be called before querying results.
118118
* Automatically skips recomputation if already computed.
119+
*
120+
* <p>Betweenness and closeness are computed in a single fused BFS pass
121+
* per source vertex, halving the O(V·E) traversal cost compared to
122+
* running two independent BFS sweeps.</p>
119123
*/
120124
public void compute() {
121125
if (computed) return;
122126

123127
computeDegreeCentrality();
124-
computeBetweennessCentrality();
125-
computeClosenessCentrality();
128+
computeBetweennessAndCloseness();
126129
computed = true;
127130
}
128131

@@ -306,6 +309,9 @@ public Map<String, Object> getSummary() {
306309
/**
307310
* Classifies the network topology based on degree distribution characteristics.
308311
*
312+
* <p>Reuses the degree data already computed by {@link #computeDegreeCentrality()}
313+
* instead of iterating all vertices again.</p>
314+
*
309315
* @return one of: "Trivial" (≤1 node), "Disconnected" (isolated nodes exist),
310316
* "Hub-and-Spoke" (one node dominates), "Distributed" (even degree distribution),
311317
* "Hierarchical" (moderate degree variance)
@@ -317,7 +323,7 @@ public String classifyTopology() {
317323
if (n <= 1) return "Trivial";
318324
if (graph.getEdgeCount() == 0) return "Disconnected";
319325

320-
// Check for isolated nodes
326+
// Reuse cached degree data from computeDegreeCentrality()
321327
int isolated = 0;
322328
int maxDeg = 0;
323329
double sumDeg = 0;
@@ -333,11 +339,9 @@ public String classifyTopology() {
333339
double avgDeg = sumDeg / n;
334340
if (avgDeg == 0) return "Disconnected";
335341

336-
// Check hub-and-spoke: max degree much higher than average
337342
double hubRatio = maxDeg / avgDeg;
338343
if (hubRatio > 4.0 && maxDeg > n * 0.3) return "Hub-and-Spoke";
339344

340-
// Check for distributed (coefficient of variation of degree < 0.5)
341345
double sumSqDiff = 0;
342346
for (String node : graph.getVertices()) {
343347
double diff = graph.degree(node) - avgDeg;
@@ -368,24 +372,29 @@ private void computeDegreeCentrality() {
368372
}
369373

370374
/**
371-
* Betweenness centrality using Brandes' algorithm (2001).
372-
* Complexity: O(V * E) for unweighted graphs.
375+
* Betweenness + closeness centrality in a single fused BFS pass per source.
376+
*
377+
* <p>Previously, betweenness used Brandes' algorithm (BFS + back-propagation
378+
* from each source) and closeness used a separate BFS from each source —
379+
* two independent O(V·E) traversals. This fused version does both in one
380+
* BFS per source vertex, halving the total graph traversal cost.</p>
373381
*
374-
* <p>For each source vertex s, performs a BFS to compute shortest paths,
375-
* then accumulates dependencies on the back-sweep. The result is normalized
376-
* by 2/((V-1)(V-2)) for undirected graphs to give values in [0, 1].</p>
382+
* <p>Betweenness: Brandes (2001), O(V·E) for unweighted graphs, normalized
383+
* by (V-1)(V-2) for undirected. Closeness: Wasserman-Faust normalization
384+
* for potentially disconnected graphs.</p>
377385
*/
378-
private void computeBetweennessCentrality() {
379-
// Initialize all betweenness to 0
386+
private void computeBetweennessAndCloseness() {
387+
// Initialize all to 0
380388
for (String node : graph.getVertices()) {
381389
betweennessCentrality.put(node, 0.0);
390+
closenessCentrality.put(node, 0.0);
382391
}
383392

384393
int n = graph.getVertexCount();
385-
if (n <= 2) return;
394+
if (n <= 1) return;
386395

387396
for (String s : graph.getVertices()) {
388-
// Stacks, predecessors, sigma, distance
397+
// --- Single BFS from s, shared by both metrics ---
389398
Deque<String> stack = new ArrayDeque<String>();
390399
Map<String, List<String>> predecessors = new HashMap<String, List<String>>();
391400
Map<String, Integer> sigma = new HashMap<String, Integer>();
@@ -400,10 +409,13 @@ private void computeBetweennessCentrality() {
400409
sigma.put(s, 1);
401410
dist.put(s, 0);
402411

403-
// BFS from s
404412
Queue<String> queue = new LinkedList<String>();
405413
queue.add(s);
406414

415+
// Closeness accumulators for this source
416+
int sumDist = 0;
417+
int reachable = 0;
418+
407419
while (!queue.isEmpty()) {
408420
String v = queue.poll();
409421
stack.push(v);
@@ -414,8 +426,12 @@ private void computeBetweennessCentrality() {
414426

415427
// First visit to w
416428
if (dist.get(w) < 0) {
429+
int nd = dist.get(v) + 1;
430+
dist.put(w, nd);
417431
queue.add(w);
418-
dist.put(w, dist.get(v) + 1);
432+
// Closeness: accumulate distance
433+
sumDist += nd;
434+
reachable++;
419435
}
420436

421437
// Shortest path to w via v?
@@ -426,26 +442,35 @@ private void computeBetweennessCentrality() {
426442
}
427443
}
428444

429-
// Back-propagation of dependencies
430-
Map<String, Double> delta = new HashMap<String, Double>();
431-
for (String t : graph.getVertices()) {
432-
delta.put(t, 0.0);
445+
// --- Closeness for source s ---
446+
if (reachable > 0 && sumDist > 0) {
447+
double cc = ((double) reachable * reachable) / ((n - 1.0) * sumDist);
448+
closenessCentrality.put(s, cc);
433449
}
434450

435-
while (!stack.isEmpty()) {
436-
String w = stack.pop();
437-
for (String v : predecessors.get(w)) {
438-
double contribution = ((double) sigma.get(v) / sigma.get(w)) * (1.0 + delta.get(w));
439-
delta.put(v, delta.get(v) + contribution);
451+
// --- Betweenness back-propagation ---
452+
if (n > 2) {
453+
Map<String, Double> delta = new HashMap<String, Double>();
454+
for (String t : graph.getVertices()) {
455+
delta.put(t, 0.0);
440456
}
441-
if (!w.equals(s)) {
442-
betweennessCentrality.put(w, betweennessCentrality.get(w) + delta.get(w));
457+
458+
while (!stack.isEmpty()) {
459+
String w = stack.pop();
460+
for (String v : predecessors.get(w)) {
461+
double contribution = ((double) sigma.get(v) / sigma.get(w))
462+
* (1.0 + delta.get(w));
463+
delta.put(v, delta.get(v) + contribution);
464+
}
465+
if (!w.equals(s)) {
466+
betweennessCentrality.put(w,
467+
betweennessCentrality.get(w) + delta.get(w));
468+
}
443469
}
444470
}
445471
}
446472

447-
// Normalize for undirected graph: divide by 2 (each pair counted twice)
448-
// and by (n-1)(n-2) to normalize to [0, 1]
473+
// Normalize betweenness for undirected graph: divide by (n-1)(n-2)
449474
double normFactor = (n - 1.0) * (n - 2.0);
450475
if (normFactor > 0) {
451476
for (String node : graph.getVertices()) {
@@ -455,58 +480,6 @@ private void computeBetweennessCentrality() {
455480
}
456481
}
457482

458-
/**
459-
* Closeness centrality: (reachable-1) / sum_of_distances.
460-
* Uses BFS from each node to find shortest path distances.
461-
*
462-
* <p>For disconnected graphs, uses the Wasserman-Faust normalization:
463-
* closeness = (reachable - 1)² / ((V - 1) * sumDist)
464-
* which gives 0 for isolated nodes and properly scales for partial connectivity.</p>
465-
*/
466-
private void computeClosenessCentrality() {
467-
int n = graph.getVertexCount();
468-
469-
for (String s : graph.getVertices()) {
470-
if (n <= 1) {
471-
closenessCentrality.put(s, 0.0);
472-
continue;
473-
}
474-
475-
// BFS to compute distances from s
476-
Map<String, Integer> dist = new HashMap<String, Integer>();
477-
Queue<String> queue = new LinkedList<String>();
478-
dist.put(s, 0);
479-
queue.add(s);
480-
481-
int sumDist = 0;
482-
int reachable = 0;
483-
484-
while (!queue.isEmpty()) {
485-
String current = queue.poll();
486-
int currentDist = dist.get(current);
487-
488-
for (edge e : graph.getIncidentEdges(current)) {
489-
String neighbor = getOtherEnd(e, current);
490-
if (neighbor != null && !dist.containsKey(neighbor)) {
491-
int nd = currentDist + 1;
492-
dist.put(neighbor, nd);
493-
sumDist += nd;
494-
reachable++;
495-
queue.add(neighbor);
496-
}
497-
}
498-
}
499-
500-
if (reachable == 0 || sumDist == 0) {
501-
closenessCentrality.put(s, 0.0);
502-
} else {
503-
// Wasserman-Faust normalization for potentially disconnected graphs
504-
double cc = ((double) reachable * reachable) / ((n - 1.0) * sumDist);
505-
closenessCentrality.put(s, cc);
506-
}
507-
}
508-
}
509-
510483
private String getOtherEnd(edge e, String current) {
511484
String v1 = e.getVertex1();
512485
String v2 = e.getVertex2();

0 commit comments

Comments
 (0)