Data Structure 📂 Graph Algorithms · 1 of 6 75 min read

Graphs — BFS & DFS (Breadth-First Search & Depth-First Search)

Learn graphs from scratch: nodes, edges, types, representations, BFS, DFS, shortest path, cycle detection, topological sort — animated & interactive.

Section 01 — Foundations

What Is a Graph?

A Graph Is Just a Map of Relationships
Think about your city. Every intersection is a point. Every road connecting two intersections is a link. The entire city road network — that is a graph.

Now think about your phone contacts. Every person is a point. Every friendship or follow between two people is a link. Instagram, Twitter, LinkedIn — all graphs.

A graph is simply a way to model anything that has things and relationships between those things. Almost every hard computer science problem — routing, scheduling, recommendation engines, compilers, AI pathfinding — reduces to a graph problem underneath.

Formally, a graph G = (V, E) is a pair of two sets: V (a set of vertices / nodes) and E (a set of edges / connections). That is the entire definition. Everything else is built on top of it.

🔬 Graph Anatomy — Nodes, Edges & Properties
A Simple Graph
e₁ e₂ e₃ e₄ e₅ A B C D ← Node (Vertex) ← Edge
Directed Graph (Digraph)
A B C D Arrows show direction of edge
Weighted Graph
4 7 2 9 1 A B C D Numbers on edges = weights / costs
🔵
Node (Vertex)
An entity in the graph. A city, a person, a web page, a function in code. Also called a vertex. Notation: V or n.
Edge
A connection between two nodes. A road, a friendship, a hyperlink. Can be directed (one-way) or undirected (two-way). Notation: E or m.
🔢
Weight
An optional number on an edge representing cost, distance, or capacity. Road distance, travel time, bandwidth. Not all graphs are weighted.
📐
Degree
How many edges touch a node. In a directed graph: in-degree (edges arriving) and out-degree (edges leaving) are tracked separately.
🔄
Cycle
A path that starts and ends at the same node. A → B → C → A is a cycle. Graphs without cycles are called acyclic (e.g. trees, DAGs).
🛤️
Path
A sequence of nodes where each consecutive pair shares an edge. The length of a path = number of edges traversed (or sum of weights in weighted graphs).
🏝️
Component
A maximal set of nodes that are all reachable from each other. A disconnected graph has multiple components — BFS/DFS won't cross component boundaries.
🌳
Tree (Special Graph)
A connected, acyclic, undirected graph with exactly V−1 edges. Every data structure "tree" (BST, AVL, heap) is a graph — a very restricted one.

Section 02 — Graph Types

Types of Graphs — A Visual Reference

Graphs come in many flavours. The type of graph determines which algorithms you can use and what guarantees they provide. Misidentifying your graph type is the #1 source of algorithm bugs.

📊 Six Fundamental Graph Types
Undirected
A B C D
Edges have no direction — A–B means you can travel A→B and B→A. Symmetric relationships.
✦ Facebook friends, road networks, circuit boards
Directed (Digraph)
A B C D
Edges have a direction — A→B does NOT mean B→A. Asymmetric relationships. Also called digraph.
✦ Twitter follows, web links, task dependencies
Weighted
5km 2km A B C
Each edge carries a numeric value (weight). Could be distance, cost, time, bandwidth, or probability.
✦ GPS routing, network bandwidth, flight prices
DAG (Directed Acyclic)
A B C D
Directed + no cycles. Edges only flow "downward". Topological ordering always possible on a DAG.
✦ Build systems, pip dependencies, spreadsheets
Cyclic Graph
A B C D
Contains at least one cycle — a path from a node back to itself. Requires cycle detection before certain algorithms.
✦ Road networks, state machines, deadlock detection
Disconnected Graph
A B C D E F
Not all nodes are reachable from each other. Has 2+ connected components. Single BFS/DFS won't cover the full graph.
✦ Island detection, clustering, network partitions

Section 03 — Terminology

Graph Vocabulary — Every Term You'll Encounter

📖 Complete Graph Glossary
Node / Vertex
A fundamental unit of a graph. Represents an entity: city, person, function, state. Plural: vertices. Count denoted V or n.
Edge / Arc
A connection between two nodes. In undirected graphs: written as {u,v}. In directed: written as (u→v). Count denoted E or m.
Adjacent
Two nodes are adjacent (neighbours) if an edge connects them directly. Node B is adjacent to A if edge (A,B) or {A,B} exists.
Degree
Number of edges touching a node. In-degree: edges arriving (directed). Out-degree: edges leaving (directed). Sum of all degrees = 2×|E|.
Path
A sequence of distinct nodes v₁,v₂,...,vₖ where each consecutive pair is connected by an edge. Length = number of edges (or sum of weights).
Cycle
A path that starts and ends at the same node: v₁→v₂→...→v₁. Self-loop: edge from a node to itself. Multigraph: multiple edges between same pair.
Connected
A graph is connected if every node can reach every other node via some path. Trees are always connected. Disconnected graphs have isolated components.
Sparse vs Dense
Sparse: E ≪ V² (few edges, like real-world networks). Dense: E ≈ V² (many edges, like complete graphs). Critical for choosing representation.
Subgraph
A graph formed by a subset of V and E from the original. A spanning subgraph uses all original vertices. A spanning tree is a connected acyclic spanning subgraph.
Weighted Path
Total cost of a path = sum of edge weights along it. Shortest path on weighted graph = path with minimum total weight (not fewest hops).
📐
Tree = Special Case of Graph

Every data structure tree you know — binary tree, BST, AVL tree, heap, trie — is a connected, undirected, acyclic graph with exactly V−1 edges. BFS and DFS work on all of them. Level-order traversal of a BST is just BFS. Pre/in/post-order traversal is just DFS. The graph algorithms you learn here apply directly to every tree problem you'll ever encounter.


Section 04 — Representation

Representing Graphs in Python

Before you can run BFS or DFS, you need to store the graph in memory. The two main approaches — adjacency list and adjacency matrix — have wildly different performance characteristics.

# ══════════════════════════════════════════════════════════════
# METHOD 1 — Adjacency List (dict of lists)  ← use this 99% of time
# ══════════════════════════════════════════════════════════════

# Undirected graph:  A—B, A—C, B—D, C—D, D—E
undirected = {
    'A': ['B', 'C'],
    'B': ['A', 'D'],
    'C': ['A', 'D'],
    'D': ['B', 'C', 'E'],
    'E': ['D'],
}

# Directed graph:  A→B, A→C, B→D, C→D
directed = {
    'A': ['B', 'C'],   # A points TO B and C
    'B': ['D'],         # B points TO D only
    'C': ['D'],
    'D': [],             # D has no outgoing edges
}

# Weighted graph: edge (A,B) has weight 4, (A,C) has weight 7
weighted = {
    'A': [('B', 4), ('C', 7)],
    'B': [('A', 4), ('D', 2)],
    'C': [('A', 7), ('D', 9)],
    'D': [('B', 2), ('C', 9)],
}

# ══════════════════════════════════════════════════════════════
# METHOD 2 — Adjacency Matrix (2D list)  ← use for dense graphs
# ══════════════════════════════════════════════════════════════
# Nodes indexed: A=0, B=1, C=2, D=3
# matrix[i][j] = 1 if edge (i,j) exists, else 0
matrix = [
    [0, 1, 1, 0],  # A: connected to B, C
    [1, 0, 0, 1],  # B: connected to A, D
    [1, 0, 0, 1],  # C: connected to A, D
    [0, 1, 1, 0],  # D: connected to B, C
]

# ── Helper: build graph from edge list (common in interviews) ──
def build_graph(n, edges, directed=False):
    """Build adjacency list from edge list.
    n: number of nodes (0-indexed)
    edges: list of [u, v] pairs
    """
    graph = {i: [] for i in range(n)}
    for u, v in edges:
        graph[u].append(v)
        if not directed:
            graph[v].append(u)  # add reverse for undirected
    return graph

# Usage:
g = build_graph(5, [[0,1],[0,2],[1,3],[2,3],[3,4]])
print(g)
# {0:[1,2], 1:[0,3], 2:[0,3], 3:[1,2,4], 4:[3]}
OUTPUT
{0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2, 4], 4: [3]}
OperationAdjacency ListAdjacency Matrix
SpaceO(V + E)O(V²)
Check edge (u,v) existsO(degree(u))O(1)
Iterate all neighbours of uO(degree(u))O(V)
Add a nodeO(1)O(V²) rebuild
BFS / DFS performanceO(V+E) — optimalO(V²) — scans all
Best forSparse (real world)Dense or tiny graphs
⚠️
Always Use Adjacency List Unless Told Otherwise

A graph with 1,000,000 nodes and 2,000,000 edges (very common in social networks) needs only ~16 MB as an adjacency list. The same graph as an adjacency matrix needs 8 TB — completely impossible. Real-world graphs are almost always sparse. In coding interviews, default to adjacency list unless the problem explicitly says "dense graph".


Section 05 — The Big Idea

The Story That Explains Graph Traversal

Finding Your Seat in a Cinema — Two Completely Different Strategies
You arrive at a massive cinema with hundreds of rows and no seat map. You need to find your friend. You have two instincts:

Strategy A — BFS (Breadth-First): You check every seat in Row 1, then every seat in Row 2, then Row 3 — sweeping across the entire cinema level by level. Your friend is close to the entrance? You find them immediately. Far back? You visit every closer seat first.

Strategy B — DFS (Depth-First): You pick an aisle and sprint all the way to the back of the cinema, checking column by column as deep as possible before backtracking to the next aisle. Your friend is in the back corner? You find them fast. Near the front? You might walk the entire cinema first.

Neither strategy is wrong — they are optimised for different goals. BFS finds the shortest path. DFS explores the deepest structure.

BFS and DFS are the two fundamental algorithms for visiting every node in a graph — the backbone of hundreds of real-world applications. Both run in O(V + E) time. The difference is entirely in their order of exploration — which comes down to one data structure choice: queue vs stack.

🔵
BFS
Breadth-First Search
Explores all neighbours before going deeper. Uses a queue (FIFO). Level-by-level. Finds shortest path on unweighted graphs.
🟣
DFS
Depth-First Search
Dives as deep as possible before backtracking. Uses a stack (LIFO) or recursion. Better for cycle detection, topological sort, component analysis.
⚖️
Same Complexity
O(V + E) both
Both visit every node and edge exactly once. The choice between them is about what you need from the traversal, not speed.

Section 06 — Animation

Live Animation — BFS vs DFS on the Same Graph

Watch BFS sweep level-by-level and DFS dive deep before backtracking — on the identical 9-node graph. Press Step → to go one node at a time, or Auto Play to watch the full traversal.

🔍Graph Traversal Animation — BFS & DFS
BFS — Queue (FIFO)
▶ Press Step → to begin. Starting from node A.
BFS Visit Order
Queue
[ A ]
DFS Visit Order
Stack
[ A ]
Step 0 / 9
Unvisited
In Queue/Stack
BFS Visited
DFS Visited
Current Node
💡
Reading the Animation

Yellow nodes are discovered but not yet processed (in queue/stack). Blue = BFS visited. Purple = DFS visited. Green = currently being processed. BFS visits A → B → C → D → E → F → G → H → I (sweeps levels). DFS visits A → B → D → H → E → C → F → I → G (dives deep first).


Section 07 — BFS

BFS — Breadth-First Search

BFS uses a queue (FIFO). When you visit a node, you enqueue all its unvisited neighbours. Because the queue processes nodes in the order they were discovered, you always finish all nodes at distance d before visiting any node at distance d+1.

🔵 BFS Step-by-Step — Graph A→{B,C}, B→{D,E}, C→{F,G}
Init
Enqueue start A. Mark A visited. Queue: [A] · Visited: {A}
Step 1
Dequeue A. Enqueue unvisited neighbours B, C. Queue: [B, C]
Step 2
Dequeue B. Enqueue D, E. Queue: [C, D, E] — C still waiting from level 1!
Step 3
Dequeue C. Finish level 1. Enqueue F, G. Queue: [D, E, F, G]
Step 4–7
Dequeue D, E, F, G — level 2 complete. Leaf nodes H, I discovered and queued.
Result
Visit order: A → B → C → D → E → F → G → H → I ✅ Level by level
from collections import deque

def bfs(graph: dict, start) -> list:
    """
    Breadth-First Search — returns nodes in visit order.
    graph: adjacency list {node: [neighbour, ...]}
    """
    visited = {start}           # mark on ENQUEUE, not dequeue
    queue   = deque([start])   # O(1) popleft — critical!
    order   = []

    while queue:
        node = queue.popleft()    # FIFO — front of queue
        order.append(node)

        for neighbour in graph[node]:
            if neighbour not in visited:
                visited.add(neighbour)
                queue.append(neighbour)  # enqueue at BACK

    return order

graph = {'A':['B','C'],'B':['A','D','E'],'C':['A','F','G'],'D':['B','H'],'E':['B','H'],'F':['C','I'],'G':['C','I'],'H':['D','E'],'I':['F','G']}
print("BFS order:", " → ".join(bfs(graph, 'A')))
OUTPUT
BFS order: A → B → C → D → E → F → G → H → I
Why deque and NOT list.pop(0)?

list.pop(0) is O(n) — it shifts every remaining element left. collections.deque.popleft() is O(1). On a 10,000-node graph, using a list as a queue makes BFS run in O(V²) instead of O(V+E). This single mistake turns a fast algorithm into an impossibly slow one.


Section 08 — BFS Shortest Path

BFS Superpower — Shortest Path on Unweighted Graphs

BFS guarantees the shortest path (fewest hops) between two nodes on an unweighted graph, because it explores all paths of length 1 before length 2, and so on. This is the algorithm behind social "degrees of separation", web crawl depth limits, and game pathfinding.

from collections import deque

def bfs_shortest_path(graph, start, end):
    """Returns shortest path list from start→end, or [] if unreachable."""
    if start == end: return [start]
    visited = {start}
    queue = deque([(start, [start])])  # (node, path_so_far)
    while queue:
        node, path = queue.popleft()
        for nb in graph[node]:
            if nb not in visited:
                new_path = path + [nb]
                if nb == end: return new_path  # ← guaranteed shortest
                visited.add(nb)
                queue.append((nb, new_path))
    return []  # unreachable

def bfs_distances(graph, start):
    """Returns dict of shortest distances from start to every node."""
    dist = {start: 0}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        for nb in graph[node]:
            if nb not in dist:
                dist[nb] = dist[node] + 1  # one hop further
                queue.append(nb)
    return dist

path = bfs_shortest_path(graph, 'A', 'H')
print(f"Shortest path A→H: {' → '.join(path)} ({len(path)-1} hops)")

distances = bfs_distances(graph, 'A')
print("Distances from A:", distances)
OUTPUT
Shortest path A→H: A → B → D → H (3 hops) Distances from A: {'A': 0, 'B': 1, 'C': 1, 'D': 2, 'E': 2, 'F': 2, 'G': 2, 'H': 3, 'I': 3}
⚠️
BFS ≠ Shortest Path on Weighted Graphs

BFS finds the path with the fewest edges, not the lowest total weight. If edges have costs (road distances, prices), a path with 2 expensive edges can be worse than a path with 5 cheap edges. Use Dijkstra's algorithm (priority queue instead of plain queue) for weighted shortest paths.


Section 09 — DFS

DFS — Depth-First Search

DFS uses a stack (LIFO). You push a node, pop it, visit it, and immediately push all its unvisited neighbours. The last pushed neighbour is the next one visited — so you always go deeper before going wider.

📚
Iterative DFS
Uses an explicit Python list as a stack. Safe for very deep graphs (no recursion limit). Slightly more verbose but production-safe.
stack.append() / stack.pop()
🔁
Recursive DFS
Uses the call stack implicitly. Cleaner and easier to extend (pre/post-order, backtracking). Python's default limit: 1000 frames.
def dfs(node): for nb: dfs(nb)
🌳
Pre vs Post Order
Pre-order: process node before recursing (top-down, tree copy). Post-order: process after all children return (topological sort, tree delete).
order of the append() call
# ── Iterative DFS ─────────────────────────────────────────────
def dfs_iterative(graph, start):
    visited = set()
    stack   = [start]
    order   = []
    while stack:
        node = stack.pop()           # LIFO — last in, first out
        if node in visited: continue
        visited.add(node)
        order.append(node)
        for nb in reversed(graph[node]):  # reverse → natural order
            if nb not in visited:
                stack.append(nb)
    return order

# ── Recursive DFS ──────────────────────────────────────────────
def dfs_recursive(graph, node, visited=None, order=None):
    if visited is None: visited = set()
    if order   is None: order   = []
    visited.add(node)
    order.append(node)                # PRE-ORDER: process before recursing
    for nb in graph[node]:
        if nb not in visited:
            dfs_recursive(graph, nb, visited, order)
    return order
    # POST-ORDER: move order.append(node) to HERE (after the for loop)

print("Iterative:", dfs_iterative(graph, 'A'))
print("Recursive:", dfs_recursive(graph, 'A'))
OUTPUT
Iterative: ['A', 'B', 'D', 'H', 'E', 'C', 'F', 'I', 'G'] Recursive: ['A', 'B', 'D', 'H', 'E', 'C', 'F', 'I', 'G']

Section 10 — DFS Applications

DFS Power Applications

# ══════════════════════════════════════════════════════════════
# 1. CYCLE DETECTION — directed graph
# ══════════════════════════════════════════════════════════════
def has_cycle_directed(graph):
    """True if directed graph contains a cycle (back-edge detection)."""
    visited = set()
    rec_stack = set()   # nodes in CURRENT DFS path

    def dfs(node):
        visited.add(node); rec_stack.add(node)
        for nb in graph.get(node, []):
            if nb not in visited:
                if dfs(nb): return True
            elif nb in rec_stack:
                return True   # back-edge = cycle
        rec_stack.discard(node)
        return False

    return any(dfs(n) for n in graph if n not in visited)

# ══════════════════════════════════════════════════════════════
# 2. TOPOLOGICAL SORT — DFS post-order on a DAG
# ══════════════════════════════════════════════════════════════
def topo_sort(graph):
    """Topological ordering of a DAG using DFS post-order."""
    visited = set()
    result  = []

    def dfs(node):
        visited.add(node)
        for nb in graph.get(node, []):
            if nb not in visited: dfs(nb)
        result.append(node)    # POST-ORDER: after all children

    for node in graph:
        if node not in visited: dfs(node)
    return result[::-1]       # reverse → topological order

# ══════════════════════════════════════════════════════════════
# 3. CONNECTED COMPONENTS
# ══════════════════════════════════════════════════════════════
def connected_components(graph):
    """Returns list of components; each component is a set of nodes."""
    visited    = set()
    components = []

    def dfs(node, component):
        visited.add(node); component.add(node)
        for nb in graph[node]:
            if nb not in visited: dfs(nb, component)

    for node in graph:
        if node not in visited:
            comp = set()
            dfs(node, comp)
            components.append(comp)         # each DFS run = 1 component

    return components

# ── Tests ──
dag = {'A':['C'],'B':['C','D'],'C':['E'],'D':['F'],'E':['F'],'F':[]}
print("Topo sort:", topo_sort(dag))

disc_graph = {'A':['B'],'B':['A'],'C':['D'],'D':['C'],'E':[]}
print("Components:", connected_components(disc_graph))
OUTPUT
Topo sort: ['B', 'A', 'D', 'C', 'E', 'F'] Components: [{'A', 'B'}, {'C', 'D'}, {'E'}]
🔍
Why Post-Order Gives Topological Order

A node is appended to the result only after all nodes it depends on (all its successors in the DAG) have been fully processed. Reversing this list gives you the order in which nodes should be processed: dependencies always come before dependents. This is exactly what pip install and build systems like make compute under the hood.


Section 11 — Code Walkthrough

Animated Code Walkthrough — Line by Line

Step through each algorithm's execution. The highlighted line shows what is running now; the state panel shows every variable at that exact moment.

Program State
Press Step → to start the walkthrough.
Step 0

Section 12 — Complexity

Time & Space Complexity — Full Comparison

PropertyBFSDFSNotes
Time complexityO(V + E)O(V + E)Both visit every node and edge exactly once.
Space — data structureO(V) worstO(h) depthBFS queue can hold entire level. DFS stack holds only current path.
Shortest path (unweighted)Guaranteed ✓Not guaranteedBFS guarantees fewest hops; DFS finds A path, not THE shortest.
Wide / bushy graphsMore memoryLess memorySocial networks with millions of friends-of-friends favour DFS.
Deep / narrow graphsLess memoryStack overflow riskRecursive DFS crashes on depth > ~1000 in Python.
Cycle detectionYes (with tracking)Natural via back-edgeDFS back-edge approach is more elegant and direct.
Topological sortUse Kahn's insteadYes — post-orderDFS post-order is the textbook topological sort algorithm.
Connected componentsYesYesEither works; pick whichever is simpler for your code.
Complete traversal guaranteeYesYesBoth visit every reachable node from the start — given no bugs.
Suitable for treesLevel-orderPre/In/Post orderBoth are essential in tree interview problems.
⚠️
Space Complexity Is the Real Differentiator

Both algorithms are identical on the time axis. The choice almost always comes down to space and what you need from the result. Wide graphs (social networks) favour DFS for memory. Shortest-path demands BFS regardless of width. Topological sort and cycle detection demand DFS.


Section 13 — Decision Guide

BFS vs DFS — When to Use Which

🔵
Use BFS When…
Shortest path on unweighted graph needed.
Level-order traversal of a tree.
✦ Target is close to start — BFS finds nearby nodes fast.
Web crawl with depth limit.
✦ "Minimum steps to reach X" problems.
Queue (FIFO) · O(V+E) · O(V) space
🟣
Use DFS When…
Cycle detection in directed/undirected graph.
Topological sort of a DAG.
Path exists problems (does path exist? — not shortest).
Memory limited on wide graphs.
✦ Backtracking, maze solving, all-paths enumeration.
Stack/Recursion (LIFO) · O(V+E) · O(h) space
⚖️
Either Works When…
Counting connected components.
✦ Checking if graph is connected (A reach B?).
✦ Marking all reachable nodes.
✦ Flood fill (image processing, paint bucket tool).
Pick based on code simplicity

Section 14 — Real World

Real-World Use Cases

BFS
Social Network — "Degrees of Separation"
LinkedIn's "2nd connection" is BFS from your profile. All people at distance 1 = direct connections; at distance 2 = their connections. BFS guarantees the minimum introductions needed to reach anyone, making it the natural fit for social graph queries.
BFS
GPS & Routing — Unweighted Shortest Hops
Road network = graph. Intersections = nodes. Roads = edges. BFS finds the fewest turns (hops). For time/distance-weighted roads, Dijkstra extends BFS by replacing the plain queue with a min-priority queue — but the traversal skeleton is identical.
DFS
Dependency Resolution — pip install
Every Python package depends on packages which have their own dependencies. Topological sort via DFS post-order ensures package A is installed before anything that requires it. A cycle in the dependency graph = DFS detects it and raises an error instead of hanging forever.
DFS
Compiler — Dead Code Elimination
Source code compiles into a Control Flow Graph (CFG). DFS from the entry point marks all reachable basic blocks. Blocks never reached = unreachable (dead) code. The compiler safely removes them — DFS is the foundation of this optimisation pass in GCC, Clang, and the JVM.
BFS
Game AI — Maze & Pathfinding
A 2D grid is just a graph — cells are nodes, valid moves are edges. BFS finds the shortest path through any maze. Combined with a heuristic (A* algorithm), it becomes the gold standard in game engines (Unity, Unreal), robotics, and autonomous vehicles.

Section 15 — Reference

Complete Graph Type × Algorithm Reference

Graph TypeBFS Works?DFS Works?Notes
Undirected, unweightedYesYesBFS for shortest path; DFS for components/cycles.
Directed, unweightedYesYesDFS needed for topo sort and directed cycle detection.
Weighted (non-negative)Traversal onlyTraversal onlyUse Dijkstra's for shortest path.
Weighted (negative edges)Not for shortest pathNot for shortest pathUse Bellman-Ford.
DAGYes (Kahn's for topo)Yes (post-order topo)No cycle detection needed — DAGs are by definition acyclic.
DisconnectedOuter loop neededOuter loop neededLoop over all nodes; start BFS/DFS on each unvisited one.
TreeLevel-orderPre/In/Post-orderTrees are just acyclic connected graphs — both work natively.

Section 16 — Golden Rules

Golden Rules

🏆 Graphs, BFS & DFS — Non-Negotiable Rules
1
Always mark nodes visited before enqueuing (BFS) or immediately on pop (DFS iterative). In BFS, marking on enqueue prevents the same node from entering the queue multiple times. In iterative DFS, marking on pop (with a continue-if-visited check) handles the case where a node was pushed twice before being processed.
2
Use collections.deque for BFS — never list.pop(0). list.pop(0) is O(n); deque.popleft() is O(1). On a 100,000-node graph, the wrong choice turns an O(V+E) algorithm into O(V²) — hours instead of milliseconds.
3
BFS guarantees shortest path only on unweighted graphs. The moment edges have weights, use Dijkstra's (min-heap + BFS skeleton). BFS with weights finds the path with fewest hops, not least cost — these are completely different things.
4
For recursive DFS on large inputs, convert to iterative or raise the limit. Python's default recursion limit is 1,000. A binary tree with 10,000 nodes will crash. Use sys.setrecursionlimit() for quick fixes, or convert to an explicit stack for production code. Iterative DFS is always safer.
5
Disconnected graphs require an outer loop. A single BFS/DFS call only visits the component containing the start node. To cover the full graph: loop over every node and call BFS/DFS only if unvisited. Each separate call = exactly one connected component.
6
Directed cycle detection uses a recursion stack, not just visited. Seeing a visited neighbour in an undirected graph = cycle. In a directed graph, it means only a cross-edge (no cycle). The cycle is confirmed only when you reach a node that is currently in the active DFS call stack (back-edge). Confusing these is the most common graph bug.