We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Collections, Text and First Iterators step 12 of 21
VecDeque and breadth-first search
Breadth-first search (BFS) visits a graph in layers: first the starting node, then every node one edge away from it, then every node two edges away, and so on. In this exercise you return the order in which BFS visits the nodes.
pub fn bfs_order(adj: Vec<Vec<usize>>, start: usize) -> Vec<usize>
The contract
The graph is an adjacency list. Nodes are numbered 0..adj.len(), and
adj[i] lists the nodes that node i has an edge to. Edges are directed:
1 appearing in adj[0] does not imply that 0 appears in adj[1].
Visit the nodes like this:
- Put
startin a queue and mark it as seen. - Take the node at the front of the queue and append it to the output.
- Go through that node's list in the order it is written. For each neighbour that is in range and not yet seen, mark it as seen and add it to the back of the queue.
- Repeat from step 2 until the queue is empty.
Marking a node when it is added to the queue, rather than when it is taken out, is what makes each node appear at most once. With this rule and the list order fixed, there is exactly one correct output for every input.
These cases are all in the tests:
start >= adj.len(), including an emptyadj: return an emptyVec. Do not panic.- A neighbour index
>= adj.len(): ignore it. Do not panic. - A self-loop (
ilisted inadj[i]), or a node that several other nodes point to: the node still appears once. (The same holds if a list names a neighbour twice.) - A disconnected graph: return only the nodes reachable from
start.
The result is printed as JSON in the form {"value": [0, 1, 2]}.
Use a VecDeque as the queue. The tests only see the output, so they
cannot check which container you used. The sections below explain why a
VecDeque is the right choice and what goes wrong with the two obvious
Vec alternatives.
The compile error
The starter uses a Vec as the queue and calls pop_front, which Vec
does not have:
error[E0599]: no method named `pop_front` found for struct `Vec<usize>` in the current scope
A Vec stores its elements in one contiguous block, starting at the
beginning of the block. Removing the last element with pop is O(1): the
length goes down by one and the value is moved out. Removing the first
element is O(n), because every remaining element has to move down one place
to keep the block starting at the beginning. Vec does offer that operation,
as v.remove(0).
There are two tempting ways to make the starter compile with a Vec:
queue.remove(0)gives the correct BFS order, but each removal moves every element still in the queue. On a graph withVnodes andEedges, a BFS with an O(1) queue does O(V + E) work. Withremove(0)the shifting alone can cost O(V²) in the worst case, for example when the start node has an edge to every other node.queue.pop()compiles and never panics, but it takes the newest node instead of the oldest. The container is now a stack, and the result is not breadth-first. The first test case catches this. Foradj = [[1,2],[3],[3],[]]from node 0, BFS visits[0,1,2,3]. The stack version visits[0,2,3,1]: it pushes 1 and 2, pops 2 first, then pushes and pops 3, and only then pops 1.
The stack version is a depth-first style traversal, but because it marks
nodes when they are pushed it does not always match the order of a
recursive depth-first search (DFS). A recursive DFS of the same graph that
follows each list in order visits [0,1,3,2].
What a VecDeque is
std::collections::VecDeque is a double-ended queue, implemented as a
growable ring buffer. A ring buffer is a single block of memory in which the
first element does not have to sit at the start of the block. The deque
remembers where its first element is and how many elements it has, and the
elements may run off the end of the block and continue at its beginning.
Because the front is just a stored position, removing the first element
does not move anything. Adding or removing at either end is O(1). Adding can
occasionally need to grow the buffer, which copies the elements, so the
push methods are O(1) amortised: averaged over many pushes, the cost per
push is constant.
use std::collections::VecDeque;
let mut q: VecDeque<usize> = VecDeque::new();
q.push_back(1); // q is [1]
q.push_front(0); // q is [0, 1]
q.push_back(2); // q is [0, 1, 2]
assert_eq!(q.front(), Some(&0));
assert_eq!(q.back(), Some(&2));
assert_eq!(q.pop_front(), Some(0)); // q is [1, 2]
assert_eq!(q.pop_back(), Some(2)); // q is [1]
A first-in, first-out queue uses push_back and pop_front. A last-in,
first-out stack uses push_back and pop_back. In the traversal above,
that one choice is the difference between the BFS order and the stack order.
The usual loop is:
while let Some(node) = queue.pop_front() {
// handle `node`
}
pop_front returns Some(node) while the queue has elements and None
once it is empty, so the loop ends exactly when the queue is drained. There
is no separate is_empty check and no unwrap.
A VecDeque is not one slice
Since the elements can wrap around the end of the buffer, a VecDeque
cannot in general give you a single &[T]. as_slices returns two slices:
the part from the first element to the end of the buffer, and the part that
wrapped around to the beginning.
use std::collections::VecDeque;
let mut d: VecDeque<i32> = VecDeque::with_capacity(8);
for i in 0..6 {
d.push_back(i); // buffer positions 0..6 hold 0..6
}
d.pop_front();
d.pop_front();
d.pop_front(); // the first element, 3, is at position 3
d.push_back(100);
d.push_back(101); // positions 6 and 7: the end of the buffer
d.push_back(102); // wraps around to position 0
println!("{:?}", d.as_slices()); // ([3, 4, 5, 100, 101], [102])
The exact split depends on the buffer's capacity and on how std places
elements, neither of which the documentation promises. with_capacity(8)
only guarantees room for at least 8 elements. The output shown is what the
current implementation prints, where the capacity here is exactly 8. Code
should not rely on where the split falls.
iter() hides the split and yields 3, 4, 5, 100, 101, 102 in order. When
you need one contiguous slice, for example to pass the contents to a
function that takes &[T], call make_contiguous(). It rearranges the
elements inside the existing buffer so they no longer wrap, which can cost
O(n), and returns a single &mut [T]. A Vec can always give you a slice
because its elements are always contiguous.
Check your understanding
BFS needs a "have I seen this node?" check. Why use vec![false; n] rather than a HashSet<usize>?
Show answer
Hide answer
In this exercise the nodes are exactly the integers 0..n, so they can be
used directly as indexes. A Vec<bool> stores one byte per node, and
checking or setting seen[node] is a single bounds-checked array access. A
HashSet<usize> would have to hash each node and search a table, and it
needs extra memory per entry. The Vec<bool> access is O(1) every time and
the HashSet lookup is O(1) on average, so for dense integer keys like
these the Vec<bool> is simpler and usually faster.
A HashSet is the better fit when the node identifiers are sparse (a few
nodes with large numbers), or not integers at all, such as strings or
coordinates. Then there is no small range of indexes to allocate a Vec
for, and the set stores only the identifiers you have actually seen.
Iterating over the neighbours
The starter walks each neighbour list by index:
for i in 0..adj[node].len() {
let next = adj[node][i];
Iterating over the list directly says the same thing more simply:
for &next in &adj[node] {
&adj[node] borrows the list, the loop yields a &usize for each entry, and
the &next pattern copies the number out. There is no index variable to get
wrong.
Your solution is also checked with cargo clippy -- -D warnings, which turns
every warning Clippy reports into an error. Clippy's needless_range_loop
lint warns about a for i in 0..v.len() loop whose index is only used to
read v[i]. With Rust 1.98.1 the starter's loop, once the queue is fixed so
it compiles, triggers it (the loop variable i is only used to index adj)
and fails that check. Whether the lint fires depends on the rest of the loop
body: adding the required next < adj.len() test uses adj for something
other than indexing, and the warning goes away. Iterating directly avoids
the question.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.