Skip to content

Commit d183c26

Browse files
kylebeggsTortargdalle
authored
added iterators for dfs and bfs (#163)
* 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]>
1 parent 599ef81 commit d183c26

File tree

7 files changed

+330
-0
lines changed

7 files changed

+330
-0
lines changed

docs/src/algorithms/iterators.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Iterators
2+
3+
_Graphs.jl_ includes various routines for iterating through graphs.
4+
5+
## Index
6+
7+
```@index
8+
Pages = ["iterators.md"]
9+
```
10+
11+
## Full docs
12+
13+
```@autodocs
14+
Modules = [Graphs]
15+
Pages = [
16+
"iterators/iterators.jl",
17+
"iterators/bfs.jl",
18+
"iterators/dfs.jl",
19+
]
20+
Private = false
21+
```
22+
23+
The following names are internals, not part of the public API:
24+
25+
```@autodocs
26+
Modules = [Graphs]
27+
Pages = [
28+
"iterators/iterators.jl",
29+
"iterators/bfs.jl",
30+
"iterators/dfs.jl",
31+
]
32+
Public = false
33+
```

src/Graphs.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ export
186186
dfs_tree,
187187
dfs_parents,
188188

189+
# iterators
190+
DFSIterator,
191+
BFSIterator,
192+
189193
# random
190194
randomwalk,
191195
self_avoiding_walk,
@@ -499,6 +503,8 @@ include("traversals/dfs.jl")
499503
include("traversals/maxadjvisit.jl")
500504
include("traversals/randomwalks.jl")
501505
include("traversals/diffusion.jl")
506+
include("iterators/bfs.jl")
507+
include("iterators/dfs.jl")
502508
include("traversals/eulerian.jl")
503509
include("traversals/all_simple_paths.jl")
504510
include("connectivity.jl")

src/iterators/bfs.jl

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
BFSIterator
3+
4+
`BFSIterator` is used to iterate through graph vertices using a breadth-first search.
5+
A source node(s) is optionally supplied as an `Int` or an array-like type that can be
6+
indexed if supplying multiple sources.
7+
8+
# Examples
9+
```julia-repl
10+
julia> g = smallgraph(:house)
11+
{5, 6} undirected simple Int64 graph
12+
13+
julia> for node in BFSIterator(g,3)
14+
display(node)
15+
end
16+
3
17+
1
18+
4
19+
5
20+
2
21+
```
22+
"""
23+
struct BFSIterator{S,G<:AbstractGraph}
24+
graph::G
25+
source::S
26+
function BFSIterator(graph::G, source::S) where {S,G}
27+
if any(node -> !has_vertex(graph, node), source)
28+
error("Some source nodes for the iterator are not in the graph")
29+
end
30+
return new{S,G}(graph, source)
31+
end
32+
end
33+
34+
"""
35+
BFSVertexIteratorState
36+
37+
`BFSVertexIteratorState` is a struct to hold the current state of iteration
38+
in BFS which is needed for the `Base.iterate()` function. A queue is used to
39+
keep track of the vertices which will be visited during BFS. Since the queue
40+
can contains repetitions of already visited nodes, we also keep track of that
41+
in a `BitVector` so that to skip those nodes.
42+
"""
43+
mutable struct BFSVertexIteratorState
44+
visited::BitVector
45+
queue::Vector{Int}
46+
neighbor_idx::Int
47+
n_visited::Int
48+
end
49+
50+
Base.IteratorSize(::BFSIterator) = Base.SizeUnknown()
51+
Base.eltype(::Type{BFSIterator{S,G}}) where {S,G} = eltype(G)
52+
53+
"""
54+
Base.iterate(t::BFSIterator)
55+
56+
First iteration to visit vertices in a graph using breadth-first search.
57+
"""
58+
function Base.iterate(t::BFSIterator{<:Integer})
59+
visited = falses(nv(t.graph))
60+
visited[t.source] = true
61+
return (t.source, BFSVertexIteratorState(visited, [t.source], 1, 1))
62+
end
63+
64+
function Base.iterate(t::BFSIterator{<:AbstractArray})
65+
visited = falses(nv(t.graph))
66+
visited[first(t.source)] = true
67+
state = BFSVertexIteratorState(visited, copy(t.source), 1, 1)
68+
return (first(t.source), state)
69+
end
70+
71+
"""
72+
Base.iterate(t::BFSIterator, state::VertexIteratorState)
73+
74+
Iterator to visit vertices in a graph using breadth-first search.
75+
"""
76+
function Base.iterate(t::BFSIterator, state::BFSVertexIteratorState)
77+
graph, visited, queue = t.graph, state.visited, state.queue
78+
while !isempty(queue)
79+
if state.n_visited == nv(graph)
80+
return nothing
81+
end
82+
# we visit the first node in the queue
83+
node_start = first(queue)
84+
if !visited[node_start]
85+
visited[node_start] = true
86+
state.n_visited += 1
87+
return (node_start, state)
88+
end
89+
# which means we arrive here when the first node was visited.
90+
neigh = outneighbors(graph, node_start)
91+
if state.neighbor_idx <= length(neigh)
92+
node = neigh[state.neighbor_idx]
93+
# we update the idx of the neighbor we will visit,
94+
# if it is already visited, we repeat
95+
state.neighbor_idx += 1
96+
if !visited[node]
97+
push!(queue, node)
98+
state.visited[node] = true
99+
state.n_visited += 1
100+
return (node, state)
101+
end
102+
else
103+
# when the first node and its neighbors are visited
104+
# we remove the first node of the queue
105+
popfirst!(queue)
106+
state.neighbor_idx = 1
107+
end
108+
end
109+
end

src/iterators/dfs.jl

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
DFSIterator
3+
4+
`DFSIterator` is used to iterate through graph vertices using a depth-first search.
5+
A source node(s) is optionally supplied as an `Int` or an array-like type that can be
6+
indexed if supplying multiple sources.
7+
8+
# Examples
9+
```julia-repl
10+
julia> g = smallgraph(:house)
11+
{5, 6} undirected simple Int64 graph
12+
13+
julia> for node in DFSIterator(g, 3)
14+
display(node)
15+
end
16+
1
17+
2
18+
4
19+
3
20+
5
21+
```
22+
"""
23+
struct DFSIterator{S,G<:AbstractGraph}
24+
graph::G
25+
source::S
26+
function DFSIterator(graph::G, source::S) where {S,G}
27+
if any(node -> !has_vertex(graph, node), source)
28+
error("Some source nodes for the iterator are not in the graph")
29+
end
30+
return new{S,G}(graph, source)
31+
end
32+
end
33+
34+
"""
35+
DFSVertexIteratorState
36+
37+
`DFSVertexIteratorState` is a struct to hold the current state of iteration
38+
in DFS which is needed for the `Base.iterate()` function. A queue is used to
39+
keep track of the vertices which will be visited during DFS. Since the queue
40+
can contains repetitions of already visited nodes, we also keep track of that
41+
in a `BitVector` so that to skip those nodes.
42+
"""
43+
mutable struct DFSVertexIteratorState
44+
visited::BitVector
45+
queue::Vector{Int}
46+
end
47+
48+
Base.IteratorSize(::DFSIterator) = Base.SizeUnknown()
49+
Base.eltype(::Type{DFSIterator{S,G}}) where {S,G} = eltype(G)
50+
51+
"""
52+
Base.iterate(t::DFSIterator)
53+
54+
First iteration to visit vertices in a graph using depth-first search.
55+
"""
56+
function Base.iterate(t::DFSIterator{<:Integer})
57+
visited = falses(nv(t.graph))
58+
visited[t.source] = true
59+
return (t.source, DFSVertexIteratorState(visited, [t.source]))
60+
end
61+
62+
function Base.iterate(t::DFSIterator{<:AbstractArray})
63+
visited = falses(nv(t.graph))
64+
source_rev = reverse(t.source)
65+
visited[last(source_rev)] = true
66+
state = DFSVertexIteratorState(visited, source_rev)
67+
return (last(source_rev), state)
68+
end
69+
70+
"""
71+
Base.iterate(t::DFSIterator, state::VertexIteratorState)
72+
73+
Iterator to visit vertices in a graph using depth-first search.
74+
"""
75+
function Base.iterate(t::DFSIterator, state::DFSVertexIteratorState)
76+
graph, visited, queue = t.graph, state.visited, state.queue
77+
while !isempty(queue)
78+
# we take the last node in the queue
79+
node_start = last(queue)
80+
# we first return it
81+
if !visited[node_start]
82+
visited[node_start] = true
83+
return (node_start, state)
84+
end
85+
# and then we visit a neighbor and push it at the
86+
# end of the queue
87+
for node in outneighbors(graph, node_start)
88+
if !visited[node]
89+
push!(queue, node)
90+
visited[node] = true
91+
return (node, state)
92+
end
93+
end
94+
# we pop the last node in the queue
95+
# when it and all its neighbors were visited
96+
pop!(queue)
97+
end
98+
end

test/iterators/bfs.jl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
@testset "BFSIterator" begin
2+
g = Graph()
3+
error_exc = ErrorException("Some source nodes for the iterator are not in the graph")
4+
@test_throws error_exc BFSIterator(g, 3)
5+
g = path_graph(7)
6+
add_edge!(g, 6, 3)
7+
add_edge!(g, 3, 1)
8+
add_edge!(g, 4, 7)
9+
g2 = deepcopy(g)
10+
add_vertex!(g2)
11+
add_vertex!(g2)
12+
add_edge!(g2, 8, 9)
13+
14+
for g in testgraphs(g)
15+
nodes_visited = fill(0, nv(g))
16+
for (i, node) in enumerate(BFSIterator(g, 6))
17+
nodes_visited[i] = node
18+
end
19+
@test nodes_visited[1] == 6
20+
@test any(nodes_visited[2] .== [3, 5, 7])
21+
if nodes_visited[2] == 3
22+
@test nodes_visited[3:4] == [5, 7] || nodes_visited[3:4] == [7, 5]
23+
elseif nodes_visited[2] == 5
24+
@test nodes_visited[3:4] == [3, 7] || nodes_visited[3:4] == [7, 3]
25+
else
26+
@test nodes_visited[3:4] == [3, 5] || nodes_visited[3:4] == [5, 3]
27+
end
28+
@test any(nodes_visited[5] .== [1, 2, 4])
29+
if nodes_visited[5] == 1
30+
@test nodes_visited[6:7] == [2, 4] || nodes_visited[6:7] == [4, 2]
31+
elseif nodes_visited[5] == 2
32+
@test nodes_visited[6:7] == [1, 4] || nodes_visited[6:7] == [4, 1]
33+
else
34+
@test nodes_visited[6:7] == [1, 2] || nodes_visited[6:7] == [2, 1]
35+
end
36+
end
37+
nodes_visited = collect(BFSIterator(g2, [1, 6]))
38+
@test nodes_visited == [1, 2, 3, 6, 5, 7, 4]
39+
nodes_visited = collect(BFSIterator(g2, [8, 1, 6]))
40+
@test nodes_visited == [8, 9, 1, 2, 3, 6, 5, 7, 4]
41+
end

test/iterators/dfs.jl

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
@testset "DFSIterator" begin
2+
g = Graph()
3+
error_exc = ErrorException("Some source nodes for the iterator are not in the graph")
4+
@test_throws error_exc DFSIterator(g, 3)
5+
g = path_graph(7)
6+
add_edge!(g, 6, 3)
7+
add_edge!(g, 3, 1)
8+
add_edge!(g, 4, 7)
9+
g2 = deepcopy(g)
10+
add_vertex!(g2)
11+
add_vertex!(g2)
12+
add_edge!(g2, 8, 9)
13+
14+
for g in testgraphs(g)
15+
nodes_visited = fill(0, nv(g))
16+
for (i, node) in enumerate(DFSIterator(g, 6))
17+
nodes_visited[i] = node
18+
end
19+
@test nodes_visited[1:2] == [6, 3]
20+
@test any(nodes_visited[3] .== [1, 4])
21+
if nodes_visited[3] == 1
22+
@test nodes_visited[4] == 2
23+
@test nodes_visited[5] == 4
24+
@test any(nodes_visited[6] .== [5, 7])
25+
if nodes_visited[6] == 5
26+
@test nodes_visited[7] == 7
27+
end
28+
else
29+
@test any(nodes_visited[4] .== [5, 7])
30+
if nodes_visited[4] == 5
31+
@test nodes_visited[5] == 7
32+
end
33+
@test nodes_visited[6] == 1
34+
@test nodes_visited[7] == 2
35+
end
36+
end
37+
nodes_visited = collect(DFSIterator(g2, [1, 6]))
38+
@test nodes_visited == [1, 2, 3, 4, 5, 6, 7]
39+
nodes_visited = collect(DFSIterator(g2, [8, 1, 6]))
40+
@test nodes_visited == [8, 9, 1, 2, 3, 4, 5, 6, 7]
41+
end

test/runtests.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ tests = [
108108
"traversals/maxadjvisit",
109109
"traversals/randomwalks",
110110
"traversals/diffusion",
111+
"iterators/bfs",
112+
"iterators/dfs",
111113
"traversals/eulerian",
112114
"traversals/all_simple_paths",
113115
"community/cliques",

0 commit comments

Comments
 (0)