@@ -13,14 +13,14 @@
|
|||||||
- [x] Binary Search Tree
|
- [x] Binary Search Tree
|
||||||
- [x] AVL Tree
|
- [x] AVL Tree
|
||||||
- [x] Heap (min/max)
|
- [x] Heap (min/max)
|
||||||
- - [x] Priority Queue
|
|
||||||
- [ ] Trie
|
- [ ] Trie
|
||||||
|
|
||||||
## Graph
|
## Graph
|
||||||
|
|
||||||
- [x] Unweighted
|
- [x] Unweighted
|
||||||
- [x] Weighted
|
- [x] Weighted
|
||||||
- [ ] Directed
|
- [x] Directed
|
||||||
|
- [x] DAG
|
||||||
|
|
||||||
## Sets
|
## Sets
|
||||||
|
|
||||||
@@ -33,25 +33,35 @@
|
|||||||
|
|
||||||
# Algorithms ωψγ
|
# Algorithms ωψγ
|
||||||
|
|
||||||
|
## Etc
|
||||||
|
|
||||||
- [x] Kadane's Algorithm
|
- [x] Kadane's Algorithm
|
||||||
- [x] Euclidean GCD
|
- [x] Euclidean GCD
|
||||||
- [x] Fibonacci
|
- [x] Fibonacci
|
||||||
- [x] Heap Sort
|
- [x] Miller Rabin prime test
|
||||||
- [ ] Modular Arithmetic
|
- [ ] Modular Arithmetic
|
||||||
- [ ] Sieve of Eratosthenes
|
- [ ] Sieve of Eratosthenes
|
||||||
|
|
||||||
|
## Heaps & Trees
|
||||||
|
|
||||||
|
- [x] Priority Queue
|
||||||
|
- [x] Heap Sort
|
||||||
- [x] BFS Breadth-First Search
|
- [x] BFS Breadth-First Search
|
||||||
- [ ] DFS Depth-First Search
|
- [ ] DFS Depth-First Search
|
||||||
- [x] Miller Rabin prime test
|
|
||||||
|
## Graphs
|
||||||
|
|
||||||
|
- [ ] Topological Sort with Kahn's algorithm
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
> linear Structures
|
> Linear Structures
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go doc -all ./linear | bat -l go
|
go doc -all ./linear | bat -l go
|
||||||
```
|
```
|
||||||
|
|
||||||
> algorithms
|
> Algorithms
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go doc -all ./algo | bat -l go
|
go doc -all ./algo | bat -l go
|
||||||
@@ -63,7 +73,7 @@ go doc -all ./algo | bat -l go
|
|||||||
go doc -all ./sets/ | bat -l go
|
go doc -all ./sets/ | bat -l go
|
||||||
```
|
```
|
||||||
|
|
||||||
> trees
|
> Trees
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
for p in ./trees/ ./trees/avl ./trees/heap; do go doc -all "$p"; done | bat -l go
|
for p in ./trees/ ./trees/avl ./trees/heap; do go doc -all "$p"; done | bat -l go
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Directed Acyclic Graph (DAG) — How the Cycle Check Works
|
||||||
|
|
||||||
|
A **directed graph** stores one-way edges: `a -> b` means you can go from
|
||||||
|
`a` to `b`, but not necessarily back. In the adjacency list each edge is
|
||||||
|
stored **once**, on the source vertex's out-list:
|
||||||
|
|
||||||
|
```
|
||||||
|
a -> b
|
||||||
|
b -> c
|
||||||
|
|
||||||
|
adjList:
|
||||||
|
a: [b]
|
||||||
|
b: [c]
|
||||||
|
c: []
|
||||||
|
```
|
||||||
|
|
||||||
|
(Contrast with the undirected `Graph`, where `a -> b` is stored twice — in
|
||||||
|
both `a`'s and `b`'s lists — which is why its `Size()` divides by two and the
|
||||||
|
directed `Size()` does not.)
|
||||||
|
|
||||||
|
A **DAG** adds one rule: **no cycles**. You can never follow the arrows and
|
||||||
|
end up back where you started. `a -> b -> c -> a` is forbidden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Where the invariant is enforced
|
||||||
|
|
||||||
|
There is exactly one guard, inside `AddVertex(from, to)`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// from -> to closes a cycle iff "to" can already reach "from".
|
||||||
|
if gp.hasPath(to, from) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The reasoning is short but important:
|
||||||
|
|
||||||
|
> The graph is acyclic **before** every `AddVertex` call — it starts empty,
|
||||||
|
> and this guard has held every previous time. Given that, the *only* way the
|
||||||
|
> new edge `from -> to` can create a cycle is if a path `to -> ... -> from`
|
||||||
|
> already exists. The new edge would close that path into a loop.
|
||||||
|
|
||||||
|
So the question "would this edge create a cycle?" reduces to "can `to` already
|
||||||
|
reach `from`?" — and that is a reachability query, answered by DFS.
|
||||||
|
|
||||||
|
Note the argument order: `hasPath(to, from)`, **not** `hasPath(from, to)`. You
|
||||||
|
search *forward from `to`* trying to arrive back at `from`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. What DFS actually does
|
||||||
|
|
||||||
|
`hasPath` just seeds a fresh `visited` set and calls `dfs`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (gp *DAG[T]) dfs(current, dst T, visited map[T]bool) bool {
|
||||||
|
if current == dst { // (1) standing on the target -> found it
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
visited[current] = true // (2) never revisit this vertex
|
||||||
|
|
||||||
|
for _, next := range gp.adjList[current] { // (3) each out-neighbor
|
||||||
|
if !visited[next] && gp.dfs(next, dst, visited) { // (4) ask the same question of it
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false // (5) exhausted every path -> unreachable
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The mental model: `dfs` does **not** know the answer itself. It asks *"can I,
|
||||||
|
`current`, reach `dst`?"* by delegating the *same* question to each of its
|
||||||
|
out-neighbors. If any neighbor can reach `dst`, then so can `current`, and the
|
||||||
|
`true` bubbles straight up the call stack.
|
||||||
|
|
||||||
|
- **(1)** is the only place `true` is born.
|
||||||
|
- **(2)** the `visited` set guarantees termination. If the data ever did
|
||||||
|
contain a cycle, without it the recursion would loop forever.
|
||||||
|
- **(4)** short-circuits: the first neighbor that succeeds ends the search.
|
||||||
|
- **(5)** if every branch dead-ends, there is no path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Trace: rejecting a cycle
|
||||||
|
|
||||||
|
Graph is `a -> b`, `b -> c`. Someone calls `AddVertex("c", "a")`, which fires
|
||||||
|
`hasPath("a", "c")` → `dfs("a", "c", {})`:
|
||||||
|
|
||||||
|
| Call | `current` | `current == dst`? | neighbors | action |
|
||||||
|
| ---- | --------- | ----------------- | --------- | ------------------------------ |
|
||||||
|
| 1 | `a` | `a == c`? no | `[b]` | mark `a`, recurse into `b` |
|
||||||
|
| 2 | `b` | `b == c`? no | `[c]` | mark `b`, recurse into `c` |
|
||||||
|
| 3 | `c` | `c == c`? **yes** | — | **return `true`** |
|
||||||
|
|
||||||
|
The `true` returns up through calls 2 and 1. `hasPath` is `true`, so `c -> a`
|
||||||
|
is **rejected** — `a` could already reach `c`, so the edge would close a loop.
|
||||||
|
|
||||||
|
## 4. Trace: allowing an edge
|
||||||
|
|
||||||
|
Graph is just `a -> b`. Someone calls `AddVertex("a", "c")` → `hasPath("c", "a")`
|
||||||
|
→ `dfs("c", "a", {})`:
|
||||||
|
|
||||||
|
| Call | `current` | `current == dst`? | neighbors | action |
|
||||||
|
| ---- | --------- | ----------------- | --------- | ------------------------------ |
|
||||||
|
| 1 | `c` | `c == a`? no | `[]` | mark `c`, loop body never runs |
|
||||||
|
| — | | | | fall through → **return `false`** |
|
||||||
|
|
||||||
|
No path → the edge `a -> c` is safe and gets appended.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Cost
|
||||||
|
|
||||||
|
Each `AddVertex` runs one DFS: **O(V + E)** time, O(V) space for `visited`.
|
||||||
|
The self-loop case (`from == to`) is handled by a cheap equality check up front,
|
||||||
|
before any DFS runs.
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package graphs
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
// DAG -> Directed Acyclic Graph. Any edge that would introduce a cycle is
|
||||||
|
// rejected, so the acyclic invariant always holds.
|
||||||
|
type DAG[T comparable] struct {
|
||||||
|
adjList map[T][]T
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDAG() -> Creates a Directed Acyclic Graph
|
||||||
|
func NewDAG[T comparable]() *DAG[T] {
|
||||||
|
gp := &DAG[T]{
|
||||||
|
adjList: make(map[T][]T),
|
||||||
|
}
|
||||||
|
|
||||||
|
return gp
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddIsolatedVertex() -> Creates an isolated Vertex returns True if ok ,
|
||||||
|
// false if vertex already exist
|
||||||
|
func (gp *DAG[T]) AddIsolatedVertex(newVertex T) bool {
|
||||||
|
_, ok := gp.adjList[newVertex]
|
||||||
|
if !ok {
|
||||||
|
gp.adjList[newVertex] = make([]T, 0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddVertex() -> adds a directed edge from -> to, creating either vertex if it
|
||||||
|
// is missing. Returns false (and adds nothing) when the edge is a self loop,
|
||||||
|
// already exists, or would create a cycle.
|
||||||
|
func (gp *DAG[T]) AddVertex(from T, to T) bool {
|
||||||
|
// self loops are cycles of length 1
|
||||||
|
if from == to {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
gp.AddIsolatedVertex(from)
|
||||||
|
gp.AddIsolatedVertex(to)
|
||||||
|
|
||||||
|
if slices.Contains(gp.adjList[from], to) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// from -> to closes a cycle iff "to" can already reach "from".
|
||||||
|
if gp.hasPath(to, from) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
gp.adjList[from] = append(gp.adjList[from], to)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasPath() -> reports whether a directed path exists from src to dst.
|
||||||
|
func (gp *DAG[T]) hasPath(src T, dst T) bool {
|
||||||
|
visited := make(map[T]bool)
|
||||||
|
return gp.dfs(src, dst, visited)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp *DAG[T]) dfs(current T, dst T, visited map[T]bool) bool {
|
||||||
|
if current == dst {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
visited[current] = true
|
||||||
|
|
||||||
|
for _, next := range gp.adjList[current] {
|
||||||
|
if !visited[next] && gp.dfs(next, dst, visited) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order() -> returns the amount of Vertices
|
||||||
|
func (gp *DAG[T]) Order() int {
|
||||||
|
return len(gp.adjList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size() -> returns the amount of Edges
|
||||||
|
func (gp *DAG[T]) Size() int {
|
||||||
|
var size int
|
||||||
|
|
||||||
|
// directed: each edge is stored once, so no halving.
|
||||||
|
for _, val := range gp.adjList {
|
||||||
|
size += len(val)
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display() -> returns adjList[map]T []T
|
||||||
|
func (gp *DAG[T]) Display() map[T][]T {
|
||||||
|
return gp.adjList
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package graphs
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
// DirectedGraph
|
||||||
|
type DirectedGraph[T comparable] struct {
|
||||||
|
adjList map[T][]T
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDirectedGraph() -> Creates a Directed Graph
|
||||||
|
func NewDirectedGraph[T comparable]() *DirectedGraph[T] {
|
||||||
|
gp := &DirectedGraph[T]{
|
||||||
|
adjList: make(map[T][]T),
|
||||||
|
}
|
||||||
|
|
||||||
|
return gp
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddIsolatedVertex() -> Creates an isolated Vertex returns True if ok ,
|
||||||
|
// false if vertex already exist
|
||||||
|
func (gp *DirectedGraph[T]) AddIsolatedVertex(newVertex T) bool {
|
||||||
|
_, ok := gp.adjList[newVertex]
|
||||||
|
if !ok {
|
||||||
|
gp.adjList[newVertex] = make([]T, 0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddVertex() -> Creates a directed edge newVertex -> toVertex. Creates both
|
||||||
|
// vertices if they dont exist.
|
||||||
|
func (gp *DirectedGraph[T]) AddVertex(newVertex T, toVertex T) {
|
||||||
|
// self loops are not allowed in this graph
|
||||||
|
if newVertex == toVertex {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gp.AddIsolatedVertex(newVertex)
|
||||||
|
gp.AddIsolatedVertex(toVertex)
|
||||||
|
|
||||||
|
// directed: only store the edge on the source's out-list, once.
|
||||||
|
if !slices.Contains(gp.adjList[newVertex], toVertex) {
|
||||||
|
gp.adjList[newVertex] = append(gp.adjList[newVertex], toVertex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order() -> returns the amount of Vertices
|
||||||
|
func (gp *DirectedGraph[T]) Order() int {
|
||||||
|
return len(gp.adjList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size() -> returns the amount of Edges
|
||||||
|
func (gp *DirectedGraph[T]) Size() int {
|
||||||
|
var size int
|
||||||
|
|
||||||
|
// directed: each edge is stored once, so no halving.
|
||||||
|
for _, val := range gp.adjList {
|
||||||
|
size += len(val)
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display() -> returns adjList[map]T []T
|
||||||
|
func (gp *DirectedGraph[T]) Display() map[T][]T {
|
||||||
|
return gp.adjList
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"datastructures/graphs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDAGIsEmpty(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[int]()
|
||||||
|
|
||||||
|
if dag.Order() != 0 {
|
||||||
|
t.Errorf("new dag Order = %d, want 0", dag.Order())
|
||||||
|
}
|
||||||
|
if dag.Size() != 0 {
|
||||||
|
t.Errorf("new dag Size = %d, want 0", dag.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGAddVertexReturnsTrueOnSuccess(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[string]()
|
||||||
|
|
||||||
|
if !dag.AddVertex("a", "b") {
|
||||||
|
t.Error("AddVertex(a, b) = false, want true")
|
||||||
|
}
|
||||||
|
if dag.Order() != 2 {
|
||||||
|
t.Errorf("Order = %d, want 2", dag.Order())
|
||||||
|
}
|
||||||
|
if dag.Size() != 1 {
|
||||||
|
t.Errorf("Size = %d, want 1", dag.Size())
|
||||||
|
}
|
||||||
|
if !slices.Contains(dag.Display()["a"], "b") {
|
||||||
|
t.Errorf("expected a->b, got a: %v", dag.Display()["a"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The core invariant: an edge that would close a cycle must be rejected.
|
||||||
|
func TestDAGRejectsCycle(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[string]()
|
||||||
|
dag.AddVertex("a", "b")
|
||||||
|
dag.AddVertex("b", "c")
|
||||||
|
|
||||||
|
// c -> a would close a -> b -> c -> a
|
||||||
|
if dag.AddVertex("c", "a") {
|
||||||
|
t.Error("AddVertex(c, a) = true, want false (would create a cycle)")
|
||||||
|
}
|
||||||
|
if dag.Size() != 2 {
|
||||||
|
t.Errorf("Size = %d, want 2 (cycle edge must not be added)", dag.Size())
|
||||||
|
}
|
||||||
|
if slices.Contains(dag.Display()["c"], "a") {
|
||||||
|
t.Errorf("c->a should not have been added, got c: %v", dag.Display()["c"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A back-edge across a longer chain must also be caught.
|
||||||
|
func TestDAGRejectsLongCycle(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[int]()
|
||||||
|
dag.AddVertex(1, 2)
|
||||||
|
dag.AddVertex(2, 3)
|
||||||
|
dag.AddVertex(3, 4)
|
||||||
|
dag.AddVertex(4, 5)
|
||||||
|
|
||||||
|
// 5 -> 1 would close the whole chain into a loop
|
||||||
|
if dag.AddVertex(5, 1) {
|
||||||
|
t.Error("AddVertex(5, 1) = true, want false (long cycle)")
|
||||||
|
}
|
||||||
|
if dag.Size() != 4 {
|
||||||
|
t.Errorf("Size = %d, want 4", dag.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGSelfLoopRejected(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[int]()
|
||||||
|
|
||||||
|
if dag.AddVertex(1, 1) {
|
||||||
|
t.Error("AddVertex(1, 1) = true, want false (self loop)")
|
||||||
|
}
|
||||||
|
if dag.Size() != 0 {
|
||||||
|
t.Errorf("self-loop should add no edge, Size = %d", dag.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A diamond has multiple paths between two vertices but no cycle: all edges
|
||||||
|
// should be accepted.
|
||||||
|
func TestDAGAllowsDiamond(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[string]()
|
||||||
|
|
||||||
|
if !dag.AddVertex("a", "b") {
|
||||||
|
t.Error("a->b rejected")
|
||||||
|
}
|
||||||
|
if !dag.AddVertex("a", "c") {
|
||||||
|
t.Error("a->c rejected")
|
||||||
|
}
|
||||||
|
if !dag.AddVertex("b", "d") {
|
||||||
|
t.Error("b->d rejected")
|
||||||
|
}
|
||||||
|
if !dag.AddVertex("c", "d") {
|
||||||
|
t.Error("c->d rejected (diamond, not a cycle)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dag.Order() != 4 {
|
||||||
|
t.Errorf("Order = %d, want 4", dag.Order())
|
||||||
|
}
|
||||||
|
if dag.Size() != 4 {
|
||||||
|
t.Errorf("Size = %d, want 4", dag.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGDuplicateEdgeRejected(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[string]()
|
||||||
|
dag.AddVertex("a", "b")
|
||||||
|
|
||||||
|
if dag.AddVertex("a", "b") {
|
||||||
|
t.Error("duplicate AddVertex(a, b) = true, want false")
|
||||||
|
}
|
||||||
|
if len(dag.Display()["a"]) != 1 {
|
||||||
|
t.Errorf("expected no duplicate edge, got a: %v", dag.Display()["a"])
|
||||||
|
}
|
||||||
|
if dag.Size() != 1 {
|
||||||
|
t.Errorf("Size = %d, want 1", dag.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rejecting a cycle edge must not corrupt the graph: valid edges added
|
||||||
|
// afterward should still work.
|
||||||
|
func TestDAGStillUsableAfterRejection(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[string]()
|
||||||
|
dag.AddVertex("a", "b")
|
||||||
|
dag.AddVertex("b", "c")
|
||||||
|
dag.AddVertex("c", "a") // rejected
|
||||||
|
|
||||||
|
if !dag.AddVertex("c", "d") {
|
||||||
|
t.Error("AddVertex(c, d) = false after a rejection, want true")
|
||||||
|
}
|
||||||
|
if dag.Size() != 3 {
|
||||||
|
t.Errorf("Size = %d, want 3", dag.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAGAddIsolatedVertex(t *testing.T) {
|
||||||
|
dag := graphs.NewDAG[string]()
|
||||||
|
|
||||||
|
if !dag.AddIsolatedVertex("solo") {
|
||||||
|
t.Error("AddIsolatedVertex on new vertex = false, want true")
|
||||||
|
}
|
||||||
|
if dag.AddIsolatedVertex("solo") {
|
||||||
|
t.Error("AddIsolatedVertex on existing vertex = true, want false")
|
||||||
|
}
|
||||||
|
if dag.Order() != 1 {
|
||||||
|
t.Errorf("Order = %d, want 1", dag.Order())
|
||||||
|
}
|
||||||
|
if dag.Size() != 0 {
|
||||||
|
t.Errorf("isolated vertex should add no edges, Size = %d", dag.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"datastructures/graphs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDirectedGraphIsEmpty(t *testing.T) {
|
||||||
|
graph := graphs.NewDirectedGraph[int]()
|
||||||
|
|
||||||
|
if graph.Order() != 0 {
|
||||||
|
t.Errorf("new graph Order = %d, want 0", graph.Order())
|
||||||
|
}
|
||||||
|
if graph.Size() != 0 {
|
||||||
|
t.Errorf("new graph Size = %d, want 0", graph.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectedAddVertexIsOneWay(t *testing.T) {
|
||||||
|
graph := graphs.NewDirectedGraph[string]()
|
||||||
|
graph.AddVertex("a", "b")
|
||||||
|
|
||||||
|
if graph.Order() != 2 {
|
||||||
|
t.Errorf("Order = %d, want 2", graph.Order())
|
||||||
|
}
|
||||||
|
if graph.Size() != 1 {
|
||||||
|
t.Errorf("Size = %d, want 1", graph.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
adjacency := graph.Display()
|
||||||
|
if !slices.Contains(adjacency["a"], "b") {
|
||||||
|
t.Errorf("expected b in a's out-list, got %v", adjacency["a"])
|
||||||
|
}
|
||||||
|
// directed: the edge must NOT appear in reverse.
|
||||||
|
if slices.Contains(adjacency["b"], "a") {
|
||||||
|
t.Errorf("directed edge should not point back, got b: %v", adjacency["b"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectedSizeCountsEachEdgeOnce(t *testing.T) {
|
||||||
|
graph := graphs.NewDirectedGraph[string]()
|
||||||
|
graph.AddVertex("a", "b")
|
||||||
|
graph.AddVertex("b", "a")
|
||||||
|
|
||||||
|
// two distinct directed edges: a->b and b->a
|
||||||
|
if graph.Size() != 2 {
|
||||||
|
t.Errorf("Size = %d, want 2 (a->b and b->a are distinct)", graph.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectedAddVertexIsIdempotent(t *testing.T) {
|
||||||
|
graph := graphs.NewDirectedGraph[string]()
|
||||||
|
graph.AddVertex("a", "b")
|
||||||
|
graph.AddVertex("a", "b")
|
||||||
|
|
||||||
|
if len(graph.Display()["a"]) != 1 {
|
||||||
|
t.Errorf("expected no duplicate edge, got %v", graph.Display()["a"])
|
||||||
|
}
|
||||||
|
if graph.Size() != 1 {
|
||||||
|
t.Errorf("Size = %d, want 1", graph.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression: a vertex that is already the target of an edge must still be
|
||||||
|
// able to have its own outgoing edges (the old check2 loop broke this).
|
||||||
|
func TestDirectedTargetCanHaveOutgoingEdges(t *testing.T) {
|
||||||
|
graph := graphs.NewDirectedGraph[string]()
|
||||||
|
graph.AddVertex("c", "a") // a is now a target
|
||||||
|
graph.AddVertex("a", "b") // a must still be allowed an out-edge
|
||||||
|
|
||||||
|
if !slices.Contains(graph.Display()["a"], "b") {
|
||||||
|
t.Errorf("expected a->b even though a is a target, got a: %v", graph.Display()["a"])
|
||||||
|
}
|
||||||
|
if graph.Size() != 2 {
|
||||||
|
t.Errorf("Size = %d, want 2", graph.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectedMultipleOutEdges(t *testing.T) {
|
||||||
|
graph := graphs.NewDirectedGraph[int]()
|
||||||
|
graph.AddVertex(1, 2)
|
||||||
|
graph.AddVertex(1, 3)
|
||||||
|
graph.AddVertex(1, 4)
|
||||||
|
|
||||||
|
if graph.Order() != 4 {
|
||||||
|
t.Errorf("Order = %d, want 4", graph.Order())
|
||||||
|
}
|
||||||
|
if graph.Size() != 3 {
|
||||||
|
t.Errorf("Size = %d, want 3", graph.Size())
|
||||||
|
}
|
||||||
|
if len(graph.Display()[1]) != 3 {
|
||||||
|
t.Errorf("expected vertex 1 out-degree 3, got %v", graph.Display()[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectedSelfLoopRejected(t *testing.T) {
|
||||||
|
graph := graphs.NewDirectedGraph[int]()
|
||||||
|
graph.AddVertex(1, 1)
|
||||||
|
|
||||||
|
if graph.Size() != 0 {
|
||||||
|
t.Errorf("self-loop should add no edge, Size = %d", graph.Size())
|
||||||
|
}
|
||||||
|
if slices.Contains(graph.Display()[1], 1) {
|
||||||
|
t.Errorf("vertex 1 should not point to itself, got %v", graph.Display()[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectedAddIsolatedVertex(t *testing.T) {
|
||||||
|
graph := graphs.NewDirectedGraph[string]()
|
||||||
|
|
||||||
|
if !graph.AddIsolatedVertex("solo") {
|
||||||
|
t.Error("AddIsolatedVertex on new vertex = false, want true")
|
||||||
|
}
|
||||||
|
if graph.AddIsolatedVertex("solo") {
|
||||||
|
t.Error("AddIsolatedVertex on existing vertex = true, want false")
|
||||||
|
}
|
||||||
|
if graph.Order() != 1 {
|
||||||
|
t.Errorf("Order = %d, want 1", graph.Order())
|
||||||
|
}
|
||||||
|
if graph.Size() != 0 {
|
||||||
|
t.Errorf("isolated vertex should add no edges, Size = %d", graph.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user