@@ -44,6 +44,7 @@ public class KCoreDecomposition {
4444 private Map <String , Integer > coreness ;
4545 private int degeneracy ;
4646 private boolean computed ;
47+ private SortedMap <Integer , List <String >> cachedShells ; // cached to avoid recomputation
4748
4849 /**
4950 * Creates a new KCoreDecomposition for the given graph.
@@ -59,6 +60,7 @@ public KCoreDecomposition(Graph<String, Edge> graph) {
5960 this .coreness = new LinkedHashMap <String , Integer >();
6061 this .degeneracy = 0 ;
6162 this .computed = false ;
63+ this .cachedShells = null ;
6264 }
6365
6466 // ── Core decomposition (Batagelj–Zaversnik) ────────────────────
@@ -208,6 +210,7 @@ public int getDegeneracy() {
208210 */
209211 public SortedMap <Integer , List <String >> getCoreShells () {
210212 ensureComputed ();
213+ if (cachedShells != null ) return cachedShells ;
211214 SortedMap <Integer , List <String >> shells = new TreeMap <Integer , List <String >>();
212215 for (Map .Entry <String , Integer > entry : coreness .entrySet ()) {
213216 int k = entry .getValue ();
@@ -220,6 +223,7 @@ public SortedMap<Integer, List<String>> getCoreShells() {
220223 for (List <String > shell : shells .values ()) {
221224 Collections .sort (shell );
222225 }
226+ cachedShells = shells ;
223227 return shells ;
224228 }
225229
@@ -299,35 +303,62 @@ public String toString() {
299303 * The density should increase as k grows, since higher cores are
300304 * progressively denser subgraphs.
301305 *
306+ * <p>Uses an incremental approach: starts with all vertices (0-core)
307+ * and removes vertices shell-by-shell from lowest to highest k,
308+ * subtracting edges incident to removed vertices. This gives O(V + E)
309+ * total work instead of the previous O(degeneracy × E) approach that
310+ * re-scanned all edges per core level.</p>
311+ *
302312 * @return list of CoreDensity objects, sorted by k ascending
303313 */
304314 public List <CoreDensity > getCoreDensityProfile () {
305315 ensureComputed ();
306316 List <CoreDensity > profile = new ArrayList <CoreDensity >();
307317
308- for (int k = 0 ; k <= degeneracy ; k ++) {
309- Set <String > coreVertices = new HashSet <String >();
310- for (Map .Entry <String , Integer > entry : coreness .entrySet ()) {
311- if (entry .getValue () >= k ) {
312- coreVertices .add (entry .getKey ());
313- }
314- }
318+ if (coreness .isEmpty ()) return profile ;
315319
316- if (coreVertices .isEmpty ()) continue ;
320+ // Start with all vertices in the 0-core
321+ Set <String > coreVertices = new HashSet <String >(coreness .keySet ());
317322
318- // Count edges within the k-core
319- int edgeCount = 0 ;
320- for (Edge e : graph .getEdges ()) {
321- String v1 = graph .getEndpoints (e ).getFirst ();
322- String v2 = graph .getEndpoints (e ).getSecond ();
323- if (coreVertices .contains (v1 ) && coreVertices .contains (v2 )) {
324- edgeCount ++;
325- }
326- }
323+ // Count all edges once (the 0-core edge count)
324+ int edgeCount = graph .getEdgeCount ();
325+
326+ SortedMap <Integer , List <String >> shells = getCoreShells ();
327+
328+ for (int k = 0 ; k <= degeneracy ; k ++) {
329+ if (coreVertices .isEmpty ()) break ;
327330
328331 int v = coreVertices .size ();
329- double density = v > 1 ? (2.0 * edgeCount ) / (v * (v - 1 )) : 0.0 ;
332+ double density = v > 1 ? (2.0 * edgeCount ) / (( long ) v * (v - 1 )) : 0.0 ;
330333 profile .add (new CoreDensity (k , v , edgeCount , density ));
334+
335+ // Remove the k-shell vertices (coreness == k) to get the (k+1)-core.
336+ // Count edges to subtract BEFORE removing vertices from the set.
337+ List <String > shell = shells .get (k );
338+ if (shell != null ) {
339+ Set <String > shellSet = new HashSet <String >(shell );
340+
341+ // Count edges from shell vertices to the rest of coreVertices.
342+ // Each edge is counted once: either to a higher-core neighbor,
343+ // or between two shell vertices (using compareTo tie-break).
344+ for (String removed : shell ) {
345+ for (String neighbor : GraphUtils .neighborsOf (graph , removed )) {
346+ if (!coreVertices .contains (neighbor )) continue ;
347+ if (shellSet .contains (neighbor )) {
348+ // Intra-shell edge: count once using vertex ordering
349+ if (removed .compareTo (neighbor ) < 0 ) {
350+ edgeCount --;
351+ }
352+ } else {
353+ // Edge to a vertex staying in the (k+1)-core
354+ edgeCount --;
355+ }
356+ }
357+ }
358+
359+ // Now remove all shell vertices
360+ coreVertices .removeAll (shellSet );
361+ }
331362 }
332363
333364 return profile ;
0 commit comments