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
Walk a graph breadth-first from a starting node.
pub fn bfs_order(adj: Vec<Vec<usize>>, start: usize) -> Vec<usize>
adj[i] lists the neighbours of node i. Return the nodes in the order BFS
visits them: start first, then all of its neighbours in the order they
appear in adj[start], then their unvisited neighbours, and so on. Each node
appears once.
Edge cases that are all real test cases:
-
startout of range → emptyVec, not a panic. - a neighbour index out of range → skip it, no panic.
- a self-loop → visited once.
-
a disconnected graph → only the component containing
start.
You must use a VecDeque. The harness cannot see which container you
used, so this is on your honour — but read on for why it matters, because the
alternative is both slower and, in the shape most people write, a different
algorithm.
The compile error
The starter uses a Vec as the frontier and calls pop_front:
error[E0599]: no method named `pop_front` found for struct `Vec<usize>`
in the current scope
There is no Vec::pop_front, and its absence is deliberate. A Vec is one
contiguous block with the live elements at the front. pop from the back is
O(1) — decrement the length. Removing from the front means shifting every
remaining element down one, which is O(n). std will not give you a one-word
name for an O(n) operation on a type where you would reasonably expect O(1),
so if you want it you must spell it v.remove(0) and own the decision.
Write a BFS with v.remove(0) and you get the right answer in O(V·E) time
instead of O(V+E). Write it with v.pop() — the O(1) one — and you get a
DFS, silently, with no warning at all. That is what the “BFS and DFS
differ” test case is for: adj = [[1,2],[3],[3],[]] visits [0,1,2,3]
breadth-first and [0,2,3,1] depth-first. Both are respectable traversals.
Only one is the one you asked for.
What a VecDeque is
A ring buffer: one allocation, plus a head index and a length. Pushing or popping at either end is O(1) amortised, because the “front” is wherever the head index currently points rather than the start of the buffer.
use std::collections::VecDeque;
let mut q = VecDeque::new();
q.push_back(x); q.push_front(x);
q.pop_front(); q.pop_back();
q.front(); q.back();
A queue is push_back + pop_front. A stack is push_back + pop_back.
Swapping one word between those two lines is the entire difference between
BFS and DFS, which is worth pausing on: the algorithm lives in the data
structure, not in the loop.
while let Some(node) = queue.pop_front() is the idiomatic loop. It is one
operation instead of “check is_empty, then pop, then unwrap”, and it makes
the “the queue drained” case impossible to forget.
The price of the ring: it is not a slice
Because the elements wrap around the end of the buffer, a VecDeque cannot
hand you a &[T]. It gives you two of them:
let mut d: VecDeque<i32> = VecDeque::with_capacity(8);
for i in 0..6 { d.push_back(i); }
d.pop_front(); d.pop_front(); d.pop_front();
d.push_back(100); d.push_back(101); d.push_back(102);
d.as_slices() // ([3, 4, 5, 100, 101], [102])
There is the ring, visible: five elements to the end of the buffer, then one
that wrapped around to the beginning. iter() hides the seam and yields
[3, 4, 5, 100, 101, 102] as you would hope, but anything that genuinely
needs one contiguous slice — passing to a C function, a memcpy, a
&[u8] write — has to call make_contiguous(), which rotates the elements
in place (O(n), once) and then returns a single &mut [T].
This is a good, small lesson about abstraction: a type cannot always give you
a view its representation does not have. Vec can hand out a slice because
it is one. VecDeque cannot, and rather than lie or allocate behind your
back it makes the seam part of the API.
::: question BFS needs a “have I seen this node?” check. Why vec![false; n] rather than a HashSet<usize>?
Because the keys are dense small integers, and a Vec<bool> indexed by node
is one byte per node with an O(1) array access and perfect cache behaviour —
no hashing, no probing, no pointer chasing. For n nodes numbered 0..n,
which is exactly what an adjacency-list-as-Vec<Vec<usize>> gives you, it is
the right structure.
A HashSet earns its place when the node identifiers are sparse, large, or
not integers at all — strings, UUIDs, coordinates. Then you cannot index an
array by them without a mapping step, and the hash set is the mapping step.
This is the same judgement the container-choice article in this track is about: pick by the shape of the keys, not by the name of the operation. :::
One clippy note
The starter also walks the neighbour list by index:
for i in 0..adj[node].len() {
let next = adj[node][i];
needless_range_loop exists for that shape. for &next in &adj[node] is
shorter, has no bounds check per iteration, and cannot go out of range at
all. Indexing in a loop is almost always a for over the collection wearing
a disguise.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.