Breadth-first search (BFS) vs Depth-first search (DFS) vs Dijkstra

Three algorithms, three different shapes of graph problem. BFS walks outward in concentric rings. DFS plunges to the bottom and backtracks. Dijkstra walks outward in distance order — like BFS, but with weighted edges. Pick the wrong one and you either get the wrong answer or pay a complexity penalty for nothing.

At a glance

Breadth-first search (BFS) Depth-first search (DFS) Dijkstra
Frontier data structure Queue (FIFO) — collections.deque Stack (LIFO) — list, or recursion Min-heap (priority queue) — heapq
Time complexity O(V + E) O(V + E) O((V + E) log V)
Space complexity O(V) — frontier can be wide O(V) — recursion / stack depth O(V) — heap + dist map
Edge weights Unweighted (every edge = 1) Unweighted (weights ignored) Non-negative weights only
Shortest-path guarantee Yes — fewest edges No — finds a path, not the shortest Yes — minimum weight
Visit order Level-by-level from source Deep first, then backtrack Closest unvisited node next
Memory pattern Wide — entire BFS frontier Narrow — one path's depth Heap of pending distances
Handles negative weights N/A N/A No — use Bellman-Ford or SPFA
Handles cycles Yes (with visited set) Yes (with visited set) Yes
Iterative or recursive? Iterative (queue) Either; recursion is cleaner Iterative (heap loop)

When to use each

Reach for Breadth-first search (BFS) when…

Reach for Depth-first search (DFS) when…

Reach for Dijkstra when…

Same problem, three implementations

Find a path from A to F in this graph. BFS and DFS treat edges as unweighted; Dijkstra uses the weights. Each one tells a different story about the graph.

graph = {
    "A": [("B", 1), ("C", 4)],
    "B": [("C", 2), ("D", 5)],
    "C": [("D", 1)],
    "D": [("E", 3), ("F", 10)],
    "E": [("F", 1)],
    "F": [],
}


# ----- BFS: fewest edges -----
from collections import deque

def bfs_path(graph, start, goal):
    parents = {start: None}
    q = deque([start])
    while q:
        u = q.popleft()
        if u == goal:
            path = []
            while u is not None:
                path.append(u); u = parents[u]
            return path[::-1]
        for v, _ in graph[u]:
            if v not in parents:
                parents[v] = u
                q.append(v)
    return None


# ----- DFS: any path, not necessarily shortest -----
def dfs_path(graph, start, goal):
    visited, stack = set(), [(start, [start])]
    while stack:
        u, path = stack.pop()
        if u == goal:
            return path
        if u in visited:
            continue
        visited.add(u)
        for v, _ in graph[u]:
            if v not in visited:
                stack.append((v, path + [v]))
    return None


# ----- Dijkstra: minimum total weight -----
import heapq

def dijkstra_path(graph, start, goal):
    dist = {start: 0}
    parents = {start: None}
    pq = [(0, start)]
    while pq:
        d, u = heapq.heappop(pq)
        if u == goal:
            path = []
            while u is not None:
                path.append(u); u = parents[u]
            return path[::-1], d
        if d > dist[u]:
            continue
        for v, w in graph[u]:
            nd = d + w
            if nd < dist.get(v, float("inf")):
                dist[v] = nd
                parents[v] = u
                heapq.heappush(pq, (nd, v))
    return None, float("inf")


print(bfs_path(graph, "A", "F"))       # ['A', 'C', 'D', 'F']  — 3 edges
print(dfs_path(graph, "A", "F"))       # some path (depends on neighbor order)
print(dijkstra_path(graph, "A", "F"))  # (['A', 'B', 'C', 'D', 'E', 'F'], 8)

Notice the trade-off: BFS finds the path with fewest edges (3 hops, total weight 15 if you compute it). Dijkstra finds the path with minimum total weight (5 hops, weight 8) — longer in hops, cheaper in distance. DFS makes no claim about either; it just finds some reachable path.

Performance characteristics

Time

BFS and DFS are both O(V + E): every vertex enters and leaves the frontier once, and every edge is examined once. They are tied as the cheapest possible graph traversal — you can't do less than visit each vertex and each edge.

Dijkstra adds a log V factor from the binary heap: each heappush and heappop is O(log V), and across all iterations there are O(V + E) heap operations. With a Fibonacci heap the bound improves to O(E + V log V), but the constant factor wipes out the gain in practice and almost no real-world implementation uses one.

Space

All three are O(V) in the worst case, but the constant differs by problem shape:

Cache behavior

Adjacency-list representations dominate in Python (dict of lists). All three algorithms make pointer-chasing-style accesses, so cache locality is mostly determined by the graph layout, not the algorithm. For dense graphs an adjacency matrix can help BFS/DFS (sequential rows) but hurts Dijkstra (sparse heap operations don't benefit).

Choosing the right algorithm

  1. Are edges weighted?
  2. Do you need the shortest path, or just any path?
  3. Do you need a structural property (cycles, ordering, components)?
  4. Is the graph deep but narrow, or wide but shallow?
  5. Have a goal-direction heuristic available?

Beyond these three

BFS, DFS, and Dijkstra are the foundations; most "graph algorithms" you encounter are extensions or hybrids:

Trade-off summary

If you remember nothing else: BFS for hops, Dijkstra for weights, DFS for structure.

BFS · DFS · Dijkstra · ← Back to Algorithms