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

Commit 10eef10

Browse files
perf: replace HashMap-based BFS with array indexing in Brandes' algorithm
computeBetweennessAndCloseness() previously created 4 new HashMaps per source vertex (predecessors, sigma, dist, delta), each initialized with V entries via put() -- totalling O(V^2) HashMap.put() calls with Integer/Double boxing overhead across all sources. Changes: - Replace dist, sigma, delta Maps with primitive int[]/double[] arrays reset via Arrays.fill() (memset-fast) each iteration - Replace per-source Deque<String> stack with int[] BFS order array traversed in reverse for back-propagation - Replace per-source LinkedList<String> BFS queue with int[] queue - Lazy-allocate predecessor lists only when edges are discovered (not pre-allocated for every vertex) - Pre-build adjacency as List<Integer>[] for cache-friendly traversal instead of calling graph.getIncidentEdges() + getOtherEnd() per step - Accumulate betweenness in double[] array, normalize once at the end Benefits: - Eliminates ~4*V HashMap creations and V^2 boxing operations per compute() call - Better cache locality (contiguous arrays vs scattered Map.Entry nodes) - Reduces GC pressure from short-lived Map.Entry/Integer/Double objects - Same O(V*E) algorithmic complexity, significantly lower constant factor
1 parent b9b8b6d commit 10eef10

1 file changed

Lines changed: 101 additions & 64 deletions

File tree

Gvisual/src/gvisual/NodeCentralityAnalyzer.java

Lines changed: 101 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -374,108 +374,145 @@ private void computeDegreeCentrality() {
374374
/**
375375
* Betweenness + closeness centrality in a single fused BFS pass per source.
376376
*
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>
377+
* <p>Uses array-based storage indexed by vertex ordinal instead of
378+
* per-source HashMaps. This eliminates O(V) HashMap.put() calls per
379+
* source (V sources × V entries each = V² total), avoids Integer/Double
380+
* boxing, and provides better cache locality for the inner BFS loop.
381+
* Predecessor lists are still object-based but are allocated only when
382+
* an edge is discovered (lazy), not pre-allocated for every vertex.</p>
381383
*
382384
* <p>Betweenness: Brandes (2001), O(V·E) for unweighted graphs, normalized
383385
* by (V-1)(V-2) for undirected. Closeness: Wasserman-Faust normalization
384386
* for potentially disconnected graphs.</p>
385387
*/
386388
private void computeBetweennessAndCloseness() {
387-
// Initialize all to 0
389+
int n = graph.getVertexCount();
390+
391+
// Initialize result maps
388392
for (String node : graph.getVertices()) {
389393
betweennessCentrality.put(node, 0.0);
390394
closenessCentrality.put(node, 0.0);
391395
}
392-
393-
int n = graph.getVertexCount();
394396
if (n <= 1) return;
395397

396-
for (String s : graph.getVertices()) {
397-
// --- Single BFS from s, shared by both metrics ---
398-
Deque<String> stack = new ArrayDeque<String>();
399-
Map<String, List<String>> predecessors = new HashMap<String, List<String>>();
400-
Map<String, Integer> sigma = new HashMap<String, Integer>();
401-
Map<String, Integer> dist = new HashMap<String, Integer>();
402-
403-
for (String t : graph.getVertices()) {
404-
predecessors.put(t, new ArrayList<String>());
405-
sigma.put(t, 0);
406-
dist.put(t, -1);
398+
// Build stable vertex-to-index mapping for array-based BFS
399+
List<String> vertexList = new ArrayList<String>(graph.getVertices());
400+
Collections.sort(vertexList);
401+
Map<String, Integer> idxMap = new HashMap<String, Integer>(n * 2);
402+
for (int i = 0; i < n; i++) {
403+
idxMap.put(vertexList.get(i), i);
404+
}
405+
406+
// Pre-build adjacency as index arrays for cache-friendly traversal
407+
@SuppressWarnings("unchecked")
408+
List<Integer>[] adj = new List[n];
409+
for (int i = 0; i < n; i++) {
410+
adj[i] = new ArrayList<Integer>();
411+
}
412+
for (edge e : graph.getEdges()) {
413+
Integer ui = idxMap.get(e.getVertex1());
414+
Integer vi = idxMap.get(e.getVertex2());
415+
if (ui != null && vi != null && !ui.equals(vi)) {
416+
adj[ui].add(vi);
417+
adj[vi].add(ui);
407418
}
419+
}
408420

409-
sigma.put(s, 1);
410-
dist.put(s, 0);
421+
// Accumulator for betweenness (indexed, avoids per-source map lookups)
422+
double[] bcAccum = new double[n];
423+
424+
// Reusable per-source arrays (allocated once, reset each iteration)
425+
int[] dist = new int[n];
426+
int[] sigma = new int[n];
427+
double[] delta = new double[n];
428+
int[] bfsOrder = new int[n]; // replaces Deque<String> stack
429+
430+
@SuppressWarnings("unchecked")
431+
List<Integer>[] preds = new List[n];
432+
433+
for (int s = 0; s < n; s++) {
434+
// Reset arrays for this source (Arrays.fill is memset-fast)
435+
Arrays.fill(dist, -1);
436+
Arrays.fill(sigma, 0);
437+
Arrays.fill(delta, 0.0);
438+
for (int i = 0; i < n; i++) {
439+
preds[i] = null; // lazy allocation
440+
}
411441

412-
Queue<String> queue = new LinkedList<String>();
413-
queue.add(s);
442+
dist[s] = 0;
443+
sigma[s] = 1;
444+
int bfsHead = 0, bfsTail = 0;
445+
bfsOrder[bfsTail++] = s;
414446

415-
// Closeness accumulators for this source
447+
// Closeness accumulators
416448
int sumDist = 0;
417449
int reachable = 0;
418450

419-
while (!queue.isEmpty()) {
420-
String v = queue.poll();
421-
stack.push(v);
422-
423-
for (edge e : graph.getIncidentEdges(v)) {
424-
String w = getOtherEnd(e, v);
425-
if (w == null) continue;
426-
427-
// First visit to w
428-
if (dist.get(w) < 0) {
429-
int nd = dist.get(v) + 1;
430-
dist.put(w, nd);
431-
queue.add(w);
432-
// Closeness: accumulate distance
433-
sumDist += nd;
451+
// BFS using array-based queue (bfsOrder doubles as stack in reverse)
452+
int qHead = 0;
453+
int[] bfsQueue = bfsOrder; // reuse same array
454+
// Actually we need a separate queue since bfsOrder is our stack
455+
// But we can use bfsOrder as both: BFS fills left-to-right,
456+
// back-propagation reads right-to-left (same as stack pop order)
457+
int orderIdx = 0;
458+
459+
// Simple array-based BFS queue
460+
int[] queue = new int[n];
461+
int qStart = 0, qEnd = 0;
462+
queue[qEnd++] = s;
463+
464+
while (qStart < qEnd) {
465+
int v = queue[qStart++];
466+
bfsOrder[orderIdx++] = v;
467+
468+
for (int w : adj[v]) {
469+
if (dist[w] < 0) {
470+
dist[w] = dist[v] + 1;
471+
queue[qEnd++] = w;
472+
sumDist += dist[w];
434473
reachable++;
435474
}
436-
437-
// Shortest path to w via v?
438-
if (dist.get(w) == dist.get(v) + 1) {
439-
sigma.put(w, sigma.get(w) + sigma.get(v));
440-
predecessors.get(w).add(v);
475+
if (dist[w] == dist[v] + 1) {
476+
sigma[w] += sigma[v];
477+
if (preds[w] == null) {
478+
preds[w] = new ArrayList<Integer>(4);
479+
}
480+
preds[w].add(v);
441481
}
442482
}
443483
}
444484

445-
// --- Closeness for source s ---
485+
// Closeness for source s
446486
if (reachable > 0 && sumDist > 0) {
447487
double cc = ((double) reachable * reachable) / ((n - 1.0) * sumDist);
448-
closenessCentrality.put(s, cc);
488+
closenessCentrality.put(vertexList.get(s), cc);
449489
}
450490

451-
// --- Betweenness back-propagation ---
491+
// Betweenness back-propagation (traverse BFS order in reverse)
452492
if (n > 2) {
453-
Map<String, Double> delta = new HashMap<String, Double>();
454-
for (String t : graph.getVertices()) {
455-
delta.put(t, 0.0);
456-
}
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));
493+
for (int idx = orderIdx - 1; idx >= 1; idx--) {
494+
int w = bfsOrder[idx];
495+
if (preds[w] != null) {
496+
for (int v : preds[w]) {
497+
double contribution = ((double) sigma[v] / sigma[w])
498+
* (1.0 + delta[w]);
499+
delta[v] += contribution;
500+
}
468501
}
502+
bcAccum[w] += delta[w];
469503
}
470504
}
471505
}
472506

473507
// Normalize betweenness for undirected graph: divide by (n-1)(n-2)
474508
double normFactor = (n - 1.0) * (n - 2.0);
475509
if (normFactor > 0) {
476-
for (String node : graph.getVertices()) {
477-
double raw = betweennessCentrality.get(node);
478-
betweennessCentrality.put(node, raw / normFactor);
510+
for (int i = 0; i < n; i++) {
511+
betweennessCentrality.put(vertexList.get(i), bcAccum[i] / normFactor);
512+
}
513+
} else {
514+
for (int i = 0; i < n; i++) {
515+
betweennessCentrality.put(vertexList.get(i), bcAccum[i]);
479516
}
480517
}
481518
}

0 commit comments

Comments
 (0)