Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
147 lines
5.4 KiB
Java
147 lines
5.4 KiB
Java
package support;
|
|
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.Deque;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* Reference implementations of ModuleHashesBuilder$TopoSorter.visit() — defective and fixed.
|
|
*
|
|
* DEFECT 0003 (java.base/jdk.internal.module): visit() checks
|
|
* Deque.contains(node) to detect whether the current node is on the DFS
|
|
* recursion stack (cycle detection). ArrayDeque.contains() = O(N) linear scan.
|
|
* This code lives in java.base — present in every JDK/JRE.
|
|
*
|
|
* Fix: maintain a HashSet<Node> onStack alongside the Deque.
|
|
* HashSet.contains() = O(1).
|
|
*
|
|
* Exact comparison counts for a linear chain of V nodes (N0→N1→...→N(V-1)):
|
|
* defective: V*(V-1)/2 (visit(N_k) scans k-element stack = k comparisons;
|
|
* sum(k, k=0..V-1) = V*(V-1)/2)
|
|
* fixed: V (1 per visit — one HashSet.contains() call)
|
|
*
|
|
* Derivation of defective count:
|
|
* visit(N0): stack=[], deque.contains(N0) scans 0 elements → 0 comparisons
|
|
* visit(N1): stack=[N0], contains(N1) scans 1 element → 1 comparison
|
|
* visit(N2): stack=[N1,N0], contains(N2) scans 2 elements → 2 comparisons
|
|
* ...
|
|
* visit(N(V-1)): stack is V-1 deep → V-1 comparisons
|
|
* Total: 0+1+2+...+(V-1) = V*(V-1)/2.
|
|
*
|
|
* Growth when V doubles: defective ≈4x (quadratic), fixed ≈2x (linear).
|
|
*/
|
|
public class TopoSorterAlgorithm {
|
|
|
|
public static class Node {
|
|
public final String label;
|
|
public final List<Node> deps = new ArrayList<>();
|
|
public Node(String label) { this.label = label; }
|
|
public void addDep(Node dep) { deps.add(dep); }
|
|
@Override public String toString() { return label; }
|
|
}
|
|
|
|
public static class Result {
|
|
public final List<Node> sorted;
|
|
public final long comparisons;
|
|
Result(List<Node> sorted, long comparisons) {
|
|
this.sorted = sorted;
|
|
this.comparisons = comparisons;
|
|
}
|
|
}
|
|
|
|
// ─── DEFECTIVE: Deque.contains() for on-stack check ─────────────────────
|
|
// Mirrors ModuleHashesBuilder$TopoSorter.visit() — uses Deque.contains(m)
|
|
// to check whether the current node is already on the recursion stack.
|
|
|
|
public static Result topoSortDefective(List<Node> nodes) {
|
|
return new DefectiveSorter().sort(nodes);
|
|
}
|
|
|
|
private static class DefectiveSorter {
|
|
private final Deque<Node> stack = new ArrayDeque<>();
|
|
private final Set<Node> visited = new HashSet<>();
|
|
private final List<Node> sorted = new ArrayList<>();
|
|
private long comparisons = 0;
|
|
|
|
Result sort(List<Node> nodes) {
|
|
for (Node n : nodes) {
|
|
if (!visited.contains(n)) visit(n);
|
|
}
|
|
return new Result(sorted, comparisons);
|
|
}
|
|
|
|
void visit(Node v) {
|
|
// DEFECT: O(N) scan of the recursion stack for cycle detection
|
|
comparisons += dequeContains(stack, v);
|
|
visited.add(v);
|
|
stack.push(v);
|
|
for (Node dep : v.deps) {
|
|
if (!visited.contains(dep)) visit(dep);
|
|
}
|
|
stack.pop();
|
|
sorted.add(0, v);
|
|
}
|
|
|
|
private static long dequeContains(Deque<Node> deque, Node target) {
|
|
long count = 0;
|
|
for (Node n : deque) {
|
|
count++;
|
|
if (n == target) break;
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
// ─── FIXED: HashSet<Node> for on-stack check ─────────────────────────────
|
|
// Maintain a dedicated onStack set. HashSet.contains() = O(1).
|
|
|
|
public static Result topoSortFixed(List<Node> nodes) {
|
|
return new FixedSorter().sort(nodes);
|
|
}
|
|
|
|
private static class FixedSorter {
|
|
private final Set<Node> onStack = new HashSet<>();
|
|
private final Set<Node> visited = new HashSet<>();
|
|
private final List<Node> sorted = new ArrayList<>();
|
|
private long comparisons = 0;
|
|
|
|
Result sort(List<Node> nodes) {
|
|
for (Node n : nodes) {
|
|
if (!visited.contains(n)) visit(n);
|
|
}
|
|
return new Result(sorted, comparisons);
|
|
}
|
|
|
|
void visit(Node v) {
|
|
comparisons++; // O(1) HashSet.contains() for cycle check
|
|
// onStack.contains(v) would be false here (not a cycle); add anyway
|
|
visited.add(v);
|
|
onStack.add(v);
|
|
for (Node dep : v.deps) {
|
|
if (!visited.contains(dep)) visit(dep);
|
|
}
|
|
onStack.remove(v);
|
|
sorted.add(0, v);
|
|
}
|
|
}
|
|
|
|
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Linear chain: N0→N1→...→N(V-1). No cycles — a valid DAG for topo sort.
|
|
* DFS visits nodes in stack-depth order, producing an increasing stack depth
|
|
* at each level.
|
|
*
|
|
* Defective comparisons: V*(V-1)/2
|
|
* Fixed comparisons: V
|
|
*/
|
|
public static List<Node> buildLinearChain(int v) {
|
|
List<Node> nodes = new ArrayList<>();
|
|
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
|
|
for (int i = 0; i < v - 1; i++) nodes.get(i).addDep(nodes.get(i + 1));
|
|
return nodes;
|
|
}
|
|
}
|