diff --git a/docs/src/algorithms/iterators.md b/docs/src/algorithms/iterators.md new file mode 100644 index 000000000..4e0736562 --- /dev/null +++ b/docs/src/algorithms/iterators.md @@ -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 +``` diff --git a/src/Graphs.jl b/src/Graphs.jl index f5c548b31..86bc0946a 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -186,6 +186,10 @@ export dfs_tree, dfs_parents, + # iterators + DFSIterator, + BFSIterator, + # random randomwalk, self_avoiding_walk, @@ -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") diff --git a/src/iterators/bfs.jl b/src/iterators/bfs.jl new file mode 100644 index 000000000..57779243b --- /dev/null +++ b/src/iterators/bfs.jl @@ -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 diff --git a/src/iterators/dfs.jl b/src/iterators/dfs.jl new file mode 100644 index 000000000..44858716d --- /dev/null +++ b/src/iterators/dfs.jl @@ -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 diff --git a/test/iterators/bfs.jl b/test/iterators/bfs.jl new file mode 100644 index 000000000..c70bdf364 --- /dev/null +++ b/test/iterators/bfs.jl @@ -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 diff --git a/test/iterators/dfs.jl b/test/iterators/dfs.jl new file mode 100644 index 000000000..400767a31 --- /dev/null +++ b/test/iterators/dfs.jl @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 1764fe537..83644faff 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -108,6 +108,8 @@ tests = [ "traversals/maxadjvisit", "traversals/randomwalks", "traversals/diffusion", + "iterators/bfs", + "iterators/dfs", "traversals/eulerian", "traversals/all_simple_paths", "community/cliques",