Skiena, Chapter 5

Graph Traversal

BFS, DFS, connected components, topological sort, and articulation points. How to systematically explore every node and edge in a graph.

Prerequisites: Chapters 1-3 (stacks, queues). That's it.
9
Chapters
6+
Simulations
9
Quizzes

Chapter 0: Graphs Are Everywhere

You have a social network with 500 people. You want to know: is there a path of friend connections between Alice and Bob? How many separate friend groups exist? Who is the most central person?

These are all graph problems. A graph is a set of vertices (nodes) connected by edges (links). People are vertices, friendships are edges. Roads connecting cities, links between web pages, dependencies between tasks -- all graphs.

The fundamental question is: how do you systematically visit every vertex and every edge? You can't just pick random starting points and hope for the best. You need a strategy that guarantees you explore everything exactly once.

The core problem: Given a graph, how do you visit every reachable vertex exactly once? The two answers -- breadth-first search (BFS) and depth-first search (DFS) -- are the building blocks for almost every graph algorithm.
What makes graph traversal non-trivial?

Chapter 1: Flavors of Graphs

Graphs come in many varieties, and the flavor matters for which algorithms apply:

Directed vs Undirected
One-way streets vs two-way streets. Directed edge (x,y) goes from x to y only.
Weighted vs Unweighted
Edges may carry costs (distance, time, capacity).
Cyclic vs Acyclic
DAGs (directed acyclic graphs) have no cycles. Trees are connected acyclic graphs.
Sparse vs Dense
Sparse graphs have ~n edges. Dense graphs have ~n2 edges.
Modeling insight: Many real-world problems become graph problems once you define what the vertices and edges represent. Road networks, circuit boards, scheduling constraints, web links, social connections -- the first step is always recognizing the graph.

A simple graph has no self-loops (edge from a vertex to itself) and no multi-edges (multiple edges between the same pair). Most algorithms assume simple graphs unless stated otherwise.

A project has tasks with dependencies (task B requires task A to finish first). What kind of graph models this?

Chapter 2: Graph Representations

How do you store a graph in a computer? Two approaches dominate:

An adjacency matrix is an n × n grid M where M[i][j] = 1 if there's an edge from vertex i to j. Fast to check if an edge exists (O(1)), but uses O(n2) space regardless of how many edges there are.

An adjacency list stores, for each vertex, a linked list of its neighbors. Uses O(n + m) space (n vertices, m edges). Traversal is efficient because you only visit actual edges. But testing whether a specific edge exists requires scanning a list.

OperationAdj MatrixAdj List
Is (x,y) an edge?O(1)O(degree(x))
List neighbors of xO(n)O(degree(x))
SpaceO(n2)O(n + m)
Traverse entire graphO(n2)O(n + m)
The winner for most problems: Adjacency lists. Real-world graphs are almost always sparse (m << n2), so adjacency lists save enormous space and make traversal faster. Use matrices only for dense graphs or when you need O(1) edge lookups.
A social network has 1 million users averaging 100 friends each. Which representation is better?

Chapter 3: Breadth-First Search

Breadth-first search (BFS) explores a graph level by level, like ripples spreading from a stone dropped in a pond. It visits all vertices at distance 1, then distance 2, then distance 3, and so on.

BFS uses a queue (FIFO). Start from a source vertex s, mark it discovered, and enqueue it. Then repeat: dequeue a vertex, explore all its undiscovered neighbors, mark and enqueue them. When the queue is empty, you've visited everything reachable from s.

python
def bfs(graph, start):
    discovered = {start}
    queue = [start]
    parent = {start: None}
    while queue:
        v = queue.pop(0)  # dequeue front
        for neighbor in graph[v]:
            if neighbor not in discovered:
                discovered.add(neighbor)
                parent[neighbor] = v
                queue.append(neighbor)
    return parent
BFS gives shortest paths: In an unweighted graph, the BFS tree encodes the shortest path from the source to every reachable vertex. The parent array lets you trace the path backwards. This is because BFS discovers vertices in order of increasing distance.
BFS Visualization

Click "Step" to advance BFS one level at a time. Blue = discovered, orange = currently being processed.

Click Step to begin BFS from node 0

BFS runs in O(n + m) time: each vertex is enqueued once, and each edge is examined once. This is optimal -- you can't do better than looking at the whole graph.

Why does BFS find shortest paths in unweighted graphs?

Chapter 4: BFS Applications

BFS is not just a traversal -- it's a problem-solving tool. Here are the key applications that fall out of BFS almost for free:

Connected components: In an undirected graph, a connected component is a maximal set of vertices reachable from each other. Run BFS from any unvisited vertex -- everything it reaches forms one component. Repeat until all vertices are visited. Each BFS call discovers one component.

Two-coloring (bipartiteness): Can you color every vertex red or blue such that no edge connects two vertices of the same color? BFS can test this: color the source red, all its neighbors blue, all their neighbors red, etc. If you ever find an edge between same-colored vertices, the graph is not bipartite.

Why bipartiteness matters: A bipartite graph is one whose vertices split into two groups with all edges crossing between them. Job assignment (workers to tasks), matching (students to dorms), and scheduling problems are all bipartite graph problems. Testing bipartiteness with BFS takes O(n + m) time.

Shortest path tree: The parent pointers from BFS form a tree rooted at the source. Following parents from any vertex back to the source traces the shortest path. This tree is the BFS tree.

How do you find all connected components in an undirected graph?

Chapter 5: Depth-First Search

Depth-first search (DFS) is the other fundamental traversal. Instead of exploring level-by-level, it dives as deep as possible along each branch before backtracking. Think of it as exploring a maze by always turning left until you hit a dead end, then backtracking.

DFS uses a stack (LIFO) -- either explicitly or via the call stack of recursion. The recursive version is elegant:

python
def dfs(graph, v, discovered, parent):
    discovered.add(v)
    for neighbor in graph[v]:
        if neighbor not in discovered:
            parent[neighbor] = v
            dfs(graph, neighbor, discovered, parent)
BFS vs DFS -- same complexity, different shape: Both run in O(n + m) time and visit every vertex once. The difference is the shape of the traversal tree. BFS trees are wide and shallow (shortest paths). DFS trees are narrow and deep (useful for detecting cycles, topological sorting, and finding cut vertices).

DFS classifies every edge in the graph into one of four types:

Edge TypeMeaning
Tree edgeLeads to an undiscovered vertex (part of the DFS tree)
Back edgeLeads to an ancestor in the DFS tree (proves a cycle exists)
Forward edgeLeads to a descendant (directed graphs only)
Cross edgeLeads to a vertex in a different subtree (directed graphs only)
How does DFS detect cycles in a graph?

Chapter 6: DFS Applications

DFS powers several important graph algorithms that BFS cannot easily do:

Topological sorting: Given a DAG (directed acyclic graph), produce an ordering of vertices such that for every directed edge (u, v), u appears before v. Think of it as a valid ordering of tasks respecting all dependencies. DFS gives us topological order by reverse finishing time: process a vertex only after all its descendants are finished.

Articulation points (cut vertices): A vertex whose removal disconnects the graph. These are critical points of failure in a network. DFS can find all articulation points in O(n + m) time using entry times and low-link values.

Strongly connected components: In a directed graph, a strongly connected component is a maximal set of vertices where every vertex is reachable from every other. DFS on the graph and its reverse (Kosaraju's algorithm) finds all SCCs in O(n + m) time.

The power of DFS timing: DFS assigns each vertex an entry time and an exit time. These timestamps encode the entire tree structure. Vertex u is an ancestor of v if and only if entry(u) < entry(v) < exit(v) < exit(u). This interval nesting property is the key to efficient algorithms for topological sort, articulation points, and SCCs.
What does topological sort produce?

Chapter 7: The Graph Explorer

Time to watch BFS and DFS explore the same graph side by side. Notice how they visit vertices in completely different orders and produce different tree shapes.

BFS vs DFS Side-by-Side

Click "Step" to advance both algorithms simultaneously. Warm = BFS frontier, teal = DFS frontier. Numbers show discovery order.

Click Step to begin
What to notice: BFS explores in concentric rings from the start vertex -- all neighbors first, then neighbors of neighbors. DFS dives deep along one path before backtracking. BFS discovers vertex 4 before vertex 7 (closer first). DFS might reach vertex 7 before vertex 4 (deeper first). Both visit every vertex exactly once.
Both BFS and DFS run in O(n + m) time. When would you choose BFS over DFS?

Chapter 8: Connections

Graph traversal is the foundation for everything in the next four chapters:

ConceptWhere It Leads
BFSChapter 6: Dijkstra's builds on BFS with a priority queue
DFSChapter 7: Backtracking is DFS on an implicit search tree
Topological SortChapter 8: DP on DAGs follows topological order
Connected ComponentsChapter 6: Kruskal's MST uses union-find for components
Edge ClassificationChapter 9: Reductions often model problems as graph searches
Skiena's take-home lessons from Chapter 5:
• Graphs are the most important and flexible data structure -- most interesting real-world problems can be modeled as graph problems.
• BFS and DFS are both O(n + m), but they serve different purposes. BFS gives shortest paths; DFS gives topological order, cycle detection, and connectivity structure.
• Adjacency lists are the right representation for most graphs. Use matrices only for dense graphs.
• DFS edge classification (tree, back, forward, cross) is the key to understanding DFS-based algorithms.
• Modeling is half the battle. Define your vertices and edges, and the algorithm often follows naturally.
A graph has n vertices and m edges. What is the time complexity of BFS or DFS?