Skip to content

Commit

Permalink
added iterators for dfs and bfs (#163)
Browse files Browse the repository at this point in the history
* added iterators for dfs and bfs

* fixed exisintg identifier issue

* added iterators for dfs and bfs

* fixed exisintg identifier issue

* add abstract iterator state and fix first iter arg

* iterator refactor

* created iterators src folder, added kruskal

* created iterators src folder, added kruskal

* multi source version for bfs and dfs

* fix

* remove kruskal

* Update Graphs.jl

* remove kruskal from pages

* format

* format 2

* format branch

* improve perf of bfs

* optimize bfs

* use copy of source nodes

* Update bfs.jl

* improve dfs

* Update bfs.jl

* use size unknown

* Update bfs.jl

* Clean up

* Fix typo

* Fix tests and length

* Remove ref

* Fix eltype

* address review comments

* some more comments

* Update bfs.jl

* Update bfs.jl

* Update dfs.jl

* formatting

* Apply suggestions from code review

---------

Co-authored-by: Tortar <[email protected]>
Co-authored-by: Tortar <[email protected]>
Co-authored-by: Guillaume Dalle <[email protected]>
  • Loading branch information
4 people authored May 4, 2024
1 parent 599ef81 commit d183c26
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 0 deletions.
33 changes: 33 additions & 0 deletions docs/src/algorithms/iterators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Iterators

_Graphs.jl_ includes various routines for iterating through graphs.

## Index

```@index
Pages = ["iterators.md"]
```

## Full docs

```@autodocs
Modules = [Graphs]
Pages = [
"iterators/iterators.jl",
"iterators/bfs.jl",
"iterators/dfs.jl",
]
Private = false
```

The following names are internals, not part of the public API:

```@autodocs
Modules = [Graphs]
Pages = [
"iterators/iterators.jl",
"iterators/bfs.jl",
"iterators/dfs.jl",
]
Public = false
```
6 changes: 6 additions & 0 deletions src/Graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ export
dfs_tree,
dfs_parents,

# iterators
DFSIterator,
BFSIterator,

# random
randomwalk,
self_avoiding_walk,
Expand Down Expand Up @@ -499,6 +503,8 @@ include("traversals/dfs.jl")
include("traversals/maxadjvisit.jl")
include("traversals/randomwalks.jl")
include("traversals/diffusion.jl")
include("iterators/bfs.jl")
include("iterators/dfs.jl")
include("traversals/eulerian.jl")
include("traversals/all_simple_paths.jl")
include("connectivity.jl")
Expand Down
109 changes: 109 additions & 0 deletions src/iterators/bfs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
BFSIterator
`BFSIterator` is used to iterate through graph vertices using a breadth-first search.
A source node(s) is optionally supplied as an `Int` or an array-like type that can be
indexed if supplying multiple sources.
# Examples
```julia-repl
julia> g = smallgraph(:house)
{5, 6} undirected simple Int64 graph
julia> for node in BFSIterator(g,3)
display(node)
end
3
1
4
5
2
```
"""
struct BFSIterator{S,G<:AbstractGraph}
graph::G
source::S
function BFSIterator(graph::G, source::S) where {S,G}
if any(node -> !has_vertex(graph, node), source)
error("Some source nodes for the iterator are not in the graph")
end
return new{S,G}(graph, source)
end
end

"""
BFSVertexIteratorState
`BFSVertexIteratorState` is a struct to hold the current state of iteration
in BFS which is needed for the `Base.iterate()` function. A queue is used to
keep track of the vertices which will be visited during BFS. Since the queue
can contains repetitions of already visited nodes, we also keep track of that
in a `BitVector` so that to skip those nodes.
"""
mutable struct BFSVertexIteratorState
visited::BitVector
queue::Vector{Int}
neighbor_idx::Int
n_visited::Int
end

Base.IteratorSize(::BFSIterator) = Base.SizeUnknown()
Base.eltype(::Type{BFSIterator{S,G}}) where {S,G} = eltype(G)

"""
Base.iterate(t::BFSIterator)
First iteration to visit vertices in a graph using breadth-first search.
"""
function Base.iterate(t::BFSIterator{<:Integer})
visited = falses(nv(t.graph))
visited[t.source] = true
return (t.source, BFSVertexIteratorState(visited, [t.source], 1, 1))
end

function Base.iterate(t::BFSIterator{<:AbstractArray})
visited = falses(nv(t.graph))
visited[first(t.source)] = true
state = BFSVertexIteratorState(visited, copy(t.source), 1, 1)
return (first(t.source), state)
end

"""
Base.iterate(t::BFSIterator, state::VertexIteratorState)
Iterator to visit vertices in a graph using breadth-first search.
"""
function Base.iterate(t::BFSIterator, state::BFSVertexIteratorState)
graph, visited, queue = t.graph, state.visited, state.queue
while !isempty(queue)
if state.n_visited == nv(graph)
return nothing
end
# we visit the first node in the queue
node_start = first(queue)
if !visited[node_start]
visited[node_start] = true
state.n_visited += 1
return (node_start, state)
end
# which means we arrive here when the first node was visited.
neigh = outneighbors(graph, node_start)
if state.neighbor_idx <= length(neigh)
node = neigh[state.neighbor_idx]
# we update the idx of the neighbor we will visit,
# if it is already visited, we repeat
state.neighbor_idx += 1
if !visited[node]
push!(queue, node)
state.visited[node] = true
state.n_visited += 1
return (node, state)
end
else
# when the first node and its neighbors are visited
# we remove the first node of the queue
popfirst!(queue)
state.neighbor_idx = 1
end
end
end
98 changes: 98 additions & 0 deletions src/iterators/dfs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
DFSIterator
`DFSIterator` is used to iterate through graph vertices using a depth-first search.
A source node(s) is optionally supplied as an `Int` or an array-like type that can be
indexed if supplying multiple sources.
# Examples
```julia-repl
julia> g = smallgraph(:house)
{5, 6} undirected simple Int64 graph
julia> for node in DFSIterator(g, 3)
display(node)
end
1
2
4
3
5
```
"""
struct DFSIterator{S,G<:AbstractGraph}
graph::G
source::S
function DFSIterator(graph::G, source::S) where {S,G}
if any(node -> !has_vertex(graph, node), source)
error("Some source nodes for the iterator are not in the graph")
end
return new{S,G}(graph, source)
end
end

"""
DFSVertexIteratorState
`DFSVertexIteratorState` is a struct to hold the current state of iteration
in DFS which is needed for the `Base.iterate()` function. A queue is used to
keep track of the vertices which will be visited during DFS. Since the queue
can contains repetitions of already visited nodes, we also keep track of that
in a `BitVector` so that to skip those nodes.
"""
mutable struct DFSVertexIteratorState
visited::BitVector
queue::Vector{Int}
end

Base.IteratorSize(::DFSIterator) = Base.SizeUnknown()
Base.eltype(::Type{DFSIterator{S,G}}) where {S,G} = eltype(G)

"""
Base.iterate(t::DFSIterator)
First iteration to visit vertices in a graph using depth-first search.
"""
function Base.iterate(t::DFSIterator{<:Integer})
visited = falses(nv(t.graph))
visited[t.source] = true
return (t.source, DFSVertexIteratorState(visited, [t.source]))
end

function Base.iterate(t::DFSIterator{<:AbstractArray})
visited = falses(nv(t.graph))
source_rev = reverse(t.source)
visited[last(source_rev)] = true
state = DFSVertexIteratorState(visited, source_rev)
return (last(source_rev), state)
end

"""
Base.iterate(t::DFSIterator, state::VertexIteratorState)
Iterator to visit vertices in a graph using depth-first search.
"""
function Base.iterate(t::DFSIterator, state::DFSVertexIteratorState)
graph, visited, queue = t.graph, state.visited, state.queue
while !isempty(queue)
# we take the last node in the queue
node_start = last(queue)
# we first return it
if !visited[node_start]
visited[node_start] = true
return (node_start, state)
end
# and then we visit a neighbor and push it at the
# end of the queue
for node in outneighbors(graph, node_start)
if !visited[node]
push!(queue, node)
visited[node] = true
return (node, state)
end
end
# we pop the last node in the queue
# when it and all its neighbors were visited
pop!(queue)
end
end
41 changes: 41 additions & 0 deletions test/iterators/bfs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@testset "BFSIterator" begin
g = Graph()
error_exc = ErrorException("Some source nodes for the iterator are not in the graph")
@test_throws error_exc BFSIterator(g, 3)
g = path_graph(7)
add_edge!(g, 6, 3)
add_edge!(g, 3, 1)
add_edge!(g, 4, 7)
g2 = deepcopy(g)
add_vertex!(g2)
add_vertex!(g2)
add_edge!(g2, 8, 9)

for g in testgraphs(g)
nodes_visited = fill(0, nv(g))
for (i, node) in enumerate(BFSIterator(g, 6))
nodes_visited[i] = node
end
@test nodes_visited[1] == 6
@test any(nodes_visited[2] .== [3, 5, 7])
if nodes_visited[2] == 3
@test nodes_visited[3:4] == [5, 7] || nodes_visited[3:4] == [7, 5]
elseif nodes_visited[2] == 5
@test nodes_visited[3:4] == [3, 7] || nodes_visited[3:4] == [7, 3]
else
@test nodes_visited[3:4] == [3, 5] || nodes_visited[3:4] == [5, 3]
end
@test any(nodes_visited[5] .== [1, 2, 4])
if nodes_visited[5] == 1
@test nodes_visited[6:7] == [2, 4] || nodes_visited[6:7] == [4, 2]
elseif nodes_visited[5] == 2
@test nodes_visited[6:7] == [1, 4] || nodes_visited[6:7] == [4, 1]
else
@test nodes_visited[6:7] == [1, 2] || nodes_visited[6:7] == [2, 1]
end
end
nodes_visited = collect(BFSIterator(g2, [1, 6]))
@test nodes_visited == [1, 2, 3, 6, 5, 7, 4]
nodes_visited = collect(BFSIterator(g2, [8, 1, 6]))
@test nodes_visited == [8, 9, 1, 2, 3, 6, 5, 7, 4]
end
41 changes: 41 additions & 0 deletions test/iterators/dfs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@testset "DFSIterator" begin
g = Graph()
error_exc = ErrorException("Some source nodes for the iterator are not in the graph")
@test_throws error_exc DFSIterator(g, 3)
g = path_graph(7)
add_edge!(g, 6, 3)
add_edge!(g, 3, 1)
add_edge!(g, 4, 7)
g2 = deepcopy(g)
add_vertex!(g2)
add_vertex!(g2)
add_edge!(g2, 8, 9)

for g in testgraphs(g)
nodes_visited = fill(0, nv(g))
for (i, node) in enumerate(DFSIterator(g, 6))
nodes_visited[i] = node
end
@test nodes_visited[1:2] == [6, 3]
@test any(nodes_visited[3] .== [1, 4])
if nodes_visited[3] == 1
@test nodes_visited[4] == 2
@test nodes_visited[5] == 4
@test any(nodes_visited[6] .== [5, 7])
if nodes_visited[6] == 5
@test nodes_visited[7] == 7
end
else
@test any(nodes_visited[4] .== [5, 7])
if nodes_visited[4] == 5
@test nodes_visited[5] == 7
end
@test nodes_visited[6] == 1
@test nodes_visited[7] == 2
end
end
nodes_visited = collect(DFSIterator(g2, [1, 6]))
@test nodes_visited == [1, 2, 3, 4, 5, 6, 7]
nodes_visited = collect(DFSIterator(g2, [8, 1, 6]))
@test nodes_visited == [8, 9, 1, 2, 3, 4, 5, 6, 7]
end
2 changes: 2 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ tests = [
"traversals/maxadjvisit",
"traversals/randomwalks",
"traversals/diffusion",
"iterators/bfs",
"iterators/dfs",
"traversals/eulerian",
"traversals/all_simple_paths",
"community/cliques",
Expand Down

0 comments on commit d183c26

Please sign in to comment.