To run all tests:
dotnet testTo run tests for a specific project:
dotnet test tests/SSSPP.Graph.Tests/SSSPP.Graph.Tests.csprojTo run the performance benchmarks:
dotnet run -c Release --project benchmarks/SSSPP.Graph.Benchmark/SSSPP.Graph.Benchmark.csprojThe benchmark results will be saved in the BenchmarkDotNet.Artifacts/results/ directory.
Note: Benchmarks must be run in Release mode (-c Release) for accurate performance measurements.
Description: Each vertex maintains a list of all adjacent (connected) vertices. Often implemented as an array (or map) of lists.
Advantages:
- Efficient for sparse graphs (few edges).
- Easy to iterate over neighbors of a vertex.
- Space-saving when the number of edges is much less than ( V^2 ).
Disadvantages:
- Checking if two vertices are connected can take O(deg(V)) time (not constant).
- Slightly more complex structure to implement.
Memory Complexity:
- O(V + E) where ( V ) is number of vertices and ( E ) is number of edges.
Description:
A 2D array (matrix) of size ( V \times V ) where entry ([i][j]) is 1 (or weight) if there is an edge from vertex ( i ) to ( j ), otherwise 0.
Advantages:
- Constant-time O(1) edge lookup and update.
- Good for dense graphs (many edges).
- Easy representation for matrix-based algorithms (e.g., Floyd–Warshall).
Disadvantages:
- Consumes a lot of memory for sparse graphs.
- Iterating over all neighbors of a vertex takes O(V) time.
Memory Complexity:
- O(V²) regardless of the number of edges.
Description: A list of all edges, each represented as a pair (or triplet for weighted graphs) ( (u, v [, w]) ).
Advantages:
- Simplest representation.
- Very memory efficient if only edge relationships matter.
- Convenient for algorithms like Kruskal’s (which need edge sorting).
Disadvantages:
- Finding neighbors or checking adjacency is O(E).
- Not efficient for traversal-based algorithms (e.g., BFS, DFS).
Memory Complexity:
- O(E).
| Representation | Edge Lookup | Neighbor Iteration | Best For | Space Complexity |
|---|---|---|---|---|
| Adjacency List | O(deg(V)) | O(deg(V)) | Sparse graphs | O(V + E) |
| Adjacency Matrix | O(1) | O(V) | Dense graphs | O(V²) |
| Edge List | O(E) | O(E) | Simple storage | O(E) |
Both approaches defined in ShortestPath
Standard libraries omit decreaseKey because:
- Complex to implement efficiently - requires auxiliary data structures
- The workaround is good enough - inserting duplicates performs well in practice
- Simplicity over specialization - general-purpose design wins
Classical Dijkstra uses decreaseKey, but modern implementations use "lazy deletion" (duplicate insertions) instead.
| Scenario | DecreaseKey | Lazy Deletion |
|---|---|---|
| Sparse graphs | O((V+E) log V) | O((V+E) log E) ≈ O((V+E) log V) |
| Dense graphs | Better theory | Competitive practice |
| Cache locality | Worse | Better |
| Code simplicity | Complex | Simple |
| Real-world speed | Slower | Often faster |
As an alternative to the binary heap, I've implemented Dijkstra's algorithm using a Bucket Queue (also known as Dial's Algorithm).
Binary Heap (PriorityQueue):
- Time Complexity: O((V + E) log V)
- Space Complexity: O(V)
- Works with any numeric weights
- General-purpose solution
Bucket Queue:
- Time Complexity: O(V + E + W) where W is the maximum distance
- Space Complexity: O(V + W)
- Optimized for small integer weights
- Can achieve O(V + E) when W = O(V)
