Graph Traversal Algorithms: BFS, DFS, Dijkstra & A* Search
Understand graph traversal and pathfinding algorithms. Compare BFS, DFS, Dijkstra, and A* search with interactive visuals and code samples.

Graph algorithms form the backbone of modern computer science. From social network recommendation engines and network routing protocols to GPS navigation systems (Google Maps) and game engine AI navigation meshes, efficient graph traversal is an essential engineering skill.
However, developers frequently struggle with algorithm selection: When is simple Breadth-First Search optimal? How does Dijkstra guarantee the shortest path on weighted graphs? Why does the A* heuristic outperform Dijkstra in spatial navigation?
This comprehensive guide visualizes and breaks down the four foundational graph traversal algorithms: Breadth-First Search (BFS), Depth-First Search (DFS), Dijkstra's Algorithm, and A* (A-Star) Pathfinding.
💡 Key Takeaways (TL;DR)#
- BFS (Breadth-First Search): Explores level-by-level using a FIFO Queue. Guarantees the shortest path on unweighted graphs with O(V + E) time complexity.
- DFS (Depth-First Search): Explores deep along branches using a LIFO Stack or recursion. Ideal for cycle detection, topological sorting, and maze solving, but does not guarantee the shortest path.
- Dijkstra's Algorithm: Expands lowest-cost frontier nodes using a Min-Priority Queue. Guarantees the shortest path on non-negative weighted graphs with O((V + E) log V) complexity.
- A* (A-Star) Search: Enhances Dijkstra by incorporating an admissible heuristic function
h(n)(e.g., Euclidean or Manhattan distance). Explores dramatically fewer nodes by aiming toward the target goal:f(n) = g(n) + h(n).
1. The Graph Traversal Taxonomy#
Every graph search algorithm maintains a frontier of discovered nodes, but the underlying data structure dictates their traversal behavior and optimal use case.
Algorithm Comparison Matrix#
| Algorithm | Frontier Data Structure | Guarantees Shortest Path? | Supports Weighted Edges? | Time Complexity | Space Complexity |
|---|---|---|---|---|---|
| BFS | FIFO Queue | Yes (Unweighted only) | No | O(V + E) | O(V) |
| DFS | LIFO Stack / Call Stack | No | No | O(V + E) | O(V) (Recursion depth) |
| Dijkstra | Min-Priority Queue | Yes (Non-negative) | Yes | O((V + E) log V) | O(V) |
| A* Search | Min-Priority Queue | Yes (If h(n) is admissible) | Yes | O(E) to O(V log V) | O(V) |
2. Unweighted Graphs: BFS vs. DFS#
Breadth-First Search (BFS) Mechanics#
BFS expands outwards in concentric ripples from the starting vertex. It inspects all immediate neighbors at depth 1 before evaluating depth 2.
TypeScript BFS Implementation
export function breadthFirstSearch(
adjList: Map<string, string[]>,
startNode: string,
targetNode: string
): string[] | null {
const queue: string[] = [startNode];
const visited = new Set<string>([startNode]);
const parentMap = new Map<string, string>();
while (queue.length > 0) {
const current = queue.shift()!;
if (current === targetNode) {
// Reconstruct shortest path
const path: string[] = [];
let curr: string | undefined = targetNode;
while (curr) {
path.unshift(curr);
curr = parentMap.get(curr);
}
return path;
}
for (const neighbor of adjList.get(current) || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
parentMap.set(neighbor, current);
queue.push(neighbor);
}
}
}
return null; // Target unreachable
}
Depth-First Search (DFS) Mechanics#
DFS plunges as deeply as possible along each branch before backtracking. While inefficient for shortest paths, DFS is essential for:
- Topological Sorting (build systems, dependency resolution).
- Detecting Cycles in Directed Graphs (deadlock detection).
- Finding Connected Components (island counting algorithms).
3. Weighted Graphs: Dijkstra's Algorithm#
When edges have non-uniform weights (representing road distance, network latency, or toll costs), BFS fails because a path with fewer hops might have a higher total weight.
Dijkstra's Algorithm maintains the shortest known distance from the source to every discovered node, greedily selecting the lowest-distance candidate using a Min-Heap.
In the diagram above:
- BFS would choose path
A -> B -> D(2 hops, total weight = 9). - Dijkstra correctly discovers
A -> C -> B -> D(3 hops, total weight = 8).
4. Informed Search: A* (A-Star) Pathfinding#
While Dijkstra is mathematically guaranteed to find the shortest path, it explores nodes uniformly in all directions (like an expanding circle), wasting compute on paths moving away from the target.
A* Search introduces a heuristic function h(n) that estimates the remaining distance to the goal.
Total Estimated Cost: f(n) = g(n) + h(n)
g(n): Exact accumulated cost from the start node to noden.h(n): Estimated heuristic cost from nodento the goal.
Common Heuristic Functions#
- Manhattan Distance (Grid with 4-directional movement: Up, Down, Left, Right):
Code
h(n) = |x1 - x2| + |y1 - y2| - Euclidean Distance (Grid with continuous / any-angle movement):
Code
h(n) = sqrt((x1 - x2)^2 + (y1 - y2)^2) - Admissibility Rule: A heuristic is admissible if it never overestimates the true remaining cost (
h(n) <= h*(n)). Ifh(n)is admissible, A* is guaranteed to return the optimal shortest path.
Python A* Pathfinding Implementation#
import heapq
import math
def heuristic(a, b):
# Euclidean distance heuristic
return math.hypot(a[0] - b[0], a[1] - b[1])
def a_star_search(grid, start, goal):
# Priority Queue stores: (f_score, current_node)
frontier = []
heapq.heappush(frontier, (0, start))
came_from = {start: None}
g_score = {start: 0}
while frontier:
_, current = heapq.heappop(frontier)
if current == goal:
# Reconstruct path
path = []
while current:
path.append(current)
current = came_from[current]
return path[::-1]
# Check 4-directional neighbors
x, y = current
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
neighbor = (x + dx, y + dy)
# Boundary & obstacle check
if 0 <= neighbor[0] < len(grid) and 0 <= neighbor[1] < len(grid[0]):
if grid[neighbor[0]][neighbor[1]] == 1: # 1 = Wall/Obstacle
continue
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
g_score[neighbor] = tentative_g
f_score = tentative_g + heuristic(neighbor, goal)
heapq.heappush(frontier, (f_score, neighbor))
came_from[neighbor] = current
return None # No path found
5. Architectural Decision Guide: Which Algorithm to Choose?#
When building production features, follow this decision framework:
- Peer-to-Peer & Social Networks (e.g., 6 Degrees of Separation):
- Use Bi-directional BFS. Running two simultaneous BFS searches from both start and target converges in
O(b^(d/2))time instead ofO(b^d).
- Use Bi-directional BFS. Running two simultaneous BFS searches from both start and target converges in
- Package Dependency Resolvers (e.g., npm / Cargo):
- Use DFS with Kahn's Algorithm / Post-order Traversal for topological sorting and circular dependency detection.
- Network Routing Protocols (e.g., OSPF / IS-IS):
- Use Dijkstra's Algorithm to compute the shortest link-state routing tables across internet routers.
- Video Game Navigation Meshes & Robotics:
- Use A* Search (or Hierarchical Pathfinding HPA*) on spatial grids to achieve 60 FPS real-time character movement.
Frequently Asked Questions#
Can Dijkstra's algorithm handle negative edge weights?#
No. Dijkstra assumes that once a node is visited, its shortest distance is finalized. Negative edge weights violate this assumption and can cause infinite loops or incorrect paths. For graphs with negative weights, use the Bellman-Ford Algorithm (O(V * E)).
What happens if the A* heuristic is always set to 0?#
If h(n) = 0 for all nodes, f(n) = g(n) + 0 = g(n). A* degrades exactly into Dijkstra's Algorithm, exploring blindly in all directions.
What is the difference between an Admissible and a Consistent heuristic?#
An admissible heuristic never overestimates the true remaining cost. A consistent (or monotonic) heuristic satisfies the triangle inequality: h(A) <= c(A, B) + h(B). All consistent heuristics are admissible and guarantee that no node needs to be re-evaluated once visited.
Joey Jazwinski
Hi, I'm Joey — a software engineer building modern applications, exploring artificial intelligence, and sharing my journey through code. 🚀
Recommended Articles
View all posts →
Demystifying MongoDB Connection Strings: URI Parameters, Replica Sets, and TLS
A complete engineering guide to MongoDB connection URIs. Learn DNS SRV lookups, replica set topology discovery, write concerns, pool sizing, and TLS options.

How QR Codes Work Under the Hood: Reed-Solomon Error Correction, Mask Patterns, and Matrix Encodings
Explore the computer science and math behind Quick Response codes. Learn 1:1:3:1:1 finder patterns, Galois field error correction, mask formulas, and URI schemes.

Runtime Type Validation in TypeScript: Zod Architecture, Static Type Inference, and API Boundary Defense
Master runtime validation in TypeScript. Learn how Zod executes schema parsing, derives static types with z.infer, and protects API boundaries against invalid input.