📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is graph traversal? Explain breadth-first search (BFS) with a C# implementation, why it finds the shortest path in an unweighted graph, and where you would use it in a real system.

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.

Intermediate Coding Intermediate algorithms bfs graph-traversal graphs shortest-path

🎯 Key points

  • Graph traversal visits every node reachable from a start node once, using a frontier and a visited set; it works on trees, cyclic graphs and, by restarting from unvisited nodes, disconnected graphs
  • BFS uses a FIFO queue and explores nodes layer by layer (distance 0, 1, 2, ...)
  • In an unweighted graph the first time BFS reaches a node is by the fewest edges, so it finds shortest paths by hop count
  • Mark nodes visited when you enqueue them (`if (visited.Add(next)) queue.Enqueue(next)`) and keep a parent map to rebuild the path
  • Time is O(V + E) with an adjacency list; an adjacency matrix makes it O(V²), which only suits small dense graphs
  • Memory is the widest layer, which can be millions of nodes in social graphs; use depth limits, bidirectional BFS or traverse in the database
  • Weighted shortest paths need Dijkstra (non-negative weights), 0-1 BFS (weights 0 or 1) or Bellman-Ford (negative weights)
  • Real uses: impact analysis over reversed dependency edges, degrees of separation, nearest resource, depth-limited crawling, multi-source distance; recursive CTEs do BFS-style traversal in SQL

⏱ 30-second answer

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.

🗣 Full answer (senior)

Graph traversal basics

A traversal starts at a node and repeatedly explores neighbours. Two things are always needed:

  • A frontier: the nodes discovered but not yet expanded (queue for BFS, stack for DFS).
  • A visited set: so each node is processed once, which is essential when graphs have cycles.

Both BFS and DFS run in O(V + E): each node is enqueued once and each edge examined once (twice for undirected graphs).

BFS step by step

        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

Why BFS gives shortest paths (unweighted)

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 versus DFS at a glance

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

Real uses

  • Degrees of separation in social or org graphs ("who can introduce me?").
  • Impact analysis: which services or packages are affected within 2 hops of a change.
  • Nearest match: closest warehouse in a grid or road graph with uniform cost.
  • Crawlers with a depth limit.
  • Flood fill and grid problems (minimum moves).
  • Permission inheritance levels (user → group → parent group).

Production example

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.

🔍 What the interviewer is checking

Checks the candidate can implement BFS correctly and knows what it guarantees.

Green flags

  • Uses a queue and a visited set, marking on enqueue.
  • Explains layer-by-layer order and why it gives fewest-edge paths.
  • Rebuilds the path with a parent map.
  • States O(V + E) time and O(V) space.
  • Gives real uses (impact analysis, degrees of separation) and knows when to use Dijkstra instead.

Red flags

  • No visited set, or confuses queue and stack.
  • Claims BFS finds shortest weighted paths.
  • Cannot rebuild the actual path.
  • Has no idea where graphs appear in backend systems.

📚 Learn in depth

1. Representing the graph

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

2. Mark visited when you enqueue, not when you dequeue

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.

3. Levels and depth limits

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++;
}

4. Multi-source BFS

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.

5. Bidirectional BFS

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 /

6. Memory and big graphs

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.)

7. Try it yourself

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

💻 Code

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);

⚠ Common mistakes

  • 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).

🏛 Senior insight: trade-offs

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).

🔗 Follow-ups the interviewer may ask

  1. When: Candidate compares BFS with DFS Why: Checks DFS, cycle detection and topological order
    How to answer

    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:

    • Cycle detection in directed graphs with three states: unvisited, in progress (on the current path), done. Reaching an in-progress node means a back edge, so a cycle.
    • Topological sort: add each node to a list after all its dependencies finish (post-order); that list is a valid build order.
    • Connected components, path existence, backtracking (permutations, sudoku).
  2. When: Candidate builds an adjacency list Why: Checks graph representations and real backend uses
    How to answer

    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:

    • Adjacency list (Dictionary<TNode, List<TNode>>): O(V + E) memory, best for sparse graphs (most real ones).
    • Adjacency matrix (bool[V,V]): O(V²) memory, O(1) edge check, for small dense graphs.
    • Edge list: simple, good for storage and some algorithms.

    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.

  3. When: Interviewer adds weights to the edges Why: Checks they switch to Dijkstra and know why it is greedy
    How to answer

    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.

    • Optimal: interval scheduling by earliest end time, Dijkstra (non-negative weights), Huffman coding, Kruskal/Prim, fractional knapsack.
    • Not optimal: coins {1, 3, 4} for 6, 0/1 knapsack, travelling salesman.

    In production, greedy is often a good-enough heuristic (bin packing, load balancing) as long as you know it is approximate.

↩ Usually asked after

More in Algorithms

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me