gograph

package module
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2026 License: Apache-2.0 Imports: 10 Imported by: 9

README

gograph, a generic graph library for Go

Dependency graphs and graph algorithms for Go, with no dependencies outside the standard library.

build coverage Go Reference release Go version license Mentioned in Awesome Go XERJ Corpus Hub contributor CodeRabbit Pull Request Reviews Sponsor GitHub stars

Documentation · Interactive demos · Examples · Changelog · Roadmap

Dijkstra finding the quickest drive from Manhattan to Brooklyn on a New York street map, on gograph.dev
path.Dijkstra finding the quickest drive from Canal St in Manhattan to Court St in Brooklyn on gograph.dev, where every algorithm runs step by step on example graphs.

gograph is a generic graph library for Go (Golang) with first-class support for dependency graphs. Acyclic graphs refuse edges that would create a cycle, and TopologySort gives you an order to run things in. It also covers traversal, shortest paths, strongly connected components and graph partitioning.

  • Generic: vertex labels can be any comparable type, such as strings, integers or your own structs.
  • Graph data structures: directed, undirected, acyclic (DAG) and weighted graphs, with weights on vertices and edges.
  • Dependency graphs: Acyclic() graphs reject cycles, TopologySort returns a valid order, and the dag package finds what depends on what and what can run in parallel.
  • Traversal: BFS (breadth-first search), DFS (depth-first search), topological sort, closest-first and random-walk iterators, and depth-first search events that label each edge as a tree, back, forward or cross edge.
  • Shortest paths: Dijkstra, Bellman-Ford, Floyd-Warshall, multi-source Dijkstra, k-center and transitive reduction.
  • Connectivity: connected components and cycle detection that can return the cycle, strongly connected components with Tarjan, Kosaraju and Gabow, and condensation into a DAG.
  • Partitioning: maximal cliques (Bron-Kerbosch), Girvan-Newman communities and randomized k-cut.
  • Diagrams: encoding/mermaid writes a graph as a Mermaid flowchart that GitHub renders in Markdown, and encoding/dot writes it in the Graphviz DOT language for larger graphs.
  • Diffs: Diff lists the vertices, edges, weights and edge labels that changed between two graphs, one per line or as JSON, and Apply makes the same changes to a copy.
  • Deterministic: a graph built the same way gives the same topological order, components and cliques on every run.
  • Testing: gographtest checks topological orders, groups of vertices and whole graphs in tests, and builds paths, cycles, grids and seeded random graphs.

Imported by 20+ public Go modules, including the Cilium project's ariane, BoostSecurity's smokedmeat, simplecontainer and faultline, deterministic simulation testing for Go by the same author.

⭐ If gograph is useful to you, a star on GitHub helps other Go developers find it.

Table of contents

Installation

go get github.com/hmdsefi/gograph

gograph requires Go 1.26 or later and uses only the standard library.

Quick start

package main

import (
	"errors"
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	// An edge A -> B means A has to happen before B.
	g := gograph.New[string](gograph.Acyclic())

	checkout := g.AddVertexByLabel("checkout")
	build := g.AddVertexByLabel("build")
	test := g.AddVertexByLabel("test")
	release := g.AddVertexByLabel("release")

	_, _ = g.AddEdge(checkout, build)
	_, _ = g.AddEdge(build, test)
	_, _ = g.AddEdge(test, release)

	// Acyclic graphs reject any edge that would create a cycle.
	_, err := g.AddEdge(release, checkout)
	fmt.Println(errors.Is(err, gograph.ErrDAGCycle)) // true

	order, _ := gograph.TopologySort(g)
	for _, v := range order {
		fmt.Println(v.Label()) // checkout, build, test, release
	}
}

Use cases

  • Build and CI pipelines: order the steps with TopologySort, run independent steps together with dag.Levels or dag.Tracker, and find the chain of steps that sets the total time with dag.CriticalPath.
  • Task schedulers and workflow engines: dag.Tracker hands out tasks as their dependencies finish.
  • Incremental builds and spreadsheets: dag.Affected lists everything a change reaches, so you recompute only that.
  • Module and package dependencies: find requirement cycles with connectivity.Tarjan and collapse them with connectivity.Condense. The gomodgraph example does this for go mod graph output.
  • Routing and placement: shortest routes with path.Dijkstra, the nearest depot for every address with path.DijkstraMultiSource, and where to put k facilities with path.KCenter.
  • Networks and communities: loops in service calls with strongly connected components, and groups in a social graph with partition.MaximalCliques and partition.GirvanNewman.

Graphs

gograph.New[T] creates a graph. T is the vertex label type and must be comparable, so slices, maps and functions can't be labels. Options choose the kind of graph:

  • gograph.Directed() creates a directed graph. Without it, graphs are undirected.
  • gograph.Acyclic() creates a directed graph that rejects edges that would create a cycle.
  • gograph.Weighted() marks the graph as weighted. This flag is separate from weights supplied with WithEdgeWeight: Dijkstra uses those weights even without it. BellmanFord and FloydWarshall require the flag or return ErrNotWeighted; random walks use edge weights only when the flag is set.

Every graph implements the Graph[T] interface. See the package documentation for the full list of methods.

AddEdge creates missing vertices, so gograph.NewVertex is enough for quick examples. To keep a reference to a vertex, use AddVertexByLabel, which adds the vertex and returns it.

The diagrams in this section are the output of encoding/mermaid for each example graph.

Directed

flowchart LR
    n0["1"]
    n1["2"]
    n2["3"]
    n3["4"]
    n4["5"]
    n5["6"]
    n0 --> n1
    n0 --> n2
    n1 --> n3
    n2 --> n3
    n3 --> n4
    n4 --> n5
g := gograph.New[int](gograph.Directed())

_, _ = g.AddEdge(gograph.NewVertex(1), gograph.NewVertex(2))
_, _ = g.AddEdge(gograph.NewVertex(1), gograph.NewVertex(3))
_, _ = g.AddEdge(gograph.NewVertex(2), gograph.NewVertex(4))
_, _ = g.AddEdge(gograph.NewVertex(3), gograph.NewVertex(4))
_, _ = g.AddEdge(gograph.NewVertex(4), gograph.NewVertex(5))
_, _ = g.AddEdge(gograph.NewVertex(5), gograph.NewVertex(6))

Acyclic

flowchart LR
    n0["1"]
    n1["2"]
    n2["3"]
    n0 --> n1
    n1 --> n2
g := gograph.New[int](gograph.Acyclic())

_, _ = g.AddEdge(gograph.NewVertex(1), gograph.NewVertex(2))
_, _ = g.AddEdge(gograph.NewVertex(2), gograph.NewVertex(3))

_, err := g.AddEdge(gograph.NewVertex(3), gograph.NewVertex(1))
fmt.Println(err) // edges would create cycle

Undirected

flowchart LR
    n0["A"]
    n1["B"]
    n2["C"]
    n3["D"]
    n0 --- n1
    n0 --- n3
    n1 --- n2
    n1 --- n3
// Graphs are undirected by default.
g := gograph.New[string]()

a := g.AddVertexByLabel("A")
b := g.AddVertexByLabel("B")
c := g.AddVertexByLabel("C")
d := g.AddVertexByLabel("D")

_, _ = g.AddEdge(a, b)
_, _ = g.AddEdge(a, d)
_, _ = g.AddEdge(b, c)
_, _ = g.AddEdge(b, d)

// Every undirected edge can be followed both ways.
fmt.Println(g.ContainsEdge(a, b), g.ContainsEdge(b, a)) // true true

An undirected edge between distinct vertices is stored in both directions. For two undirected edges A - B and A - C:

Call Result
g.Size() / len(g.AllEdges()) 4
a.Degree() 4
a.InDegree() / a.OutDegree() 2
len(g.EdgesOf(a)) 4
len(g.GetAllEdges(a, b)) 2

Use OutDegree() to count neighbors in an undirected graph. A self-loop has the same source and destination, so it has only one stored direction.

Weighted

flowchart LR
    n0["A"]
    n1["B"]
    n2["C"]
    n3["D"]
    n0 ---|"4"| n1
    n0 ---|"3"| n3
    n1 ---|"3"| n2
    n1 ---|"1"| n3
    n2 ---|"2"| n3
g := gograph.New[string](gograph.Weighted())

a := g.AddVertexByLabel("A")
b := g.AddVertexByLabel("B")
c := g.AddVertexByLabel("C")
d := g.AddVertexByLabel("D")

_, _ = g.AddEdge(a, b, gograph.WithEdgeWeight(4))
_, _ = g.AddEdge(a, d, gograph.WithEdgeWeight(3))
_, _ = g.AddEdge(b, c, gograph.WithEdgeWeight(3))
_, _ = g.AddEdge(b, d, gograph.WithEdgeWeight(1))
_, _ = g.AddEdge(c, d, gograph.WithEdgeWeight(2))

dist := path.Dijkstra(g, "A")
fmt.Println(dist["C"]) // 5

Vertices can have weights too:

flowchart LR
    n0["A, weight 3"]
    n1["B, weight 2"]
    n2["C, weight 4"]
    n0 --> n1
    n1 --> n2
g := gograph.New[string](gograph.Directed(), gograph.Weighted())

a := g.AddVertexByLabel("A", gograph.WithVertexWeight(3))
b := g.AddVertexByLabel("B", gograph.WithVertexWeight(2))
c := g.AddVertexByLabel("C", gograph.WithVertexWeight(4))

_, _ = g.AddEdge(a, b)
_, _ = g.AddEdge(b, c)

fmt.Println(a.Weight(), b.Weight(), c.Weight()) // 3 2 4

Traversal

The traverse package provides iterators that all implement the same interface:

type Iterator[T comparable] interface {
	HasNext() bool
	Next() *gograph.Vertex[T]
	Iterate(func(v *gograph.Vertex[T]) error) error
	Reset()
}
g := gograph.New[string](gograph.Directed())

_, _ = g.AddEdge(gograph.NewVertex("A"), gograph.NewVertex("B"))
_, _ = g.AddEdge(gograph.NewVertex("A"), gograph.NewVertex("C"))
_, _ = g.AddEdge(gograph.NewVertex("B"), gograph.NewVertex("D"))

it, err := traverse.NewBreadthFirstIterator(g, "A")
if err != nil {
	fmt.Println(err)
	return
}

for it.HasNext() {
	fmt.Println(it.Next().Label()) // A, B, C, D
}
Iterator Order Demo
NewBreadthFirstIterator Nearest vertices first, level by level watch
NewDepthFirstIterator Each branch as deep as it goes before the next watch
NewTopologicalIterator Topological order, one vertex at a time watch
NewClosestFirstIterator By shortest weighted distance from the start watch
NewRandomWalkIterator A random walk that picks each edge in proportion to its weight in a weighted graph watch

The breadth-first, depth-first, closest-first and topological traversals also come as sequences for a range loop, so break stops one early and slices.Collect gathers it:

for v, depth := range traverse.BFSWithDepth(g, "A") {
	fmt.Println(v.Label(), depth) // A 0, B 1, C 1, D 2
}

traverse.BFS, DFS, BFSWithDepth, ClosestFirst and Topological follow the iterators above, and gograph.Vertices and gograph.Edges range over a whole graph. See Range over a traversal for details. traverse.DFSEvents and DFSEventsAll yield each step of a depth-first search, with the kind of each edge, as described in Depth-first search events.

Algorithms

V is the number of vertices and E the number of edges. Every link in the Demo column runs the algorithm step by step on example graphs at gograph.dev.

Ordering and subgraphs (gograph package)

Function Answers Time Demo
TopologySort An order where every edge points forward (Kahn's algorithm) O(V+E) watch
StableTopologySort The same, picking the smallest ready vertex by your compare function O((V+E) log V) watch
InducedSubgraph A chosen set of vertices and the edges between them O(k+D) for k labels with D outgoing edges
Clone A copy of the graph you can change without changing the original O(V+E)
Diff The vertices, edges, weights and edge labels that changed between two graphs O(V+E)

Dependencies (dag package)

Function Answers Time Demo
Descendants What depends on a vertex O(V+E) watch
Ancestors What a vertex depends on O(V+E) watch
Affected Everything a change to some vertices reaches O(V+E) watch
Sources Which vertices have no incoming edges O(V)
Sinks Which vertices have no outgoing edges O(V)
Levels Which vertices can run at the same time O(V+E)
NewTracker Which vertices are ready as others finish O(V+E) in total
CriticalPath The most expensive chain of dependent vertices O(V+E)

Shortest paths (path package)

Shortest-path algorithm Unreachable distance Requirement
path.Dijkstra / path.DijkstraSimple math.MaxFloat64 Non-negative weights; not checked
path.BellmanFord math.Inf(1) Directed and Weighted(); negative weights allowed, reachable negative cycles rejected
path.FloydWarshall math.Inf(1) Directed and Weighted(); negative weights allowed, negative cycles anywhere in the graph rejected
Function Answers Time Demo
Dijkstra Distances from one vertex, no negative weights O((V+E) log V) watch
BellmanFord Distances from one vertex, negative weights allowed O(V·E) watch
FloydWarshall Distances between every pair of vertices O(V³) watch
DijkstraMultiSource Each vertex's nearest source, in one search O((V+E) log V)
KCenter k centers that keep every vertex close to one, within twice the best radius on undirected graphs O(k (V+E) log V)
TransitiveReduction The fewest edges that keep every path O(V·(V+E)) watch

Connectivity (connectivity package, notes)

Function Answers Time Demo
ConnectedComponents Connected components. A directed graph is treated as undirected O(V+E)
HasCycle Whether the graph has a cycle, including a self-loop O(V+E)
FindCycle The vertices of one cycle, in order O(V+E)
Tarjan Strongly connected components O(V+E) watch
Kosaraju Strongly connected components O(V+E) watch
Gabow Strongly connected components O(V+E) watch
Condense The DAG of components, one vertex per component O(V+E)

Partitioning (partition package)

Function Answers Time Demo
MaximalCliques Every group where all vertices connect to each other (Bron-Kerbosch) O(V·3^(V/3)) worst case, including the output watch
GirvanNewman Communities, by removing the most central edges O(E·V·(V+E)) watch
RandomizedKCut A split into k groups with few edges between them O(V·E) per run watch

Diagrams

encoding/mermaid writes a graph as a Mermaid flowchart, which GitHub and GitLab render in Markdown. Options set the direction, the vertex and edge text, and classes for highlighting:

g := gograph.New[string](gograph.Directed())

_, _ = g.AddEdge(gograph.NewVertex("checkout"), gograph.NewVertex("build"))
_, _ = g.AddEdge(g.GetVertexByID("build"), gograph.NewVertex("test"))

// highlight the vertices that failed
failed := map[string]bool{"test": true}

err := mermaid.Write(os.Stdout, g,
	mermaid.WithVertexClass(func(v *gograph.Vertex[string]) string {
		if failed[v.Label()] {
			return "failed"
		}
		return ""
	}),
	mermaid.WithClassDef[string]("failed", "fill:#f96"),
)
flowchart TD
    n0["build"]
    n1["checkout"]
    n2["test"]
    n0 --> n2
    n1 --> n0
    classDef failed fill:#f96
    class n2 failed

For graphs too large for a Mermaid diagram, encoding/dot writes the Graphviz DOT language, which dot -Tsvg turns into an image.

Determinism

When several orders are valid, TopologySort follows the order the vertices and edges were added, so the same graph gives the same result on every run. To choose the order yourself, StableTopologySort takes a compare function such as cmp.Compare and always picks the smallest vertex that is ready.

GetAllVertices returns vertices in the order they were added, and AllEdges and EdgesOf follow the same order. Tarjan, Kosaraju, Gabow, MaximalCliques, GirvanNewman and TransitiveReduction give the same result on every run for a graph built the same way.

Testing

gographtest helps test code that uses gograph. IsTopologicalOrder accepts any valid topological order, and SameGroups compares components, cliques or communities in any order, so a test keeps passing when an algorithm returns another valid answer. Equal compares two graphs whatever order they were built in, and lists the vertices and edges that differ. Its generators build paths, cycles, complete graphs, grids and seeded random graphs with the labels 0 to n-1:

func TestBuildOrder(t *testing.T) {
	g := gographtest.RandomDAG(500, 0.02, rand.New(rand.NewPCG(1, 2)))

	order, err := gograph.TopologySort(g)
	if err != nil {
		t.Fatal(err)
	}
	gographtest.IsTopologicalOrder(t, g, order)
}

Performance

Measured with go test -run '^$' -bench . -benchmem ./... on an Apple M4 Max with Go 1.27:

Benchmark Graph Time per run
TopologySort Binary tree, 100,000 vertices 7.0 ms
StableTopologySort Binary tree, 100,000 vertices 15.9 ms
dag.CriticalPath Binary tree, 100,000 vertices 10.8 ms
path.DijkstraMultiSource Path of 2,000 vertices, 32 sources 0.19 ms
path.KCenter Path of 2,000 vertices, k = 32 0.31 ms
InducedSubgraph 256 vertices out of 4,000 0.05 ms

Each algorithm page on gograph.dev also has a performance card that times the algorithm on that page's example graphs.

Examples

  • gomodgraph: loads go mod graph output and finds requirement cycles, the modules that depend on a module, why a module is needed, which version of each module builds, what changed between two versions of go.mod, and a diagram of a module with its direct dependencies and dependents.

Stability

gograph follows semantic versioning and is used by other projects, so changes keep existing code working: exported interfaces keep their method sets, exported functions keep their signatures, and sentinel errors stay comparable with ==. The rules are in CONTRIBUTING.md, and every release is listed in the changelog.

Roadmap

Planned work, in the order it's likely to land, is tracked in #136. Issues labeled good first issue are a good place to start.

Contributing

Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request. The README examples are also Go examples in example_test.go, so go test ./... checks that they still compile and print what they claim.

To report a security problem, follow SECURITY.md.

Sponsoring

If gograph saves you time, you can support its development through GitHub Sponsors.

License

Apache License 2.0, see LICENSE for details.

Documentation

Overview

Example
package main

import (
	"errors"
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	// An edge A -> B means A has to happen before B.
	g := gograph.New[string](gograph.Acyclic())

	checkout := g.AddVertexByLabel("checkout")
	build := g.AddVertexByLabel("build")
	test := g.AddVertexByLabel("test")
	release := g.AddVertexByLabel("release")

	_, _ = g.AddEdge(checkout, build)
	_, _ = g.AddEdge(build, test)
	_, _ = g.AddEdge(test, release)

	// Acyclic graphs reject any edge that would create a cycle.
	_, err := g.AddEdge(release, checkout)
	fmt.Println(errors.Is(err, gograph.ErrDAGCycle))

	order, _ := gograph.TopologySort(g)
	for _, v := range order {
		fmt.Println(v.Label())
	}

}
Output:
true
checkout
build
test
release
Example (BreadthFirst)
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
	"github.com/hmdsefi/gograph/traverse"
)

func main() {
	g := gograph.New[string](gograph.Directed())

	_, _ = g.AddEdge(gograph.NewVertex("A"), gograph.NewVertex("B"))
	_, _ = g.AddEdge(gograph.NewVertex("A"), gograph.NewVertex("C"))
	_, _ = g.AddEdge(gograph.NewVertex("B"), gograph.NewVertex("D"))

	it, err := traverse.NewBreadthFirstIterator(g, "A")
	if err != nil {
		fmt.Println(err)
		return
	}

	for it.HasNext() {
		fmt.Println(it.Next().Label())
	}

}
Output:
A
B
C
D
Example (UndirectedEdgeCounts)
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[string]()
	a := g.AddVertexByLabel("A")
	b := g.AddVertexByLabel("B")
	c := g.AddVertexByLabel("C")
	_, _ = g.AddEdge(a, b)
	_, _ = g.AddEdge(a, c)
	fmt.Println(g.Size(), len(g.AllEdges()), len(g.EdgesOf(a)), len(g.GetAllEdges(a, b)))
	fmt.Println(a.Degree(), a.InDegree(), a.OutDegree())
}
Output:
4 4 4 2
4 2 2

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrIncompatibleGraphs is returned by Diff when one graph is directed and
	// the other is undirected, since their edges can't be matched.
	ErrIncompatibleGraphs = errors.New("graphs are not both directed or both undirected")

	// ErrDiffMismatch is returned by GraphDiff.Apply when the diff doesn't fit
	// the graph it's applied to.
	ErrDiffMismatch = errors.New("diff does not match the graph")
)
View Source
var (
	ErrNilVertices        = errors.New("vertices are nil")
	ErrVertexDoesNotExist = errors.New("vertex does not exist")
	ErrEdgeAlreadyExists  = errors.New("edge already exists")
	ErrDAGCycle           = errors.New("edges would create cycle")
	ErrDAGHasCycle        = errors.New("the graph contains a cycle")
	ErrNotDirected        = errors.New("graph is not directed")
)

Functions

func ConnectedComponents added in v0.9.0

func ConnectedComponents[T comparable](g Graph[T]) [][]*Vertex[T]

ConnectedComponents returns the connected components of g. For a directed graph, edge directions are ignored, so it returns the weakly connected components. The vertices in the result are the vertices stored in g, not copies, and an empty graph returns an empty slice.

The components are in the order of their first vertex in GetAllVertices. In an undirected graph, the vertices of a component are in breadth-first order from that vertex, following the order the edges were added. In a directed graph, they are in GetAllVertices order. The components share one backing array, but appending to one doesn't change the others.

A graph from New is read from the neighbor lists stored on its vertices. An undirected one takes O(V+E) time. A directed one takes O((V+E)·α(V)) time, where α is the inverse Ackermann function, which is below 5 for any graph that fits in memory. Both use O(V) extra space. Other directed Graph implementations are read through GetAllVertices and AllEdges, also in O((V+E)·α(V)) time. Other undirected Graph implementations are read through GetAllVertices and EdgesOf, with one EdgesOf call per vertex. It panics if g is nil.

connectivity.ConnectedComponents calls this.

Example
g := New[string]()
_, _ = g.AddEdge(NewVertex("a"), NewVertex("b"))
_, _ = g.AddEdge(NewVertex("b"), NewVertex("c"))
_, _ = g.AddEdge(NewVertex("d"), NewVertex("e"))
g.AddVertexByLabel("f")

for _, component := range ConnectedComponents(g) {
	var labels []string
	for _, v := range component {
		labels = append(labels, v.Label())
	}
	fmt.Println(labels)
}
Output:
[a b c]
[d e]
[f]

func Edges added in v0.10.0

func Edges[T comparable](g Graph[T]) iter.Seq[*Edge[T]]

Edges returns the edges of g in the order of AllEdges. In an undirected graph, an edge between two vertices comes once in each direction, and a self-loop once. If g changes during the loop, edges can be skipped or repeated.

For a graph from New, it reads the edges in place: ranging over all of them takes O(V+E) time and allocates nothing. For another implementation of Graph, it calls AllEdges before the first edge.

Example
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[string](gograph.Directed(), gograph.Weighted())
	_, _ = g.AddEdge(gograph.NewVertex("A"), gograph.NewVertex("B"), gograph.WithEdgeWeight(2))
	_, _ = g.AddEdge(gograph.NewVertex("A"), gograph.NewVertex("C"), gograph.WithEdgeWeight(5))
	_, _ = g.AddEdge(gograph.NewVertex("B"), gograph.NewVertex("C"), gograph.WithEdgeWeight(1))

	total := 0.0
	for e := range gograph.Edges(g) {
		total += e.Weight()
	}
	fmt.Println(total)
}
Output:
8

func HasCycle added in v0.9.0

func HasCycle[T comparable](g Graph[T]) bool

HasCycle reports whether g contains a cycle. A self-loop is a cycle. In a directed graph, a cycle follows the edge directions. In an undirected graph, the two stored directions of an edge count as one edge, so any other cycle needs at least three vertices.

A graph created with Acyclic refuses edges that would create a cycle, so it returns false without a search. A graph from New takes O(V+E) time and O(V) extra space, and the search stops at the first cycle it finds. Any other graph is read through its methods: TopologySort when it is directed, and AllNeighbors when it is undirected. It panics if g is nil.

connectivity.HasCycle calls this.

Example
g := New[string](Directed())
_, _ = g.AddEdge(NewVertex("build"), NewVertex("test"))
_, _ = g.AddEdge(NewVertex("test"), NewVertex("deploy"))
fmt.Println(HasCycle(g))

_, _ = g.AddEdge(g.GetVertexByID("deploy"), g.GetVertexByID("build"))
fmt.Println(HasCycle(g))
Output:
false
true

func Vertices added in v0.10.0

func Vertices[T comparable](g Graph[T]) iter.Seq[*Vertex[T]]

Vertices returns the vertices of g in the order of GetAllVertices. Each vertex is the graph's own vertex. If g changes during the loop, vertices can be skipped or repeated.

For a graph from New, it reads the vertices in place: ranging over all of them takes O(V) time and allocates nothing. For another implementation of Graph, it calls GetAllVertices before the first vertex.

Example
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[string](gograph.Directed())
	_, _ = g.AddEdge(gograph.NewVertex("A"), gograph.NewVertex("B"))
	_, _ = g.AddEdge(gograph.NewVertex("A"), gograph.NewVertex("C"))
	_, _ = g.AddEdge(gograph.NewVertex("C"), gograph.NewVertex("B"))

	// the vertices with no outgoing edges
	for v := range gograph.Vertices(g) {
		if v.OutDegree() == 0 {
			fmt.Println(v.Label())
		}
	}
}
Output:
B

Types

type DiffOption added in v0.10.0

type DiffOption func(*diffOptions)

DiffOption changes how Diff compares two graphs.

func WithWeightTolerance added in v0.10.0

func WithWeightTolerance(eps float64) DiffOption

WithWeightTolerance makes Diff treat two weights as equal when they differ by at most eps. A negative or NaN eps keeps the exact comparison.

type Edge

type Edge[T comparable] struct {
	// contains filtered or unexported fields
}

Edge represents an edges in a graph. It contains start and end points.

func NewEdge

func NewEdge[T comparable](source *Vertex[T], dest *Vertex[T], options ...EdgeOptionFunc) *Edge[T]

func (*Edge[T]) Destination added in v0.2.0

func (e *Edge[T]) Destination() *Vertex[T]

Destination returns edge dest vertex

func (*Edge[T]) InsertionIndex added in v0.9.0

func (e *Edge[T]) InsertionIndex() int

InsertionIndex reports the order in which this edge was added to its graph. Edges are numbered from 1 in the order they are stored. On an undirected graph each AddEdge stores the opposite direction first, so the edge AddEdge returns is not always number 1. An edge created with NewEdge and not added to a graph returns 0.

func (*Edge[T]) Label added in v0.9.0

func (e *Edge[T]) Label() string

Label returns the edge label. It returns an empty string when no label was set.

func (Edge[T]) Metadata added in v0.7.0

func (e Edge[T]) Metadata() any

Metadata returns the metadata associated with the edge.

func (*Edge[T]) OtherVertex added in v0.2.0

func (e *Edge[T]) OtherVertex(v T) *Vertex[T]

OtherVertex accepts the label of one the vertices of the edge and returns the other one. If the input label doesn't match to either of the vertices, returns nil.

func (*Edge[T]) Source added in v0.2.0

func (e *Edge[T]) Source() *Vertex[T]

Source returns edge source vertex

func (*Edge[T]) Weight

func (e *Edge[T]) Weight() float64

Weight returns the weight of the edge.

type EdgeChange added in v0.10.0

type EdgeChange[T comparable] struct {
	From, To             T
	OldWeight, NewWeight float64
	OldLabel, NewLabel   string
}

EdgeChange is an edge whose weight or edge label differs between the two graphs.

type EdgeEntry added in v0.10.0

type EdgeEntry[T comparable] struct {
	From, To T
	Weight   float64
	Label    string
}

EdgeEntry is an edge that a GraphDiff adds or removes, with its weight and its edge label.

type EdgeOptionFunc

type EdgeOptionFunc func(properties *EdgeProperties)

EdgeOptionFunc represent an alias of function type that modifies the specified edge properties.

func WithEdgeLabel added in v0.9.0

func WithEdgeLabel(label string) EdgeOptionFunc

WithEdgeLabel sets a label on the edge. An empty label means the edge has no label.

Example
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[string](gograph.Directed())

	edge, _ := g.AddEdge(
		gograph.NewVertex("service"),
		gograph.NewVertex("database"),
		gograph.WithEdgeLabel("queries"),
	)

	fmt.Println(edge.Label())

}
Output:
queries

func WithEdgeWeight

func WithEdgeWeight(weight float64) EdgeOptionFunc

WithEdgeWeight sets the edge weight for the specified edge properties in the returned EdgeOptionFunc.

type EdgeProperties

type EdgeProperties struct {
	// contains filtered or unexported fields
}

EdgeProperties represents the properties of an edge.

type Graph

type Graph[T comparable] interface {
	GraphType

	// AddEdge adds an edge from the vertex with the 'from' label to
	// the vertex with the 'to' label by appending the 'to' vertex to the
	// 'neighbors' slice of the 'from' vertex, in directed graph.
	//
	// In undirected graph, it also adds an edge from the vertex with
	// the 'to' label to the vertex with the 'from' label by appending
	// the 'from' vertex to the 'neighbors' slice of the 'to' vertex. it
	// means that it create the edges in both direction between the specified
	// vertices. A self-loop is stored once.
	//
	// This method accepts additional edge options such as weight and adds
	// them to the new edge.
	//
	// In a graph created with Acyclic, an edge that would close a cycle
	// returns ErrDAGCycle without adding the edge. A graph from New keeps a
	// topological order of its vertices for this check. An edge that agrees
	// with the order, an edge from a vertex with no incoming edges, and an
	// edge to a vertex with no outgoing edges take O(1) time. Any other edge
	// searches forward from the 'to' vertex and backward from the 'from'
	// vertex, and moves the vertices it finds. That takes at most O(R log R)
	// time for the R vertices and edges reachable from the 'to' vertex, and
	// O((V+E) log V) in the worst case. On a graph that Clone copied from
	// another implementation of Graph, the first AddEdge also builds the
	// order in O(V+E) time.
	//
	// It creates the input vertices if they don't exist in the graph, the
	// same way AddVertex does, so a vertex from another graph is copied.
	// If any of the specified vertices is nil, returns nil.
	// If edge already exist, returns error.
	AddEdge(from, to *Vertex[T], options ...EdgeOptionFunc) (*Edge[T], error)

	// GetAllEdges returns a slice of all edges connecting source vertex to
	// target vertex if such vertices exist in this graph.
	//
	// In directed graph, or if both vertices are the same, it returns a single edge.
	//
	// If any of the specified vertices is nil, returns nil.
	// If any of the vertices does not exist, returns nil.
	// If both vertices exist but no edges found, returns nil.
	GetAllEdges(from, to *Vertex[T]) []*Edge[T]

	// AllEdges returns all the edges in the graph. The edges are grouped by
	// source vertex in the order of GetAllVertices, and the edges of each
	// source vertex are in the order they were added. In an undirected
	// graph, an edge between distinct vertices is stored in both directions,
	// so it appears in the group of both its vertices. A self-loop is stored once.
	AllEdges() []*Edge[T]

	// GetEdge returns an edge connecting source vertex to target vertex
	// if such vertices and such edge exist in this graph.
	//
	// In undirected graph, returns only the edge from the "from" vertex to
	// the "to" vertex.
	//
	// If any of the specified vertices is nil, returns nil.
	// If edge does not exist, returns nil.
	GetEdge(from, to *Vertex[T]) *Edge[T]

	// EdgesOf returns a slice of all edges touching the specified vertex.
	// If no edges are touching the specified vertex returns nil.
	//
	// An undirected edge between distinct vertices appears in both directions.
	// Use v.OutDegree() for the number of neighbors in an undirected graph.
	//
	// The edges that start from the vertex come first, in the order they
	// were added. The edges that end at the vertex follow, in the order of
	// their source vertices in GetAllVertices. For a graph from New, it takes
	// O(d log d) time for a vertex with d edges.
	//
	// If the input vertex is nil, returns nil.
	// If the input vertex does not exist, returns nil.
	EdgesOf(v *Vertex[T]) []*Edge[T]

	// RemoveEdges removes input edges from the graph from the specified
	// slice of edges, if they exist. In undirected graph, removes edges
	// in both directions. For a graph from New, removing an edge takes time
	// proportional to the degrees of its two vertices.
	RemoveEdges(edges ...*Edge[T])

	// AddVertexByLabel adds a new vertex with the given label to the graph.
	// Label of the vertex is a comparable type. This method also accepts the
	// vertex properties such as weight.
	//
	// If there is a vertex with the same label in the graph, returns nil.
	// Otherwise, returns the created vertex.
	AddVertexByLabel(label T, options ...VertexOptionFunc) *Vertex[T]

	// AddVertex adds the input vertex to the graph. It doesn't add
	// vertex to the graph if the input vertex label is already exists
	// in the graph.
	//
	// A vertex belongs to one graph. If the input vertex has already been
	// added to a graph, including this one before it was removed, the graph
	// stores a copy with the same label, weight and metadata, and no edges.
	AddVertex(v *Vertex[T])

	// GetVertexByID returns the vertex with the input label.
	//
	// If vertex doesn't exist, returns nil.
	GetVertexByID(label T) *Vertex[T]

	// GetAllVerticesByID returns a slice of vertices with the specified label list.
	//
	// If vertex doesn't exist, doesn't add nil to the output list.
	GetAllVerticesByID(label ...T) []*Vertex[T]

	// GetAllVertices returns a slice of all existing vertices in the graph,
	// in the order they were added. A vertex that is removed and added
	// again moves to the end.
	GetAllVertices() []*Vertex[T]

	// RemoveVertices removes all the specified vertices from this graph including
	// all its touching edges if present. A removed vertex reports no neighbors
	// and an in-degree of 0. A vertex of another graph with the same label as a
	// removed one keeps its edges. For a graph from New, removing a vertex
	// takes time proportional to the degrees of its neighbors.
	RemoveVertices(vertices ...*Vertex[T])

	// ContainsEdge returns 'true' if and only if this graph contains an edge
	// going from the source vertex to the target vertex.
	//
	// If any of the specified vertices does not exist in the graph, or if is nil,
	// returns 'false'.
	ContainsEdge(from, to *Vertex[T]) bool

	// ContainsVertex returns 'true' if this graph contains the specified vertex.
	//
	// If the specified vertex is nil, returns 'false'.
	ContainsVertex(v *Vertex[T]) bool

	// Order returns the number of vertices in the graph.
	Order() uint32

	// Size returns the number of stored edges in the graph. An undirected edge
	// between distinct vertices is stored in both directions and counts twice.
	Size() uint32
}

Graph defines methods for managing a graph with vertices and edges. It is the base interface in the graph hierarchy. Each graph object contains a set of vertices and edges.

Through generics, a graph can be typed to specific classes for vertices' label T.

func Clone added in v0.9.1

func Clone[T comparable](g Graph[T]) Graph[T]

Clone returns an independent copy of g, preserving its directed, weighted, and acyclic properties, vertex and edge weights, labels, edge insertion indexes, and iteration order. Vertices and edges are newly allocated. Metadata is copied as an interface value; reference values stored in metadata still refer to the same data. A nil graph panics. The graph must not be modified while it is being copied.

The copy keeps its vertices, its edges, its neighbor lists and the lists of edges that end at each vertex in one array each, so a vertex or edge of the copy that is still referenced keeps that storage alive.

Time and space are O(V + E).

Example
g := New[string](Directed(), Weighted())
a := g.AddVertexByLabel("a")
b := g.AddVertexByLabel("b")
_, err := g.AddEdge(a, b, WithEdgeWeight(5))
if err != nil {
	panic(err)
}
copy := Clone(g)
copy.RemoveVertices(copy.GetVertexByID("a"))
fmt.Println(g.Order(), copy.Order(), copy.IsDirected(), copy.IsWeighted())
Output:
2 1 true true

func InducedSubgraph added in v0.9.0

func InducedSubgraph[T comparable](g Graph[T], labels ...T) (Graph[T], error)

InducedSubgraph returns a new graph with the given vertices and every edge of g whose endpoints are both in that set.

The result has the same directed, weighted, and acyclic properties as g. Vertex weights, edge weights, edge labels, metadata, and edge insertion indexes are copied. The result does not share vertices or edges with g. Duplicate labels are ignored, and vertex order follows the first occurrence of each label. No labels returns an empty graph with the same properties.

A label that is not in g returns ErrVertexDoesNotExist wrapped with that label, and the returned graph is nil. A nil graph, or a graph that was not created by New, panics.

Time is O(k + D), where k is the number of distinct labels and D is the sum of the out-degrees of those vertices. Extra space beyond the result is O(k).

Example
g := New[string](Directed(), Weighted())
a := g.AddVertexByLabel("api")
b := g.AddVertexByLabel("auth")
c := g.AddVertexByLabel("billing")
_, _ = g.AddEdge(a, b, WithEdgeWeight(1))
_, _ = g.AddEdge(b, c, WithEdgeWeight(2))
_, _ = g.AddEdge(a, c, WithEdgeWeight(4))

sub, err := InducedSubgraph(g, "api", "billing")
if err != nil {
	fmt.Println(err)
	return
}
for _, v := range sub.GetAllVertices() {
	fmt.Println(v.Label())
}
edge := sub.GetEdge(sub.GetVertexByID("api"), sub.GetVertexByID("billing"))
fmt.Println(edge.Weight())
Output:
api
billing
4

func New

func New[T comparable](options ...GraphOptionFunc) Graph[T]

New creates a new instance of base graph that implemented the Graph interface.

Example (Acyclic)
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[int](gograph.Acyclic())

	_, _ = g.AddEdge(gograph.NewVertex(1), gograph.NewVertex(2))
	_, _ = g.AddEdge(gograph.NewVertex(2), gograph.NewVertex(3))

	_, err := g.AddEdge(gograph.NewVertex(3), gograph.NewVertex(1))
	fmt.Println(err)

}
Output:
edges would create cycle
Example (Directed)
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[int](gograph.Directed())

	_, _ = g.AddEdge(gograph.NewVertex(1), gograph.NewVertex(2))
	_, _ = g.AddEdge(gograph.NewVertex(1), gograph.NewVertex(3))
	_, _ = g.AddEdge(gograph.NewVertex(2), gograph.NewVertex(4))
	_, _ = g.AddEdge(gograph.NewVertex(3), gograph.NewVertex(4))
	_, _ = g.AddEdge(gograph.NewVertex(4), gograph.NewVertex(5))
	_, _ = g.AddEdge(gograph.NewVertex(5), gograph.NewVertex(6))

	fmt.Println(g.Order(), g.Size())
	for _, e := range g.AllEdges() {
		fmt.Println(e.Source().Label(), "->", e.Destination().Label())
	}

}
Output:
6 6
1 -> 2
1 -> 3
2 -> 4
3 -> 4
4 -> 5
5 -> 6
Example (Undirected)
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	// Graphs are undirected by default.
	g := gograph.New[string]()

	a := g.AddVertexByLabel("A")
	b := g.AddVertexByLabel("B")
	c := g.AddVertexByLabel("C")
	d := g.AddVertexByLabel("D")

	_, _ = g.AddEdge(a, b)
	_, _ = g.AddEdge(a, d)
	_, _ = g.AddEdge(b, c)
	_, _ = g.AddEdge(b, d)

	// Every undirected edge can be followed both ways.
	fmt.Println(g.ContainsEdge(a, b), g.ContainsEdge(b, a))

}
Output:
true true
Example (Weighted)
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
	"github.com/hmdsefi/gograph/path"
)

func main() {
	g := gograph.New[string](gograph.Weighted())

	a := g.AddVertexByLabel("A")
	b := g.AddVertexByLabel("B")
	c := g.AddVertexByLabel("C")
	d := g.AddVertexByLabel("D")

	_, _ = g.AddEdge(a, b, gograph.WithEdgeWeight(4))
	_, _ = g.AddEdge(a, d, gograph.WithEdgeWeight(3))
	_, _ = g.AddEdge(b, c, gograph.WithEdgeWeight(3))
	_, _ = g.AddEdge(b, d, gograph.WithEdgeWeight(1))
	_, _ = g.AddEdge(c, d, gograph.WithEdgeWeight(2))

	dist := path.Dijkstra(g, "A")
	fmt.Println(dist["C"])

}
Output:
5

type GraphDiff added in v0.10.0

type GraphDiff[T comparable] struct {
	AddedVertices   []VertexEntry[T]  `json:",omitempty"`
	RemovedVertices []VertexEntry[T]  `json:",omitempty"`
	ChangedVertices []VertexChange[T] `json:",omitempty"`

	AddedEdges   []EdgeEntry[T]  `json:",omitempty"`
	RemovedEdges []EdgeEntry[T]  `json:",omitempty"`
	ChangedEdges []EdgeChange[T] `json:",omitempty"`

	// OldProperties and NewProperties are set when the graphs differ in
	// whether they're weighted or acyclic.
	OldProperties, NewProperties *PropertiesSummary `json:",omitempty"`
}

GraphDiff holds the differences that turn one graph into another, as Diff returns them. Its fields are plain exported values, so encoding/json can write and read it, as long as no weight is NaN or infinite.

func Diff added in v0.10.0

func Diff[T comparable](a, b Graph[T], opts ...DiffOption) (*GraphDiff[T], error)

Diff returns the differences that turn graph a into graph b. It matches vertices by label and edges by the labels of their two vertices. It compares the weights of vertices and edges, the edge labels, and whether the graphs are weighted or acyclic. It leaves out metadata, the order of the vertices and edges, and the edge insertion indexes.

Weights are equal when they're == or both NaN, so a graph has no differences from itself. WithWeightTolerance also accepts weights that are close. A removed vertex removes its edges, and RemovedEdges lists those edges too, so the diff is complete on its own.

In an undirected graph, each edge is listed once, in the orientation in which it's found first: in a for a removed edge, and in b for an added or changed one. A - B in a and B - A in b are the same edge. If one graph is directed and the other is undirected, it returns ErrIncompatibleGraphs.

Removed vertices and edges come in the order of a, and added and changed ones in the order of b, so the same graphs always give the same diff.

For graphs from New, it takes O(V+E) time for the vertices and edges of both graphs, and allocates only the diff it returns. Any other Graph is read through GetAllVertices, AllEdges, GetVertexByID and GetEdge, so it also takes the O(V+E) space of the slices they return, and an undirected one a map of O(V) more. It panics if a or b is nil.

Example
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	before := gograph.New[string](gograph.Directed())
	app := before.AddVertexByLabel("app")
	_, _ = before.AddEdge(app, gograph.NewVertex("db"), gograph.WithEdgeLabel("reads"))
	_, _ = before.AddEdge(app, gograph.NewVertex("log"))

	after := gograph.Clone(before)
	after.RemoveVertices(after.GetVertexByID("log"))
	_, _ = after.AddEdge(after.GetVertexByID("app"), gograph.NewVertex("cache"), gograph.WithEdgeWeight(2))
	after.RemoveEdges(after.GetEdge(after.GetVertexByID("app"), after.GetVertexByID("db")))
	_, _ = after.AddEdge(after.GetVertexByID("app"), after.GetVertexByID("db"), gograph.WithEdgeLabel("writes"))

	d, err := gograph.Diff(before, after)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(d)

}
Output:
+ vertex cache
- vertex log
+ edge app -> cache weight 2
- edge app -> log
~ edge app -> db label "reads" -> "writes"

func (*GraphDiff[T]) Apply added in v0.10.0

func (d *GraphDiff[T]) Apply(base Graph[T]) (Graph[T], error)

Apply returns a copy of base with the differences of d made to it, so Diff(a, b) applied to a gives a graph with no differences from b. The copy comes from Clone, so it's a graph from New, and base doesn't change.

It removes the edges, then the vertices, adds the vertices, then the edges, and then sets the changed weights, edge labels and properties. A removed or changed vertex or edge has to be in base, an added one can't be, and an added edge needs both of its vertices. When d changes the properties, base has to have the old ones. The first difference that doesn't fit returns ErrDiffMismatch, wrapped with what it found. New properties that change whether the graph is directed, or that make an undirected graph acyclic, return ErrIncompatibleGraphs. Added edges that close a cycle in an acyclic graph return ErrDAGCycle, and making a graph with a cycle acyclic returns ErrDAGHasCycle. On an error it returns nil. A nil diff returns a copy of base.

Copying base takes O(V+E) time and space. A removed edge or vertex then costs what RemoveEdges or RemoveVertices does, and every other difference O(1). When the result is acyclic and d adds edges or makes it acyclic, one O(V+E) search checks it for a cycle, so a large diff to an acyclic graph costs no more than copying it. It panics if base is nil.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	v1 := gograph.New[string](gograph.Acyclic())
	_, _ = v1.AddEdge(gograph.NewVertex("parse"), gograph.NewVertex("check"))

	v2 := gograph.Clone(v1)
	_, _ = v2.AddEdge(v2.GetVertexByID("check"), gograph.NewVertex("emit"))

	d, _ := gograph.Diff(v1, v2)
	patched, err := d.Apply(v1)
	if err != nil {
		fmt.Println(err)
		return
	}
	rest, _ := gograph.Diff(patched, v2)
	fmt.Println(v1.Order(), patched.Order(), rest.IsEmpty())

	// Applying it again finds the new vertex already there.
	_, err = d.Apply(patched)
	fmt.Println(errors.Is(err, gograph.ErrDiffMismatch))

}
Output:
2 3 true
true

func (*GraphDiff[T]) IsEmpty added in v0.10.0

func (d *GraphDiff[T]) IsEmpty() bool

IsEmpty reports whether d has no differences. A nil diff is empty.

func (*GraphDiff[T]) String added in v0.10.0

func (d *GraphDiff[T]) String() string

String returns the differences one per line: "+" for an added vertex or edge, "-" for a removed one and "~" for a change, as in

~ weighted false -> true
+ vertex D
- vertex B weight 2
~ vertex C weight 1 -> 2
+ edge A -> D label "uses"
- edge A -> B
~ edge A -> C weight 3 -> 5

A changed property comes first, then the added, removed and changed vertices, then the edges in the same order, each group in the order of the diff. An added or removed item shows its weight when it isn't 0, and an edge its label when it isn't empty. An undirected edge shows the orientation of the diff. An empty diff returns "".

type GraphOptionFunc

type GraphOptionFunc func(properties *GraphProperties)

GraphOptionFunc represent an alias of function type that modifies the specified graph properties.

func Acyclic

func Acyclic() GraphOptionFunc

Acyclic returns a GraphOptionFunc that modifies the specified graph properties. It sets the isAcyclic and isDirected to true. Only direct graph can be acyclic.

func Directed

func Directed() GraphOptionFunc

Directed returns a GraphOptionFunc that modifies the specified graph properties. It sets the isDirected to true.

func Weighted

func Weighted() GraphOptionFunc

Weighted returns a GraphOptionFunc that modifies the specified graph properties. It sets the isWeighted to true.

type GraphProperties

type GraphProperties struct {
	// contains filtered or unexported fields
}

GraphProperties represents the properties of a graph.

type GraphType

type GraphType interface {
	// IsDirected returns true if the graph is directed, false otherwise.
	IsDirected() bool

	// IsAcyclic returns true if the graph is acyclic, false otherwise.
	IsAcyclic() bool

	// IsWeighted returns true if the graph is weighted, false otherwise.
	IsWeighted() bool
}

GraphType defines methods to determine the type of graph. A graph can have multiple types. e.g., a directed graph can be a weighted or acyclic.

type PropertiesSummary added in v0.10.0

type PropertiesSummary struct {
	Directed, Weighted, Acyclic bool
}

PropertiesSummary holds the properties a graph reports through GraphType.

type Vertex

type Vertex[T comparable] struct {
	// contains filtered or unexported fields
}

Vertex represents a node or point in a graph

func NewVertex

func NewVertex[T comparable](label T, options ...VertexOptionFunc) *Vertex[T]

NewVertex creates a vertex with the given label and applies the options, such as WithVertexWeight, to it. The vertex doesn't belong to a graph until it's added with AddVertex or AddEdge.

func StableTopologySort added in v0.8.0

func StableTopologySort[T comparable](g Graph[T], compare func(a, b T) int) ([]*Vertex[T], error)

StableTopologySort performs a topological sort of the graph that picks, at every step, the smallest vertex with no incoming edges left, using compare on the labels. compare returns a negative number if a < b, zero if a == b and a positive number if a > b, so cmp.Compare works for string and number labels.

If compare only returns zero for equal labels, the result doesn't depend on the order the vertices and edges were added. As in TopologySort, an edge from A to B puts A before B.

It returns ErrNotDirected if the graph is not directed, and ErrDAGHasCycle if the graph has a cycle. It runs in O((V + E) log V) time.

Example
package main

import (
	"cmp"
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[string](gograph.Acyclic())

	// "lint" and "build" have no dependencies, so either can go first.
	lint := g.AddVertexByLabel("lint")
	build := g.AddVertexByLabel("build")
	test := g.AddVertexByLabel("test")

	_, _ = g.AddEdge(lint, test)
	_, _ = g.AddEdge(build, test)

	labels := func(vertices []*gograph.Vertex[string]) []string {
		out := make([]string, len(vertices))
		for i, v := range vertices {
			out[i] = v.Label()
		}
		return out
	}

	// TopologySort follows the order the vertices were added.
	order, _ := gograph.TopologySort(g)
	fmt.Println(labels(order))

	// With cmp.Compare, the smallest ready label always comes first.
	order, _ = gograph.StableTopologySort(g, cmp.Compare[string])
	fmt.Println(labels(order))

}
Output:
[lint build test]
[build lint test]

func TopologySort

func TopologySort[T comparable](g Graph[T]) ([]*Vertex[T], error)

TopologySort performs a topological sort of the graph using Kahn's algorithm. If the sorted list of vertices does not contain all vertices in the graph, it means there is a cycle in the graph.

The order is stable: it only depends on the order of GetAllVertices and the order the edges were added, so a graph built the same way gives the same result on every run. Use StableTopologySort to choose the order of the vertices that the edges don't order.

It returns ErrNotDirected if the graph is not directed, and ErrDAGHasCycle if it finds a cycle in the graph.

func (*Vertex[T]) AllNeighbors added in v0.10.0

func (v *Vertex[T]) AllNeighbors() iter.Seq[*Vertex[T]]

AllNeighbors returns an iterator over the vertices this vertex has an edge to, in the order of Neighbors. For a vertex of a graph, it yields the graph's own vertices without copying them, so each one is the vertex GetVertexByID returns for its label. In an undirected graph it yields each neighbor once, and a vertex with a self-loop yields itself once. A removed vertex yields nothing.

Ranging over all neighbors takes O(OutDegree) time and allocates nothing. If the graph changes during the loop, the iterator can skip or repeat a neighbor.

Example
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[string](gograph.Directed())
	a := g.AddVertexByLabel("A")
	for _, label := range []string{"B", "C", "D"} {
		_, _ = g.AddEdge(a, g.AddVertexByLabel(label))
	}

	// The loop gets the graph's own vertices, and can stop early.
	for n := range a.AllNeighbors() {
		fmt.Println(n.Label(), n == g.GetVertexByID(n.Label()))
		if n.Label() == "C" {
			break
		}
	}

}
Output:
B true
C true

func (*Vertex[T]) Degree added in v0.2.0

func (v *Vertex[T]) Degree() int

Degree returns the total degree of the vertex which is the sum of in and out degrees. In an undirected graph this counts incoming and outgoing edges separately; use OutDegree for the number of neighbors.

func (*Vertex[T]) HasNeighbor

func (v *Vertex[T]) HasNeighbor(vertex *Vertex[T]) bool

HasNeighbor checks if the input vertex is the neighbor of the current node or not. It returns 'true' if it finds the input in the neighbors. Otherwise, or if the input is nil, returns 'false'.

func (*Vertex[T]) InDegree

func (v *Vertex[T]) InDegree() int

InDegree returns the number of incoming edges to the current vertex.

func (*Vertex[T]) Label

func (v *Vertex[T]) Label() T

Label returns vertex label.

func (*Vertex[T]) Metadata added in v0.7.0

func (v *Vertex[T]) Metadata() any

Metadata returns the metadata associated with the vertex.

func (*Vertex[T]) NeighborByLabel

func (v *Vertex[T]) NeighborByLabel(label T) *Vertex[T]

NeighborByLabel iterates over the neighbor slice and returns the vertex which its label is equal to the input label.

It returns nil if there is no neighbor with that label.

func (*Vertex[T]) Neighbors

func (v *Vertex[T]) Neighbors() []*Vertex[T]

Neighbors returns a copy of neighbor slice. If the caller changed the result slice, it won't impact the graph or the vertex. The returned vertices are snapshots that share their own neighbor lists with the graph, so their Neighbors, OutDegree and HasNeighbor aren't reliable once the graph changes. Use GetVertexByID to read the current neighbors of a vertex, and AllNeighbors to read them without copies.

func (*Vertex[T]) OutDegree

func (v *Vertex[T]) OutDegree() int

OutDegree returns the number of outgoing edges to the current vertex.

func (*Vertex[T]) Weight

func (v *Vertex[T]) Weight() float64

Weight returns the vertex weight.

type VertexChange added in v0.10.0

type VertexChange[T comparable] struct {
	Label                T
	OldWeight, NewWeight float64
}

VertexChange is a vertex whose weight differs between the two graphs.

type VertexEntry added in v0.10.0

type VertexEntry[T comparable] struct {
	Label  T
	Weight float64
}

VertexEntry is a vertex that a GraphDiff adds or removes.

type VertexOptionFunc

type VertexOptionFunc func(properties *VertexProperties)

VertexOptionFunc represent an alias of function type that modifies the specified vertex properties.

func WithVertexWeight

func WithVertexWeight(weight float64) VertexOptionFunc

WithVertexWeight sets the weight for the specified vertex properties in the returned VertexOptionFunc.

Example
package main

import (
	"fmt"

	"github.com/hmdsefi/gograph"
)

func main() {
	g := gograph.New[string](gograph.Directed(), gograph.Weighted())

	a := g.AddVertexByLabel("A", gograph.WithVertexWeight(3))
	b := g.AddVertexByLabel("B", gograph.WithVertexWeight(2))
	c := g.AddVertexByLabel("C", gograph.WithVertexWeight(4))

	_, _ = g.AddEdge(a, b)
	_, _ = g.AddEdge(b, c)

	fmt.Println(a.Weight(), b.Weight(), c.Weight())

}
Output:
3 2 4

type VertexProperties

type VertexProperties struct {
	// contains filtered or unexported fields
}

VertexProperties represents the properties of a vertex.

Directories

Path Synopsis
Package dag answers the everyday questions about dependency graphs: what a vertex depends on, what depends on it, what a change affects, and which vertices can run at the same time.
Package dag answers the everyday questions about dependency graphs: what a vertex depends on, what depends on it, what a change affects, and which vertices can run at the same time.
encoding
dot
Package dot writes graphs in the DOT language of Graphviz, which dot, the other Graphviz layout tools and many viewers and editor plugins read.
Package dot writes graphs in the DOT language of Graphviz, which dot, the other Graphviz layout tools and many viewers and editor plugins read.
mermaid
Package mermaid writes graphs as Mermaid flowcharts, which GitHub, GitLab and many documentation tools render inside Markdown.
Package mermaid writes graphs as Mermaid flowcharts, which GitHub, GitLab and many documentation tools render inside Markdown.
examples
gomodgraph command
Command gomodgraph loads the output of "go mod graph" into a gograph graph and answers a few questions about the module requirements:
Command gomodgraph loads the output of "go mod graph" into a gograph graph and answers a few questions about the module requirements:
Package gographtest helps test code that uses gograph, in the style of net/http/httptest.
Package gographtest helps test code that uses gograph, in the style of net/http/httptest.
Package traverse visits the vertices of a gograph.Graph in breadth-first, depth-first, closest-first, topological and random walk order.
Package traverse visits the vertices of a gograph.Graph in breadth-first, depth-first, closest-first, topological and random walk order.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL