Depth-First Search Algorithm Explained with Examples

By · · Algorithms

If you've ever tried to solve a maze by following one path as far as possible before backtracking, you already understand DFS intuitively. That's literally the strategy. Go deep, hit a dead end, back up, try the next option.

Depth-First Search shows up everywhere in programming. From detecting cycles in dependency graphs to solving puzzles to figuring out if two nodes are connected. It's one of the first graph algorithms worth learning, and honestly one of the most useful.

Interactive visualization

Use the controls below to step through the algorithm or let it play automatically.

name: dfs
input: {"nodes": ["A", "B", "C", "D", "E", "F", "G"], "edges": [["A", "B"], ["A", "C"], ["B", "D"], ["B", "E"], ["C", "F"], ["C", "G"]], "start": "A"}
speed: 700

How DFS actually works

The idea is simple. You start at some node, pick a neighbor, and keep going deeper until you can't anymore. Then you backtrack to the last node that had unexplored neighbors and repeat.

You can implement this with either recursion (which uses the call stack) or an explicit stack data structure. Both give you the same traversal order.

Here's a graph we'll work through:

Starting from node A, DFS visits nodes in this order: A → B → D → H → E → C → F → G

Notice how it goes all the way down through A → B → D → H before it ever touches C. That's the "depth-first" part.

Step-by-step stack walkthrough

Let's trace through the algorithm manually. This is the part that makes everything click.

Here's what happens at each step:

Step Pop from Stack Push Neighbors Stack After Visited So Far
1 A B, C [C, B] {A}
2 B D, E [C, E, D] {A, B}
3 D H [C, E, H] {A, B, D}
4 H (none new) [C, E] {A, B, D, H}
5 E (none new) [C] {A, B, D, H, E}
6 C F, G [G, F] {A, B, D, H, E, C}
7 F (none new) [G] {A, B, D, H, E, C, F}
8 G (none new) [] {A, B, D, H, E, C, F, G}

Stack empty. We're done. Every node visited exactly once.

Python implementation

Here's both the iterative and recursive versions. I'd suggest understanding the iterative one first since it makes the stack mechanics explicit.

Iterative DFS (using a stack)

def dfs_iterative(graph, start):
    visited = set()
    stack = [start]
    order = []

    while stack:
        node = stack.pop()
        if node in visited:
            continue

        visited.add(node)
        order.append(node)

        # Add neighbors in reverse order so we visit
        # the first neighbor first (stack is LIFO)
        for neighbor in reversed(graph[node]):
            if neighbor not in visited:
                stack.append(neighbor)

    return order


# Example graph as an adjacency list
graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F', 'G'],
    'D': ['H'],
    'E': [],
    'F': [],
    'G': [],
    'H': []
}

print(dfs_iterative(graph, 'A'))
# Output: ['A', 'B', 'D', 'H', 'E', 'C', 'F', 'G']

Recursive DFS

def dfs_recursive(graph, node, visited=None):
    if visited is None:
        visited = set()

    visited.add(node)
    print(node, end=' ')

    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs_recursive(graph, neighbor, visited)

    return visited

The recursive version is cleaner, but keep in mind Python has a default recursion limit of 1000. For large graphs, either increase it with sys.setrecursionlimit() or just use the iterative version.

DFS on a graph with cycles

Real-world graphs usually have cycles. So you need the visited set to avoid infinite loops. Without it, you'd keep going around in circles forever.

def has_cycle(graph):
    """Detect if a directed graph has a cycle using DFS."""
    WHITE, GRAY, BLACK = 0, 1, 2
    color = {node: WHITE for node in graph}

    def visit(node):
        color[node] = GRAY
        for neighbor in graph[node]:
            if color[neighbor] == GRAY:
                return True  # Back edge found, cycle exists
            if color[neighbor] == WHITE and visit(neighbor):
                return True
        color[node] = BLACK
        return False

    return any(visit(node) for node in graph if color[node] == WHITE)

The three-color approach is standard for cycle detection. White means unvisited, gray means currently being explored (still on the stack), and black means fully processed. If you hit a gray node, you've found a cycle.

When should you use DFS?

DFS is your go-to when you need to explore all possibilities or when the solution is likely deep in the search space.

Maze solving

Think of each cell as a node and each passable direction as an edge. DFS will follow one path until it hits a wall, then backtrack. It won't find the shortest path, but it will find a path if one exists.

Topological sorting

When you have tasks with dependencies (like build systems or course prerequisites), DFS gives you a valid ordering. You just record nodes in the order they finish processing, then reverse that list.

def topological_sort(graph):
    visited = set()
    result = []

    def dfs(node):
        visited.add(node)
        for neighbor in graph[node]:
            if neighbor not in visited:
                dfs(neighbor)
        result.append(node)

    for node in graph:
        if node not in visited:
            dfs(node)

    return result[::-1]

Finding connected components

Need to know which nodes can reach each other? Run DFS from an unvisited node, mark everything it touches as one component, then repeat from the next unvisited node.

Path finding in trees

DFS is natural for tree problems. Finding a path between two nodes, checking if a value exists, computing depth. Trees are just graphs without cycles, so DFS works beautifully.

Time and space complexity

Aspect Complexity Why
Time O(V + E) We visit every vertex once and check every edge once
Space O(V) The stack (or recursion depth) can hold at most V nodes

Where V is the number of vertices and E is the number of edges.

For dense graphs where E approaches V², the time complexity effectively becomes O(V²). For sparse graphs (like trees where E = V - 1), it's closer to O(V).

DFS vs BFS: quick comparison

Pick DFS when you want to go deep quickly, detect cycles, or do topological sorting. Pick BFS when you need shortest paths in unweighted graphs. They're both O(V + E) in time, so performance isn't the deciding factor. It's about what kind of answer you need.

One practical tip: if your graph is very deep but not very wide, DFS might use less memory than BFS. If it's very wide but shallow, BFS wins on memory. Think about the shape of your data.

That's DFS. Simple concept, tons of applications. Once you've got this down, you'll start seeing it everywhere.