LearnThatStack Ace your next interview

Graphs

Code in Python unless a row names the language Checked Oct 2026 23 code snippets

The model

  • A graph is nodes joined by edges. Whether the edges are directed, weighted or can form cycles decides the algorithm.
  • Every search runs one loop. Pop a node off the frontier, then push its unvisited neighbors. The frontier's type sets the order.
  • A visited mark stops loops on cycles. With it, BFS and DFS cost O(V + E), for V nodes and E edges.
  • Default to an adjacency list. It takes O(V + E) space. A matrix takes O(V^2), even when nearly empty.
  • With no weights, use BFS. With non-negative weights, use Dijkstra. With any negative edge, use Bellman-Ford.
Frontierqueue, stack or heapCurrent node ubeing expandedNeighborseach v in adj[u]Visited seteach node oncepop uread adj[u]seen v before?mark, push if new
A queue pops the oldest node, so BFS spreads out by hops. A stack pops the newest, so DFS goes deep. A min-heap pops the nearest node, and that's Dijkstra.

Graph terms & modeling

name the parts, and turn a problem into a graph

Paths, cycles and connected graphs

Define a pathA chain of nodes, each joined to the next. Its length counts the edges.
Define a cycleA path back to its start that reuses no edge.
Define a connected graphEvery pair of nodes has a path. If not, it splits into components.

TipReachability asks whether a path exists. Deadlock asks whether a cycle exists.

Directed, undirected and weighted

Model a two-way linkAn undirected edge, for friendship, two-way roads and cables.
Model a one-way linkA directed edge, for follows, web links and dependencies.
Put a cost on each edgeA weighted edge. The weight is miles, minutes, latency or price.

TipIn a directed graph, a path from A to B tells you nothing about a path back.

TrapModel "depends on" as undirected and nothing errors. Topological sort and cycle checks just stop making sense.

Model a problem as a graph

Model a grid or mazedirs = [(1, 0), (-1, 0), (0, 1), (0, -1)]Each cell is a node. Work out its neighbors as you go, and skip walls.
Model a social networkAn adjacency list. Undirected for friendship, directed for follows.
Suggest friends of friendsBFS to depth two, ranked by shared neighbors.

TipRoads, friendships, web links and task schedules are all graphs. So one search answers "can A reach B?" on all of them.

TrapA celebrity node can hold millions of edges. Cap the fan-out on hubs, or one query stalls the system.

Self-loops and parallel edges

Handle an edge from a node to itselfA self-loop is a cycle. In an undirected graph it adds 2 to the degree.
Store two edges between the same pairA list keeps both. A matrix cell holds one unless you store counts.
Detect cycles when parallel edges existTrack the edge you came in on by its id, not by its parent node.

TrapIn an undirected graph, two parallel edges form a two-node cycle. A plain parent-skip check misses it.

Storing a graph

adjacency list, matrix or flat arrays

Adjacency list or matrix

adj = {0: [1, 2], 1: [2], 2: []}list, directedadj[u].append(v); adj[v].append(u)undirected, both waysm = [[0] * n for _ in range(n)]matrix, n by nm[u][v] = 1edge from u to v
Pick the defaultAn adjacency list. Most real graphs are sparse.
Choose a matrix insteadWhen V is small, the graph is dense, or you check edges constantly.
Visit every edgeO(V + E) with a list. O(V^2) with a matrix.

TrapA matrix for a sparse graph is mostly zeros. A road map with millions of nodes would need terabytes.

Check whether an edge exists

Check an edge in a matrixm[u][v] == 1O(1). The pair maps to one cell.
Check an edge in an adjacency listv in adj[u]O(degree of u). It scans u's list.
Make that check O(1) with a listadj[u] = set()Store neighbors in hash sets. More memory, more cache misses.

Compressed sparse row and the cache

offset: [0, 2, 3, 5]v owns offset[v] to offset[v+1]neighbors: [1, 2, 0, 0, 1]v0 links to 1 and 2, v1 to 0
Read node v's neighbors in CSRnbrs[off[v]:off[v + 1]]One sequential read the prefetcher handles well.
Explain why linked neighbor lists scan slowlyEach hop is likely a cache miss, which costs hundreds of cycles.
Add an edge to a CSR graphRebuild or shift the arrays. Use CSR for graphs that don't change.

TipBig-O stays O(V + E) either way. The memory layout decides the real traversal time.

BFS & DFS

visit every node once, in the order you need

BFS or DFS

123456
BFSNumbers show visit order. Every node one hop away comes before any node two hops away.
125346
DFSThe same graph. DFS follows one branch to its end, then backs up to the next.
Choose BFSFor the fewest hops, the nearest match, or level-by-level work.
Choose DFSFor cycles, components, topological order and the graph's shape.
Give the running time of BFS and DFSO(V + E) for both. Each node and edge is handled once.
Search a wide, shallow graphDFS uses less memory. The BFS queue holds a whole level.
Search a long, deep graphBFS uses less memory. The DFS stack grows with the depth.

TrapPlain DFS can reach a node by a long route first, so it doesn't give the fewest hops. Use BFS for that.

Write a BFS

q = deque([s]); seen = {s}while q: u = q.popleft() for v in adj[u]: if v not in seen: seen.add(v)mark on enqueue q.append(v)
Take the next node off the queueq.popleft()O(1) on a deque. list.pop(0) is O(n).
Handle one level at a timefor _ in range(len(q)):Read the queue's length, then pop that many nodes.
Find each cell's distance to the nearest exitq = deque(exits)Multi-source BFS. Queue every exit at distance 0, then run once.

TipMulti-source BFS needs equal edge costs. With weights, run Dijkstra from a super-source.

TrapMark a node when you enqueue it. Mark it on dequeue and the same node can enter the queue many times.

Write a DFS

def dfs(u): seen.add(u) for v in adj[u]: if v not in seen: dfs(v)go deeper
Write DFS without recursionu = stack.pop()Pop a node, skip it if seen, push its neighbors.
Check Python's recursion limitsys.getrecursionlimit()1000 by default. A 100,000-node chain goes far past it.

TrapRecursion depth follows the longest path, not the graph's size. A chain of V nodes needs V nested calls.

Track visited nodes

Say what breaks without visited marksCycles loop forever. Shared nodes get explored once per path.
Track visited when ids count up from 0seen = [False] * nO(1) checks in one tight block. The fastest choice.
Track visited for strings, tuples or scattered idsseen = set()It holds only what you touch, but hashes on every check.
Track visited when the set outgrows RAMA Bloom filter. Its false positives skip some real states.

TipA tree needs no visited set. In an undirected tree, just skip the parent you came from.

Shortest paths

pick the algorithm the edge weights allow

Pick a shortest-path algorithm

Find shortest paths with no weightsBFS, O(V + E). Nothing is faster.
Find shortest paths with non-negative weightsDijkstra, O((V + E) log V) with a binary heap.
Find shortest paths with negative edgesBellman-Ford, O(V * E). It also finds negative cycles.
Find shortest paths on a weighted DAGOne pass in topological order, O(V + E). Negative edges are fine.
Find every pair's distance on a small graphd[i][j] = min(d[i][j], d[i][k] + d[k][j])Floyd-Warshall, O(V^3). It handles negative edges.

TipPick the cheapest algorithm your weights allow. Bellman-Ford on non-negative weights only wastes time.

Shortest path with no weights

Count hops from the startdist[v] = dist[u] + 1Set it on first visit. That's the fewest-edge distance.
Rebuild the path itselfparent[v] = uWalk back from the target, then reverse.
Explain why BFS fails once edges have weightsIt fixes each distance by hop count. A cheaper detour comes too late.

TrapA to C costs 10 directly and 3 through B. BFS reaches C in one hop and reports 10.

Dijkstra's algorithm

dist = {}; pq = [(0, s)]while pq: d, u = heapq.heappop(pq) if u in dist: continuestale entry, skip it dist[u] = dsettled for good for v, w in adj[u]: if v not in dist: heapq.heappush(pq, (d + w, v))
Explain why a popped distance is finalAny other route passes a node at least as far, and edges only add.
Stop early for one targetReturn when the target is popped.
Swap in a Fibonacci heapO(E + V log V) on paper, but slower than a binary heap in practice.

TipSkip decrease-key. Push a duplicate entry, and drop the stale one when it's popped.

TrapA negative edge gives wrong distances with no error. Dijkstra assumes a settled node never gets cheaper.

Bellman-Ford and negative cycles

Run Bellman-Forddist[v] = min(dist[v], dist[u] + w)Relax every edge, V-1 times over.
Detect a negative cycleRun one more round. A distance that still drops proves one.
Catch cycles the source can't reachAdd a super-source with a 0-weight edge to every node.

TipCurrency arbitrage and cost-model graphs can hold negative cycles. Dijkstra would loop or return nonsense on them.

Faster than plain Dijkstra

Steer the search toward a goalA*. Order by f = g + h, cost so far plus a guess.
Keep A* exactThe guess must never overestimate. That makes it admissible.
Use A* where distance has no geometryh is 0, so A* is plain Dijkstra.
Search from both endsMeet in the middle. About 2 * b^(d/2) work, not b^d.
Find shortest paths when every weight is 0 or 1q.appendleft(v)q.append(v)0-1 BFS. A 0 edge goes to the front, a 1 edge to the back.

TipBidirectional search needs one known target and the reverse edges.

TrapThe first node both searches touch may not be on the shortest path. Track the best combined distance.

Harder shortest-path questions

Update paths when a weight changesRepair the tree. A drop spreads outward, a rise redoes a subtree.
List the k shortest pathsYen's algorithm. It forces detours and reruns Dijkstra.
Pick a safe INF in Java or C++The type's max divided by 2. Never relax from a node at INF.

TrapIn Java or C++, INF + w can wrap to a negative number. An unreachable node then looks closest.

Cycles & topological order

order tasks by dependency, and catch loops

Topological sort

indeg = Counter(v for u in adj for v in adj[u])q = deque(u for u in adj if indeg[u] == 0)order = []while q: u = q.popleft(); order.append(u) for v in adj[u]: indeg[v] -= 1 if indeg[v] == 0: q.append(v)
Order tasks so every edge points forwardTopological sort, O(V + E). It needs a DAG, which has no cycles.
Tell whether the sort hit a cyclelen(order) < len(adj)A cycle blocked the nodes left over.
Sort with DFS insteadRecord each node as its DFS call finishes, then reverse.
Choose Kahn's over DFSNo recursion, a free cycle check, a queue you can prioritize.

TrapA partial order is not a shorter valid answer. It proves the input has a cycle.

Schedule tasks with dependencies

Model tasks and prerequisitesAn edge from each prerequisite to the task that needs it.
Find which tasks can run at onceRun Kahn's level by level. Each level can run in parallel.
Find the shortest possible scheduleWeight tasks by duration. The longest path is the critical path.
Order a graph too big to loadKahn's needs one in-degree count per node. Stream the edges.

TrapPick one edge direction and never flip it. Mixing them puts tasks before their prerequisites.

Detect a cycle

ABCD
no cycleFilled means in progress, dashed means done. C reaches D, but D is done, so no cycle.
ABCD
cycleD reaches A while A is still in progress. That back edge closes a cycle.
Detect a cycle in an undirected graphDFS. A visited neighbor that isn't your parent closes a cycle.
Detect it with union-find insteadAn edge whose ends share a root closes a loop. Undirected only.
Detect a cycle in a directed graphDFS with three states. Reaching an in-progress node means a cycle.

TrapChecking only the visited flag reports false cycles in a directed graph. Reaching a node that is done is safe.

Components & union-find

which nodes connect, now and as edges arrive

Connected components

count = 0for v in range(n): if v not in seen: count += 1 dfs(v)marks v's whole component
Label every componentO(V + E) in total, across all the searches.
Count islands in a gridThe same loop over cells. Each new start is one island.
Run a cycle or bipartite check on the whole graphWrap it in the same outer loop.

TrapOne search from one start passes on connected test data. In production it drops all the other islands.

Union-find

def find(x):p[x] is x's parent if p[x] != x: p[x] = find(p[x])path compression return p[x]def union(a, b): a, b = find(a), find(b) if a == b: returnalready joined if size[a] < size[b]: a, b = b, asmaller tree goes under p[b] = a; size[a] += size[b]
Join two sets, or ask whether two nodes connectunion(a, b)find(a) == find(b)Amortized O(α(n)), under 5 for any real input.
Explain path compressionEach find points its whole path straight at the root.
Explain union by rankHang the shorter tree under the taller. The code above uses size.
Choose union-find over DFSWhen edges keep arriving and you keep asking who connects.
Remove an edge from union-findYou can't. Deletions need link-cut or Euler-tour trees.

TrapUnion-find says two nodes connect, not how. For a path or a component's members, use DFS.

Strongly connected components

Say what an SCC isA largest set of directed-graph nodes that all reach each other.
Find every SCCTarjan's in one DFS. Kosaraju's in two, one on the reversed graph. Both O(V + E).
Turn a directed graph with cycles into a DAGCollapse each SCC into one node.

TrapA path one way is not strong connectivity. A must reach B, and B must also reach A.

Check for a bipartite graph

Check whether a graph is bipartiteTwo-color it with BFS. A same-color edge means it isn't.
Say what makes it failAn odd cycle. A graph is bipartite exactly when it has none.

TrapRestart the coloring at every unvisited node. One component can pass while another fails.

Articulation points and bridges

Find a bridgelow[v] > disc[u]If true, edge u to child v is a bridge.
Find an articulation pointlow[v] >= disc[u]If true for any child v, u is one. The DFS root needs two children.

TipTarjan's single DFS finds both in O(V + E). low is the earliest discovery time a subtree can reach.

TrapSkip the edge you arrived by, not the parent node. A second edge to the parent keeps the graph connected.

Minimum spanning trees

connect every node at the lowest total cost

Minimum spanning tree

Say what an MST isThe cheapest edges that link every node with no cycle. It has V-1 edges.
Name where MSTs are usedLaying cable, pipes or power lines. Also clustering.
Tell when the MST is uniqueWhen all edge weights differ. Ties can give several MSTs.

TrapAn MST is not a shortest-path tree. It minimizes the total weight, not the distance from one node.

Kruskal's or Prim's

Build an MST with Kruskal'sSort edges by weight, keep each that joins two trees. O(E log E).
Build an MST with Prim'sGrow one tree by its cheapest outgoing edge. O(E log V) with a heap.
Build an MST on a dense graphPrim's with a plain array, O(V^2). No sorting needed.

TipKruskal's suits sparse graphs and edges that arrive sorted. Prim's suits dense graphs and adjacency matrices.

TrapOn a disconnected graph you get a forest, one tree per component. Check for V-1 edges before you call it a tree.

Graphs at scale

disk, threads and a billion edges

A billion nodes and edges

Say what breaks first at a billion nodesMemory. One 4-byte value per node is already 4 GB.
Size node ids and path sumsUse 64-bit. A signed 32-bit int tops out at 2,147,483,647.
Traverse a graph bigger than RAMKeep visited bits in memory. Stream edges from disk in order.
Spread a graph over many machinesPartition it, and batch messages between parts, like Pregel.

TipTraversal stays O(V + E). Memory bandwidth and disk reads decide whether it finishes.

TrapA power-law hub floods its partition with messages, so that partition falls behind the rest.

Parallel and concurrent traversal

Parallelize BFSExpand each level in parallel, with a barrier between levels.
Stop two threads claiming one nodeUse an atomic compare-and-swap on its visited flag.
Traverse while other threads writeWalk a snapshot. Copy-on-write makes snapshots cheap.

TrapA deep, thin graph has many tiny levels. Then the barriers cost more than the parallel work saves.

Test a graph algorithm

Pick inputs that break graph codeEmpty, one node, self-loops, parallel edges, no edges, one big cycle.
Catch the subtle bugsCompare with a slow, plainly correct version on random graphs.
Check the output itselfEvery edge points forward in a topological order.

TipSeed the random graphs, so a failure reproduces exactly.

TrapSmall tests miss overflowed counters and blown recursion stacks. Grow the inputs until something breaks.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 60 questions it links, not just the 9 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.