Graph traversal fundamentals and BFS: queue-based implementation, visited sets, shortest paths in unweighted graphs, levels, and real uses like degrees of separation and dependency impact.
Graph traversal means visiting every node reachable from a start node exactly once, using a visited set so cycles do not loop forever. The two basic orders are BFS (breadth-first, uses a queue) and DFS (depth-first, uses a stack or recursion).
BFS visits nodes in layers of distance 0, 1, 2, ... so the first time it reaches a node is via the fewest edges: it finds shortest paths in unweighted graphs. O(V + E) time, O(V) space. Uses: degrees of separation, nearest available item, impact of a change in a dependency graph, web crawling by depth.
A traversal starts at a node and repeatedly explores neighbours. Two things are always needed:
Both BFS and DFS run in O(V + E): each node is enqueued once and each edge examined once (twice for undirected graphs).
A
/ \
B C
/ \ \
D E --- F
queue: [A] visited {A}
pop A -> push B, C queue [B, C] level 1
pop B -> push D, E queue [C, D, E] level 2
pop C -> push F queue [D, E, F] level 2
pop D, E (F already visited), pop F
order: A, B, C, D, E, F
BFS processes all nodes at distance k before any at distance k+1, because the queue is first-in-first-out. So when a node is first discovered, the path that discovered it has the minimum number of edges. Recording each node's parent lets you rebuild the path.
This only holds when every edge has the same cost. For weighted edges use Dijkstra (non-negative weights) or 0-1 BFS for weights of 0 and 1.
| BFS | DFS | |
|---|---|---|
| Frontier | queue | stack / recursion |
| Order | by distance (layers) | goes deep first |
| Shortest path (unweighted) | yes | no |
| Memory | widest layer (can be large) | longest path |
| Typical uses | nearest, levels, shortest hops | cycles, topological sort, components, backtracking |
A microservice catalogue stored "service A calls service B" edges. Before a breaking API change, we needed every service within 3 hops downstream. A BFS from the changed service over the reversed edges, stopping at depth 3, produced the list with hop counts, which drove the rollout order (direct callers first). It ran in milliseconds over about 600 services and 2,500 edges.
Checks the candidate can implement BFS correctly and knows what it guarantees.
Green flags
Red flags
| Representation | Memory | Neighbour iteration | Edge check | Use when |
|---|---|---|---|---|
Adjacency list Dictionary<T, List<T>> |
O(V + E) | O(degree) | O(degree) | sparse graphs (almost all real systems) |
Adjacency matrix bool[V, V] |
O(V²) | O(V) | O(1) | small, dense graphs |
| Edge list | O(E) | O(E) | O(E) | input format, Kruskal |
| Implicit (grid, computed neighbours) | O(1) | computed | computed | mazes, game boards |
Wrong: mark on dequeue
A -> B, C; B -> D; C -> D
D is enqueued twice (from B and C) before either copy is processed
In dense graphs the queue can hold O(E) entries instead of O(V)
Right: mark on enqueue
if visited.Add(neighbour) then queue.Enqueue(neighbour)
HashSet<T>.Add returns false if already present, so one call both checks and marks.
Process the queue one layer at a time to know the distance without storing it per node:
int depth = 0;
while (queue.Count > 0 && depth <= maxDepth)
{
int layerSize = queue.Count;
for (int i = 0; i < layerSize; i++) { var node = queue.Dequeue(); /* ... */ }
depth++;
}
Start with all sources in the queue at distance 0. Each node's distance becomes the distance to its nearest source. Example: distance from every store to the nearest warehouse in one pass, O(V + E), instead of one BFS per store.
Search from both ends and stop when the frontiers meet. With branching factor b and distance d, this explores about 2 × b^(d/2) nodes instead of b^d. For social graphs (b ≈ 200, d = 6) that is the difference between feasible and impossible.
from start: layer1 layer2 layer3
\ meet
from goal : layer1 layer2 layer3 /
BFS memory is the widest layer. In a social graph, layer 3 can contain millions of nodes. Options: depth limits, bidirectional BFS, or a graph database (Neo4j, Azure Cosmos DB Gremlin, SQL Server graph tables, PostgreSQL recursive CTEs) that traverses near the data. A recursive CTE in SQL is effectively BFS by level:
WITH RECURSIVE downstream(service, depth) AS (
SELECT callee, 1 FROM calls WHERE caller = 'billing'
UNION
SELECT c.callee, d.depth + 1
FROM calls c JOIN downstream d ON c.caller = d.service
WHERE d.depth < 3
)
SELECT service, MIN(depth) FROM downstream GROUP BY service;
(SQL Server uses WITH without RECURSIVE, and UNION ALL, so add a depth limit to stop cycles.)
Shortest moves for a knight on a chessboard is a classic BFS on an implicit graph:
static int KnightMoves((int r, int c) from, (int r, int c) to)
{
var moves = new (int, int)[] { (1,2),(2,1),(-1,2),(-2,1),(1,-2),(2,-1),(-1,-2),(-2,-1) };
var dist = new Dictionary<(int, int), int> { [from] = 0 };
var q = new Queue<(int r, int c)>();
q.Enqueue(from);
while (q.Count > 0)
{
var cur = q.Dequeue();
if (cur == to) return dist[cur];
foreach (var (dr, dc) in moves)
{
var next = (cur.r + dr, cur.c + dc);
if (next.Item1 is < 0 or > 7 || next.Item2 is < 0 or > 7 || dist.ContainsKey(next)) continue;
dist[next] = dist[cur] + 1;
q.Enqueue(next);
}
}
return -1;
}
Console.WriteLine(KnightMoves((0, 0), (7, 7))); // 6
public static class Bfs
{
// Shortest path (fewest edges) from start to goal; null if unreachable.
public static List<T>? ShortestPath<T>(IReadOnlyDictionary<T, List<T>> graph, T start, T goal)
where T : notnull
{
var parent = new Dictionary<T, T>();
var visited = new HashSet<T> { start };
var queue = new Queue<T>();
queue.Enqueue(start);
while (queue.Count > 0)
{
var node = queue.Dequeue();
if (EqualityComparer<T>.Default.Equals(node, goal))
return Rebuild(parent, start, goal);
if (!graph.TryGetValue(node, out var neighbours)) continue;
foreach (var next in neighbours)
if (visited.Add(next)) // mark on enqueue
{
parent[next] = node;
queue.Enqueue(next);
}
}
return null;
}
private static List<T> Rebuild<T>(Dictionary<T, T> parent, T start, T goal) where T : notnull
{
var path = new List<T> { goal };
for (var n = goal; !EqualityComparer<T>.Default.Equals(n, start); n = parent[n])
path.Add(parent[n]);
path.Reverse();
return path;
}
// Everything within maxDepth hops, with its distance (impact analysis)
public static Dictionary<T, int> WithinHops<T>(IReadOnlyDictionary<T, List<T>> graph, T start, int maxDepth)
where T : notnull
{
var dist = new Dictionary<T, int> { [start] = 0 };
var queue = new Queue<T>();
queue.Enqueue(start);
while (queue.Count > 0)
{
var node = queue.Dequeue();
if (dist[node] == maxDepth || !graph.TryGetValue(node, out var neighbours)) continue;
foreach (var next in neighbours)
if (dist.TryAdd(next, dist[node] + 1))
queue.Enqueue(next);
}
return dist;
}
}
// Usage: which services are affected within 3 hops if "billing" changes?
// Reverse the "caller -> callee" edges so we walk from billing to its callers.
var callersOf = calls.GroupBy(c => c.Callee).ToDictionary(g => g.Key, g => g.Select(c => c.Caller).ToList());
var impacted = Bfs.WithinHops(callersOf, "billing", 3);
Mistake: Forgetting the visited set.
Why it is wrong: Any cycle (A → B → A) makes the traversal loop forever or grow memory without limit.
Instead: Keep a HashSet<T> (or a distance dictionary) and only enqueue unseen nodes.
Mistake: Marking nodes visited when they are dequeued rather than enqueued.
Why it is wrong: The same node can be enqueued many times from different neighbours, so the queue can hold O(E) entries instead of O(V).
Instead: Mark on enqueue with if (visited.Add(next)) queue.Enqueue(next).
Mistake: Using BFS for shortest paths on weighted graphs. Why it is wrong: BFS minimises the number of edges, not the total weight; a 2-edge path costing 100 beats a 3-edge path costing 3. Instead: Use Dijkstra for non-negative weights, or 0-1 BFS when weights are only 0 and 1.
Mistake: Using DFS when the shortest or nearest result is needed. Why it is wrong: DFS finds a path, often a long one. Instead: Use BFS for fewest hops.
Mistake: Walking edges in the wrong direction for impact analysis. Why it is wrong: "What does A depend on" and "what depends on A" are opposite questions with different answers. Instead: Build a reversed adjacency list for the "who is affected" question.
Mistake: Running an unbounded BFS over a huge graph in application memory. Why it is wrong: The frontier can hold millions of nodes and exhaust memory or stall the GC. Instead: Add depth limits, use bidirectional BFS, page through the graph, or traverse in the database (recursive CTE or a graph store).
A senior engineer sees graphs everywhere: service call maps, package dependencies, permissions, org charts and data lineage. BFS is the default tool for "how far" and "what is within N hops" questions, and they choose the edge direction deliberately, because impact analysis walks reverse edges.
| Situation | Choice | Why |
|---|---|---|
| Fewest hops, graph fits in memory | BFS with adjacency list | O(V + E), simple and exact |
| Distance to the nearest of many sources | Multi-source BFS | One pass instead of one BFS per source |
| Point-to-point on a huge graph (social) | Bidirectional BFS | Explores about 2 × b^(d/2) nodes instead of b^d |
| Edges have cost (latency, price) | Dijkstra or A* | BFS ignores weights |
| Graph lives in a database and is large | Recursive CTE or graph database | Traverses near the data, avoids loading everything |
| Small, dense graph with frequent edge checks | Adjacency matrix | O(1) edge lookup, O(V²) memory |
They are careful about scale: a BFS over 600 services is trivial, but a BFS over a social graph is not. They also make traversals safe: depth limits on anything driven by user input, and cycle-safe SQL (UNION ALL plus a depth column in SQL Server, which requires UNION ALL in recursive CTEs).
DFS goes as deep as possible along one branch before backtracking. It uses recursion or an explicit stack, plus a visited set. O(V + E) time, O(V) space (the longest path).
Key uses:
A graph is a set of vertices (nodes) connected by edges. Edges can be directed or undirected, weighted or not, and the graph may contain cycles. Trees are a special case (connected, no cycles).
Representations:
Dictionary<TNode, List<TNode>>): O(V + E) memory, best for sparse graphs (most real ones).bool[V,V]): O(V²) memory, O(1) edge check, for small dense graphs.Backend graphs: service and package dependencies, DI containers, build systems (project references), workflow/job DAGs, permission inheritance, routing, social links.
For dependencies you need a DAG: find an execution order with topological sort (Kahn's algorithm, O(V + E)); if not all nodes can be ordered, there is a cycle.
A greedy algorithm builds a solution step by step, always taking the locally best choice and never revisiting it. It is fast and simple, but only optimal when the problem has the greedy-choice property (a locally best choice is part of some optimal solution) and optimal substructure.
In production, greedy is often a good-enough heuristic (bin packing, load balancing) as long as you know it is approximate.
Last updated 23 Oct 2026.