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

Commit e713f34

Browse files
Merge pull request #103 from sauravbhattacharya001/perf/compressor-jaccard-optimization
perf: optimize byNeighborhoodSimilarity with degree pruning and allocation-free Jaccard
2 parents b7be624 + 1a45928 commit e713f34

1 file changed

Lines changed: 45 additions & 5 deletions

File tree

Gvisual/src/gvisual/GraphCompressor.java

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,13 @@ public CompressionResult byNeighborhoodSimilarity(double threshold) {
106106
neighborSets.put(v, new HashSet<>(graph.getNeighbors(v)));
107107
}
108108

109+
// Sort vertices by degree to improve pruning effectiveness.
110+
// Vertices with similar degrees are more likely to have high Jaccard
111+
// similarity, so sorting brings candidate pairs closer together.
109112
List<String> vertices = new ArrayList<>(graph.getVertices());
113+
vertices.sort((a, b) -> Integer.compare(neighborSets.get(a).size(), neighborSets.get(b).size()));
114+
110115
boolean[] merged = new boolean[vertices.size()];
111-
Map<String, Integer> indexMap = new HashMap<>();
112-
for (int i = 0; i < vertices.size(); i++) {
113-
indexMap.put(vertices.get(i), i);
114-
}
115116

116117
List<List<String>> groups = new ArrayList<>();
117118
for (int i = 0; i < vertices.size(); i++) {
@@ -120,11 +121,25 @@ public CompressionResult byNeighborhoodSimilarity(double threshold) {
120121
group.add(vertices.get(i));
121122
merged[i] = true;
122123
Set<String> refNeighbors = neighborSets.get(vertices.get(i));
124+
int refSize = refNeighbors.size();
123125

124126
for (int j = i + 1; j < vertices.size(); j++) {
125127
if (merged[j]) continue;
126128
Set<String> otherNeighbors = neighborSets.get(vertices.get(j));
127-
double jaccard = jaccardSimilarity(refNeighbors, otherNeighbors);
129+
int otherSize = otherNeighbors.size();
130+
131+
// Degree-based upper bound pruning: the maximum possible
132+
// Jaccard similarity between two sets is min(|A|,|B|)/max(|A|,|B|).
133+
// Since vertices are sorted by degree, refSize <= otherSize.
134+
// If this upper bound < threshold, no later vertex can match either
135+
// (their degrees only increase), so break early.
136+
if (otherSize > 0 && (double) refSize / otherSize < threshold) {
137+
break;
138+
}
139+
140+
// Compute Jaccard without allocating new sets: count intersection
141+
// by iterating the smaller set and checking the larger.
142+
double jaccard = jaccardFast(refNeighbors, refSize, otherNeighbors, otherSize);
128143
if (jaccard >= threshold) {
129144
group.add(vertices.get(j));
130145
merged[j] = true;
@@ -136,6 +151,31 @@ public CompressionResult byNeighborhoodSimilarity(double threshold) {
136151
return buildQuotientGraph(groups, "neighborhood_similarity(threshold=" + threshold + ")");
137152
}
138153

154+
/**
155+
* Computes Jaccard similarity without allocating intermediate HashSets.
156+
* Iterates the smaller set, counting members present in the larger set.
157+
* Union size is derived as |A| + |B| - |intersection|.
158+
*
159+
* @return Jaccard similarity in [0.0, 1.0]
160+
*/
161+
private static double jaccardFast(Set<String> a, int aSize, Set<String> b, int bSize) {
162+
if (aSize == 0 && bSize == 0) return 1.0;
163+
if (aSize == 0 || bSize == 0) return 0.0;
164+
165+
// Iterate the smaller set for fewer hash lookups
166+
Set<String> smaller = aSize <= bSize ? a : b;
167+
Set<String> larger = aSize <= bSize ? b : a;
168+
169+
int intersection = 0;
170+
for (String v : smaller) {
171+
if (larger.contains(v)) {
172+
intersection++;
173+
}
174+
}
175+
int union = aSize + bSize - intersection;
176+
return union == 0 ? 1.0 : (double) intersection / union;
177+
}
178+
139179
/**
140180
* Compresses by degree: nodes are grouped into bins by their degree.
141181
*

0 commit comments

Comments
 (0)