Graph Traversal Algorithms: BFS and DFS

Graph traversal is a cornerstone of computer science, enabling the exploration of nodes and edges in graphs. Two fundamental traversal strategies are Breadth‑First Search (BFS) and Depth‑First Search (DFS). This tutorial walks you through their theory, implementations in multiple programming languages, complexity analysis, and practical tips for real‑world applications.

Breadth‑First Search (BFS) Overview

BFS explores a graph level by level, starting from a source vertex and visiting all its immediate neighbors before moving to the next depth level. It is particularly useful for finding the shortest path in unweighted graphs, checking bipartiteness, and performing level order traversal in trees.

BFS Algorithm Steps

  1. Enqueue the source vertex and mark it as visited.
  2. While the queue is not empty:
  3.  Dequeue a vertex u.
  4.  For each unvisited neighbor v of u:
  5.   Mark v as visited and enqueue v.

Iterative BFS Implementation – Python

from collections import deque

def bfs(graph, start):
    visited = set()
    order = []
    q = deque([start])
    visited.add(start)
    while q:
        vertex = q.popleft()
        order.append(vertex)
        for neighbor in graph[vertex]:
            if neighbor not in visited:
                visited.add(neighbor)
                q.append(neighbor)
    return order

# Example usage
graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': ['F'],
    'F': []
}
print(bfs(graph, 'A'))  # Output: ['A', 'B', 'C', 'D', 'E', 'F']

Iterative BFS Implementation – Java

import java.util.*;

public class BFS {
    public static List<Integer> bfs(Map<Integer, List<Integer>> graph, int start) {
        Set<Integer> visited = new HashSet<>();
        List<Integer> order = new ArrayList<>();
        Queue<Integer> queue = new LinkedList<>();
        queue.add(start);
        visited.add(start);
        while (!queue.isEmpty()) {
            int node = queue.poll();
            order.add(node);
            for (int neighbor : graph.getOrDefault(node, Collections.emptyList())) {
                if (!visited.contains(neighbor)) {
                    visited.add(neighbor);
                    queue.add(neighbor);
                }
            }
        }
        return order;
    }

    public static void main(String[] args) {
        Map<Integer, List<Integer>> graph = new HashMap<>();
        graph.put(1, Arrays.asList(2, 3));
        graph.put(2, Arrays.asList(4, 5));
        graph.put(3, Arrays.asList(6));
        graph.put(4, Collections.emptyList());
        graph.put(5, Arrays.asList(6));
        graph.put(6, Collections.emptyList());
        System.out.println(bfs(graph, 1)); // [1, 2, 3, 4, 5, 6]
    }
}

Iterative BFS Implementation – C++

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_set>

using namespace std;

vector<int> bfs(const vector<vector<int>>& adj, int start) {
    unordered_set<int> visited;
    vector<int> order;
    queue<int> q;
    q.push(start);
    visited.insert(start);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        order.push_back(u);
        for (int v : adj[u]) {
            if (visited.find(v) == visited.end()) {
                visited.insert(v);
                q.push(v);
            }
        }
    }
    return order;
}

int main() {
    vector<vector<int>> graph = {
        {},          // 0 (unused)
        {2,3},       // 1
        {4,5},       // 2
        {6},         // 3
        {},          // 4
        {6},         // 5
        {}           // 6
    };
    vector<int> result = bfs(graph, 1);
    for (int v : result) cout << v << " "; // 1 2 3 4 5 6
    return 0;
}

BFS Time & Space Complexity

  • Time Complexity: O(V + E) – each vertex and edge is processed once.
  • Space Complexity: O(V) for the visited set and the queue.

Depth‑First Search (DFS) Overview

DFS dives as deep as possible along each branch before backtracking. It can be implemented recursively or iteratively (using a stack). DFS is ideal for tasks such as topological sorting, detecting cycles, solving puzzles, and exploring connected components.

DFS Algorithm Steps (Recursive)

  1. Visit the current vertex u and mark it as visited.
  2. For each neighbor v of u:
  3.  If v is not visited, recursively perform DFS on v.

Recursive DFS Implementation – Python

def dfs(graph, start, visited=None, order=None):
    if visited is None:
        visited = set()
    if order is None:
        order = []
    visited.add(start)
    order.append(start)
    for neighbor in graph[start]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited, order)
    return order

# Example usage
graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': ['F'],
    'F': []
}
print(dfs(graph, 'A'))  # Output: ['A', 'B', 'D', 'E', 'F', 'C']

Iterative DFS Implementation – Java

import java.util.*;

public class DFS {
    public static List<Integer> dfs(Map<Integer, List<Integer>> graph, int start) {
        Set<Integer> visited = new HashSet<>();
        List<Integer> order = new ArrayList<>();
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(start);
        while (!stack.isEmpty()) {
            int node = stack.pop();
            if (!visited.contains(node)) {
                visited.add(node);
                order.add(node);
                // push neighbors in reverse order for natural ordering
                List<Integer> neighbors = graph.getOrDefault(node, Collections.emptyList());
                for (int i = neighbors.size() - 1; i >= 0; i--) {
                    int neighbor = neighbors.get(i);
                    if (!visited.contains(neighbor)) {
                        stack.push(neighbor);
                    }
                }
            }
        }
        return order;
    }

    public static void main(String[] args) {
        Map<Integer, List<Integer>> graph = new HashMap<>();
        graph.put(1, Arrays.asList(2, 3));
        graph.put(2, Arrays.asList(4, 5));
        graph.put(3, Arrays.asList(6));
        graph.put(4, Collections.emptyList());
        graph.put(5, Arrays.asList(6));
        graph.put(6, Collections.emptyList());
        System.out.println(dfs(graph, 1)); // [1, 2, 4, 5, 6, 3]
    }
}

DFS Time & Space Complexity

  • Time Complexity: O(V + E) – same as BFS because each vertex and edge is examined once.
  • Space Complexity: O(V) for the visited set and recursion stack (or explicit stack).

BFS vs DFS – Quick Comparison

PropertyBFSDFS
Exploration OrderLevel by level (breadth)Depth first (branch)
Typical Use‑CasesShortest path in unweighted graph, level order traversalTopological sort, cycle detection, puzzle solving
Data Structure UsedQueue (FIFO)Stack (LIFO) – recursive call stack or explicit stack
Memory ConsumptionHigher for wide graphsHigher for deep graphs (recursion depth)
“Breadth‑first search is the most obvious method to find the shortest path in an unweighted graph.” – Robert Sedgewick
⚠ Warning: When implementing DFS recursively on very deep graphs, you may encounter a stack overflow. Consider using an explicit stack or increasing recursion limits if needed.
💡 Tip: Store the parent of each visited node during BFS to reconstruct the shortest path after the traversal finishes.
📝 Note: Both BFS and DFS assume the graph is stored in an adjacency list for optimal performance. An adjacency matrix leads to O(V²) time, which is inefficient for sparse graphs.

Practical Example – Finding the Shortest Path with BFS

Below is a complete Python function that returns the shortest path between two vertices in an unweighted graph using BFS.

from collections import deque

def bfs_shortest_path(graph, start, goal):
    visited = set([start])
    parent = {start: None}
    q = deque([start])
    while q:
        cur = q.popleft()
        if cur == goal:
            # reconstruct path
            path = []
            while cur is not None:
                path.append(cur)
                cur = parent[cur]
            return path[::-1]
        for nxt in graph[cur]:
            if nxt not in visited:
                visited.add(nxt)
                parent[nxt] = cur
                q.append(nxt)
    return None  # no path found

# Example graph
graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': ['F'],
    'F': []
}
print(bfs_shortest_path(graph, 'A', 'F'))  # Output: ['A', 'C', 'F']

Summary

📘 Summary: BFS and DFS are essential graph traversal techniques. BFS uses a queue to explore vertices level‑by‑level and excels at finding shortest paths in unweighted graphs. DFS leverages a stack (or recursion) to explore deep branches, making it ideal for tasks like topological sorting and connectivity analysis. Both run in O(V+E) time, but their memory footprints differ based on graph shape. Understanding when to apply each algorithm—and handling pitfalls such as recursion depth—empowers software engineers to solve a wide range of algorithmic problems efficiently.

Frequently Asked Questions

Q: Can BFS be used on weighted graphs?
A: BFS finds the shortest path only in unweighted or uniformly weighted graphs. For weighted graphs, algorithms like Dijkstra’s or Bellman‑Ford are appropriate.


Q: Is DFS guaranteed to visit all vertices in a disconnected graph?
A: No. A single DFS call explores only the component containing the start vertex. To visit every vertex, run DFS from each unvisited vertex (or use a loop that iterates over all vertices).


Q: What is the difference between tree DFS and graph DFS?
A: In a tree, there are no cycles, so a simple recursive DFS suffices. In a general graph you must maintain a visited set to avoid infinite loops caused by cycles.


Quick Quiz

Q. Which data structure does BFS primarily rely on?
  • Stack
  • Queue
  • Priority Queue
  • HashMap

Answer: Queue
BFS uses a FIFO queue to ensure vertices are processed in the order they are discovered, achieving level‑order exploration.

Q. What is the worst‑case space complexity of recursive DFS on a graph with V vertices?
  • O(1)
  • O(log V)
  • O(V)
  • O(E)

Answer: O(V)
The recursion stack can grow up to the depth of the graph, which in the worst case equals the number of vertices.

Further Reading & References

References

Explore more graph algorithms on our platform