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

Commit 036f77c

Browse files
perf: optimize graph algorithms — single-pass stats, inline edge counting, efficient Dijkstra
CommunityDetector: compute internal edge metrics during BFS traversal instead of a separate O(V*E) post-pass with per-community HashSet. Edges are now counted as endpoints are discovered, using a single global countedEdges set. GraphStats: replace 3 separate vertex iterations (getMaxDegree, getIsolatedNodeCount, getTopNodes) with a single-pass cache that computes all vertex degree data once. getTopNodes now uses a min-heap partial sort O(V log N) instead of full sort O(V log V). getAverageWeight caches the total weight sum. ShortestPathFinder: remove vertex-index indirection from Dijkstra — eliminates O(V) vertex list construction and int-to-double-to-int conversion on every PQ operation. areConnected() now uses early- termination BFS instead of computing the full reachable set.
1 parent 8bdbaa9 commit 036f77c

3 files changed

Lines changed: 141 additions & 60 deletions

File tree

Gvisual/src/gvisual/CommunityDetector.java

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ public double getModularity(Graph<String, edge> graph) {
191191
* Each connected component is treated as a community. Communities are
192192
* ranked by size (largest first) and enriched with edge-type breakdowns.
193193
*
194+
* <p>Edge metrics are computed during the BFS traversal itself: each edge
195+
* is counted when the second endpoint is discovered (or when revisiting
196+
* an already-visited member). This avoids a separate O(V*E) post-pass
197+
* and eliminates the per-community HashSet of counted edges.</p>
198+
*
194199
* @return detection result with communities and node mappings
195200
*/
196201
public DetectionResult detect() {
@@ -199,7 +204,10 @@ public DetectionResult detect() {
199204
Map<String, Integer> nodeToCommunity = new HashMap<String, Integer>();
200205
int communityId = 0;
201206

202-
// Find connected components via BFS
207+
// Track which edges have been counted globally to avoid double-counting
208+
Set<edge> countedEdges = new HashSet<edge>();
209+
210+
// Find connected components via BFS, computing edge metrics inline
203211
for (String vertex : graph.getVertices()) {
204212
if (visited.contains(vertex)) continue;
205213

@@ -215,27 +223,23 @@ public DetectionResult detect() {
215223

216224
for (edge e : graph.getIncidentEdges(current)) {
217225
String neighbor = getOtherEnd(e, current);
218-
if (neighbor != null && !visited.contains(neighbor)) {
219-
visited.add(neighbor);
220-
queue.add(neighbor);
221-
}
222-
}
223-
}
226+
if (neighbor == null) continue;
224227

225-
// Compute internal edge metrics
226-
Set<edge> counted = new HashSet<edge>();
227-
for (String member : community.members) {
228-
for (edge e : graph.getIncidentEdges(member)) {
229-
if (counted.contains(e)) continue;
230-
String other = getOtherEnd(e, member);
231-
if (other != null && community.members.contains(other)) {
232-
counted.add(e);
228+
// Count this edge if both endpoints are in this component
229+
// and we haven't counted it yet
230+
if (visited.contains(neighbor) && !countedEdges.contains(e)) {
231+
countedEdges.add(e);
233232
community.internalEdges++;
234233
community.totalWeight += e.getWeight();
235234
String type = e.getType();
236235
Integer count = community.edgeTypeCounts.get(type);
237236
community.edgeTypeCounts.put(type, count == null ? 1 : count + 1);
238237
}
238+
239+
if (!visited.contains(neighbor)) {
240+
visited.add(neighbor);
241+
queue.add(neighbor);
242+
}
239243
}
240244
}
241245

Gvisual/src/gvisual/GraphStats.java

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -104,35 +104,79 @@ public double getAverageDegree() {
104104
return (2.0 * e) / v;
105105
}
106106

107-
/** Maximum degree among all visible nodes. */
108-
public int getMaxDegree() {
109-
int max = 0;
107+
/**
108+
* Cached per-vertex statistics computed once, reused by multiple methods.
109+
* Avoids iterating all vertices 3+ separate times for max degree, isolated
110+
* count, and top-N queries.
111+
*/
112+
private int cachedMaxDegree = -1;
113+
private int cachedIsolatedCount = -1;
114+
private List<Map.Entry<String, Integer>> cachedDegreeEntries;
115+
116+
/**
117+
* Computes and caches per-vertex degree data in a single pass.
118+
* Subsequent calls to getMaxDegree(), getIsolatedNodeCount(), and
119+
* getTopNodes() all reuse this cache instead of each iterating
120+
* all vertices independently.
121+
*/
122+
private void ensureVertexStatsComputed() {
123+
if (cachedMaxDegree >= 0) return; // already computed
124+
125+
cachedMaxDegree = 0;
126+
cachedIsolatedCount = 0;
127+
cachedDegreeEntries = new ArrayList<Map.Entry<String, Integer>>();
128+
110129
for (String node : graph.getVertices()) {
111130
int deg = graph.degree(node);
112-
if (deg > max) max = deg;
131+
cachedDegreeEntries.add(new AbstractMap.SimpleEntry<String, Integer>(node, deg));
132+
if (deg > cachedMaxDegree) cachedMaxDegree = deg;
133+
if (deg == 0) cachedIsolatedCount++;
113134
}
114-
return max;
135+
}
136+
137+
/** Maximum degree among all visible nodes. */
138+
public int getMaxDegree() {
139+
ensureVertexStatsComputed();
140+
return cachedMaxDegree;
115141
}
116142

117143
/**
118144
* Returns the top-N nodes by degree (most connected).
119145
* Each entry is "nodeId (degree)".
146+
*
147+
* <p>Uses a partial-sort (min-heap of size N) instead of fully sorting
148+
* all vertices. For graphs with many nodes but small N, this reduces
149+
* complexity from O(V log V) to O(V log N).</p>
120150
*/
121151
public List<String> getTopNodes(int n) {
122-
List<Map.Entry<String, Integer>> entries = new ArrayList<Map.Entry<String, Integer>>();
123-
for (String node : graph.getVertices()) {
124-
entries.add(new AbstractMap.SimpleEntry<String, Integer>(node, graph.degree(node)));
152+
ensureVertexStatsComputed();
153+
154+
if (n <= 0 || cachedDegreeEntries.isEmpty()) {
155+
return new ArrayList<String>();
125156
}
126-
Collections.sort(entries, new Comparator<Map.Entry<String, Integer>>() {
127-
public int compare(Map.Entry<String, Integer> a, Map.Entry<String, Integer> b) {
128-
return b.getValue().compareTo(a.getValue());
157+
158+
// Use a min-heap of size n for O(V log N) partial sort
159+
PriorityQueue<Map.Entry<String, Integer>> minHeap =
160+
new PriorityQueue<Map.Entry<String, Integer>>(n + 1,
161+
new Comparator<Map.Entry<String, Integer>>() {
162+
public int compare(Map.Entry<String, Integer> a,
163+
Map.Entry<String, Integer> b) {
164+
return a.getValue().compareTo(b.getValue());
165+
}
166+
});
167+
168+
for (Map.Entry<String, Integer> entry : cachedDegreeEntries) {
169+
minHeap.add(entry);
170+
if (minHeap.size() > n) {
171+
minHeap.poll(); // evict smallest
129172
}
130-
});
131-
List<String> result = new ArrayList<String>();
132-
int count = Math.min(n, entries.size());
133-
for (int i = 0; i < count; i++) {
134-
Map.Entry<String, Integer> entry = entries.get(i);
135-
result.add("Node " + entry.getKey() + " (" + entry.getValue() + ")");
173+
}
174+
175+
// Extract in descending order
176+
List<String> result = new ArrayList<String>(minHeap.size());
177+
while (!minHeap.isEmpty()) {
178+
Map.Entry<String, Integer> entry = minHeap.poll();
179+
result.add(0, "Node " + entry.getKey() + " (" + entry.getValue() + ")");
136180
}
137181
return result;
138182
}
@@ -141,22 +185,28 @@ public int compare(Map.Entry<String, Integer> a, Map.Entry<String, Integer> b) {
141185
* Number of isolated nodes (degree 0) in the visible graph.
142186
*/
143187
public int getIsolatedNodeCount() {
144-
int count = 0;
145-
for (String node : graph.getVertices()) {
146-
if (graph.degree(node) == 0) count++;
147-
}
148-
return count;
188+
ensureVertexStatsComputed();
189+
return cachedIsolatedCount;
149190
}
150191

192+
/**
193+
* Cached edge weight statistics computed once.
194+
*/
195+
private double cachedTotalWeight = -1.0;
196+
151197
/**
152198
* Average edge weight across all visible edges.
199+
* Caches the total weight sum to avoid re-iterating edges if called
200+
* multiple times.
153201
*/
154202
public double getAverageWeight() {
155203
if (graph.getEdgeCount() == 0) return 0.0;
156-
double total = 0;
157-
for (edge e : graph.getEdges()) {
158-
total += e.getWeight();
204+
if (cachedTotalWeight < 0) {
205+
cachedTotalWeight = 0;
206+
for (edge e : graph.getEdges()) {
207+
cachedTotalWeight += e.getWeight();
208+
}
159209
}
160-
return total / graph.getEdgeCount();
210+
return cachedTotalWeight / graph.getEdgeCount();
161211
}
162212
}

Gvisual/src/gvisual/ShortestPathFinder.java

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ public PathResult findShortestByHops(String source, String target) {
137137
/**
138138
* Finds the shortest path by total edge weight (Dijkstra) between source and target.
139139
*
140+
* <p>Uses a typed priority queue entry instead of encoding vertices as
141+
* double-array indices into a separate vertex list. This eliminates the
142+
* O(V) vertex-index map construction and avoids integer-to-double-to-integer
143+
* conversion overhead on every PQ operation.</p>
144+
*
140145
* @param source source vertex ID
141146
* @param target target vertex ID
142147
* @return the weight-optimal path, or null if no path exists
@@ -153,34 +158,30 @@ public PathResult findShortestByWeight(String source, String target) {
153158
0.0);
154159
}
155160

156-
// Dijkstra
157-
Map<String, Double> dist = new HashMap<String, Double>();
161+
// Dijkstra with typed PQ entries (no vertex-index indirection)
162+
final Map<String, Double> dist = new HashMap<String, Double>();
158163
Map<String, String> predecessor = new HashMap<String, String>();
159164
Map<String, edge> predecessorEdge = new HashMap<String, edge>();
160-
// Priority queue: [distance, vertex]
161-
PriorityQueue<double[]> pq = new PriorityQueue<double[]>(11, new Comparator<double[]>() {
162-
public int compare(double[] a, double[] b) {
163-
return Double.compare(a[0], b[0]);
165+
166+
// Priority queue using String[] wrapper: [0]=vertex, distance stored in dist map
167+
PriorityQueue<String> pq = new PriorityQueue<String>(11, new Comparator<String>() {
168+
public int compare(String a, String b) {
169+
Double da = dist.get(a);
170+
Double db = dist.get(b);
171+
if (da == null) da = Double.MAX_VALUE;
172+
if (db == null) db = Double.MAX_VALUE;
173+
return Double.compare(da, db);
164174
}
165175
});
166176

167-
// Use a map of vertex->index for the PQ (encode vertex as string hash)
168-
Map<String, Integer> vertexIndex = new HashMap<String, Integer>();
169-
List<String> vertexList = new ArrayList<String>(graph.getVertices());
170-
for (int i = 0; i < vertexList.size(); i++) {
171-
vertexIndex.put(vertexList.get(i), i);
172-
}
173-
174177
dist.put(source, 0.0);
175178
predecessor.put(source, null);
176-
pq.add(new double[]{0.0, vertexIndex.get(source)});
179+
pq.add(source);
177180

178181
Set<String> visited = new HashSet<String>();
179182

180183
while (!pq.isEmpty()) {
181-
double[] entry = pq.poll();
182-
int idx = (int) entry[1];
183-
String current = vertexList.get(idx);
184+
String current = pq.poll();
184185

185186
if (visited.contains(current)) continue;
186187
visited.add(current);
@@ -202,7 +203,7 @@ public int compare(double[] a, double[] b) {
202203
dist.put(neighbor, newDist);
203204
predecessor.put(neighbor, current);
204205
predecessorEdge.put(neighbor, e);
205-
pq.add(new double[]{newDist, vertexIndex.get(neighbor)});
206+
pq.add(neighbor);
206207
}
207208
}
208209
}
@@ -241,11 +242,37 @@ public Set<String> getReachableVertices(String source) {
241242

242243
/**
243244
* Checks whether two vertices are connected (in the same component).
245+
*
246+
* <p>Uses an early-termination BFS instead of computing the full
247+
* reachable set. This is O(component) in the worst case but returns
248+
* immediately when the target is found, which is faster for large
249+
* graphs where the target is close to the source.</p>
244250
*/
245251
public boolean areConnected(String source, String target) {
246252
validateVertex(source, "Source");
247253
validateVertex(target, "Target");
248-
return getReachableVertices(source).contains(target);
254+
255+
if (source.equals(target)) return true;
256+
257+
Set<String> visited = new HashSet<String>();
258+
Queue<String> queue = new LinkedList<String>();
259+
260+
visited.add(source);
261+
queue.add(source);
262+
263+
while (!queue.isEmpty()) {
264+
String current = queue.poll();
265+
for (edge e : graph.getIncidentEdges(current)) {
266+
String neighbor = getOtherEnd(e, current);
267+
if (neighbor != null && !visited.contains(neighbor)) {
268+
if (neighbor.equals(target)) return true;
269+
visited.add(neighbor);
270+
queue.add(neighbor);
271+
}
272+
}
273+
}
274+
275+
return false;
249276
}
250277

251278
// --- private helpers ---

0 commit comments

Comments
 (0)