package unit; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; /** * neo4j-0001: Dijkstra predecessors List.contains() — O(EƗP) vs O(E) * * Simulates the predecessor deduplication guard from Dijkstra.java:324. * The defective path uses List (ArrayList.contains = O(N)). * The fixed path uses Set (HashSet.contains = O(1)). * * Each "relationship" is a unique String object. * We measure how many equality comparisons .contains() triggers * by wrapping items in a counted comparator object. */ public class Neo4jTest { static int slowContainsOps = 0; static int fastContainsOps = 0; static class CountedRel { final int id; CountedRel(int id) { this.id = id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof CountedRel)) return false; // Count each comparison slowContainsOps++; return this.id == ((CountedRel) o).id; } @Override public int hashCode() { return id; } } static class CountedRelFast { final int id; CountedRelFast(int id) { this.id = id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof CountedRelFast)) return false; fastContainsOps++; return this.id == ((CountedRelFast) o).id; } @Override public int hashCode() { return id; // proper hash — Set.contains() can short-circuit } } /** * Defective path: List predecessors. * For N predecessors, each contains() call costs O(N) in the worst case * (item not present triggers full scan). */ static void slowPath(int n) { Map> predecessors = new HashMap<>(); // Simulate adding n unique relationships to the predecessor list for node 0 predecessors.put(0, new ArrayList<>()); for (int i = 0; i < n; i++) { CountedRel rel = new CountedRel(i); List preds = predecessors.get(0); // This is the defective guard: O(P) scan per call if (!preds.contains(rel)) { preds.add(rel); } } // Now simulate a second pass: trying to add the same rels again (duplicates) // Each contains() now scans all n existing entries for (int i = 0; i < n; i++) { CountedRel rel = new CountedRel(i); List preds = predecessors.get(0); if (!preds.contains(rel)) { preds.add(rel); } } } /** * Fixed path: Set predecessors. * HashSet.contains() is O(1) — hash lookup, equals only called on collision. */ static void fastPath(int n) { Map> predecessors = new HashMap<>(); predecessors.put(0, new LinkedHashSet<>()); for (int i = 0; i < n; i++) { CountedRelFast rel = new CountedRelFast(i); Set preds = predecessors.get(0); preds.add(rel); // Set.add() deduplicates; no explicit contains() needed } // Duplicate pass: set semantics are idempotent for (int i = 0; i < n; i++) { CountedRelFast rel = new CountedRelFast(i); predecessors.get(0).add(rel); } } public static void main(String[] args) { int N = 500; int PASSES = 3; // Warm up slowPath(10); fastPath(10); slowContainsOps = 0; fastContainsOps = 0; // Measure for (int p = 0; p < PASSES; p++) { slowPath(N); fastPath(N); } // For the slow path: first pass adds N items with 0..N-1 scans = N*(N-1)/2 ops // Second pass finds all N items present: each scan goes full N = N*N ops // Total per call: ~N^2/2 + N^2 = ~1.5*N^2 comparisons // For fast path: hash collisions are rare; ideally ~0 equals() calls for unique ids long expectedSlowMin = (long)(N * N / 4); // conservative lower bound per pass long expectedFastMax = (long)(N * PASSES * 2); // generous upper bound System.out.println("N=" + N + " PASSES=" + PASSES); System.out.println("slow contains ops : " + slowContainsOps + " (expected >=" + expectedSlowMin + ")"); System.out.println("fast contains ops : " + fastContainsOps + " (expected <=" + expectedFastMax + ")"); int passed = 0; int total = 0; total++; if (slowContainsOps >= expectedSlowMin) { System.out.println("PASS 1/" + total + ": slow path O(n^2) confirmed (ops=" + slowContainsOps + " >= " + expectedSlowMin + ")"); passed++; } else { System.out.println("FAIL 1/" + total + ": slow path did not show expected O(n^2) ops"); } total++; if (fastContainsOps <= expectedFastMax) { System.out.println("PASS 2/" + total + ": fast path O(1) confirmed (ops=" + fastContainsOps + " <= " + expectedFastMax + ")"); passed++; } else { System.out.println("FAIL 2/" + total + ": fast path showed too many comparisons: " + fastContainsOps); } total++; // Ratio must be at least 10x boolean ratioOk = slowContainsOps >= fastContainsOps * 10; if (fastContainsOps == 0 || ratioOk) { System.out.println("PASS 3/" + total + ": ratio slow/fast is " + (fastContainsOps == 0 ? "inf" : (slowContainsOps / fastContainsOps)) + "x (expected >=10x)"); passed++; } else { System.out.println("FAIL 3/" + total + ": ratio too small: slow=" + slowContainsOps + " fast=" + fastContainsOps); } System.out.println(passed + "/" + total + " PASS"); if (passed != total) System.exit(1); } }