Depth-First Search (DFS) Theory Guide

Try the Depth-First Search (DFS) Solver →
Beginner8 min readLast Updated June 26, 2026
Prerequisites:Graphs and Trees, Stacks (LIFO)
Depth-First SearchDFS AlgorithmLIFO StackUninformed SearchGraph TraversalBacktrackingArtificial Intelligence

Picture an explorer in a dark cave with a ball of string. Instead of checking every passage entrance first, they sprint down the very first corridor and only follow the string back when they hit a dead end. That is DFS — total commitment to one path, retreat only when forced.

  • Plunging Deep Before Looking Around: DFS follows one path all the way to a dead end before backtracking. It never checks alternatives until the current route is completely exhausted.
  • Memory-Efficient Backtracking: DFS only stores the current path in memory using a Stack — not an entire level like BFS. This makes it dramatically kinder to RAM on deep graphs.
  • The Shortest Path Risk: DFS returns the first path it finds, not the shortest one. It explores thoroughly, but never optimally.

DFS powers maze solving, topological sorting, cycle detection, and finding connected components — anywhere exhaustive exploration matters more than finding the fastest route.

How to Trace DFS by Hand

1

Set up your `openList` (Stack) and `closedList` (Visited) before anything else. Draw two columns on your exam paper. DFS uses a Last-In, First-Out Stack — the most recently added node is always the first one processed. Place the start node at the top of the Stack and leave the `closedList` empty. This LIFO rule is the single mechanic that makes DFS plunge deep instead of sweeping wide.

2

Pop from the TOP of the Stack — this is the entire algorithm. Take the node sitting at the very top of the `openList` and make it the current node. Critical exam warning: if the node is ever popped from the bottom or front of the list, the algorithm has silently become BFS. Always pop from the top, every single time.

3

Check immediately if the popped node is the goal. The moment a node is popped from the Stack, ask: is this the destination? If yes, terminate immediately and trace the path back through parent pointers. Performing this check after popping — not after adding to the Stack — keeps the logic consistent with every other search algorithm.

4

Move the node to the `closedList` and expand its neighbours. Write the current node into the `closedList` — it is permanently processed. Now look at all unvisited neighbours not already in either list. Add them to the top of the Stack in reverse alphabetical order, so the alphabetically first neighbour ends up on top and gets processed next.

5

Repeat until the goal is found or the Stack is completely empty. Keep popping from the top, checking for the goal, locking into the `closedList`, and stacking new neighbours. Because the newest nodes always jump to the top of the line, DFS keeps diving deeper into the current branch before ever considering a sibling — that is the plunging behaviour in action.

The Engine of DFS: The LIFO Stack

Next Node=Most Recently Added Node\text{Next Node} = \text{Most Recently Added Node}

Breaking Down the Mechanics & The Plunge Effect

  • The LIFO Stack — Last In, First Out: DFS is the opposite of a checkout queue. The node that was added most recently to the `openList` is the very first one to be explored next. This Last-In-First-Out rule is the complete mechanical explanation for why DFS dives deep instead of spreading wide — the newest discovery always jumps to the front of the line.
  • No Math, Just Order: Like BFS, DFS performs zero calculations to decide what to expand next. There are no heuristic values to look up, no g(n)g(n) to accumulate, and no scores to sort. The algorithm is completely blind to cost and distance. Its entire decision-making process reduces to one physical rule: whoever was added to the Stack most recently goes next.
  • The Plunge Effect — Why the Stack Forces Depth: When a node is expanded and its neighbours are added to the top of the Stack, those brand-new neighbours immediately jump ahead of every sibling node already waiting below them. The algorithm is mechanically forced to ignore older candidates and dive straight into the freshest branch. This is not a design choice — it is an unavoidable consequence of pushing to the top of a LIFO Stack.
  • The Backtracking Trigger — No Calculation Required: Backtracking in DFS is not a deliberate strategic decision — it is a mechanical fallback. When a popped node has no unvisited neighbours left to push onto the Stack, the algorithm simply pops the next node already waiting below it. That waiting node is the most recent branching point — the exact location where the last unexplored path diverged. Backtracking happens automatically whenever the current branch runs dry.

Solved Example: Tracing DFS on Paper

Draw this graph on your paper before reading the steps. Nodes: S (Start), A, B, C, G (Goal). Edges are unweighted: S→A, S→B, A→C, C→G. There are no heuristics and no edge weights — DFS is completely blind to both. Draw two columns and label them `openList` (Stack) and `closedList` (Visited). Remember the two golden rules: newest nodes always go to the top of the Stack, and every pop comes from the top.

Step 1: Initialize the Start Node S

Push S onto the top of the Stack. The `closedList` is empty. There is no score to calculate and no heuristic to look up — just place S on the Stack and begin. The `openList` currently holds [S] with S on top.

Step 2: Pop S — Push Neighbours B then A

S is on top, so pop it and move it to the `closedList`. S connects to A and B. To ensure A gets processed first (alphabetical order), push B onto the Stack first, then push A on top of it. The `openList` now holds [A, B] with A sitting on top. The `closedList` holds [S]. This is the plunge beginning — DFS is about to dive straight down the A branch without touching B.

Step 3: Pop A — Push Neighbour C

A is on top of the Stack, so pop it and move it to the `closedList`. A connects to C. Check both lists — C is in neither, so push C onto the top of the Stack. The `openList` now holds [C, B] with C on top. The `closedList` holds [S, A]. DFS has already ignored B entirely and is diving deeper into the A branch.

Step 4: Pop C — Push Neighbour G

C is on top of the Stack, so pop it and move it to the `closedList`. C connects to G. Check both lists — G is in neither, so push G onto the top of the Stack. The `openList` now holds [G, B] with G on top. The `closedList` holds [S, A, C]. The plunge is nearly complete — DFS has gone three levels deep without ever touching B.

Step 5: Pop G — Goal Found, Terminate

G is on top of the Stack, so pop it. Immediately check — G is the goal node. The search terminates right here. The path found is S → A → C → G. Notice that node B was never visited at all — it sat at the bottom of the Stack the entire time while DFS plunged straight down the left branch. This is the defining behaviour of DFS: one full commitment to a single path before anything else gets a turn.

See the DFS Plunge in Action

Now that the Stack logic makes sense on paper, watch the solver show the plunge in real time. See the Stack grow deeper with every step and watch DFS ignore entire branches while it commits fully to one path — a completely different visual from BFS's slow, cautious ripple.

Rules & Common Mistakes

  • Exam Trap: Pulling from the Front Turns DFS into BFS
    This is the single fastest way to lose all traversal marks on a DFS question. The Stack is a Last-In-First-Out structure — the node that was added most recently must always be the first one popped. The moment a node is pulled from the bottom or front of the list instead of the top, the algorithm silently becomes BFS and every subsequent step produces the wrong traversal order. Always mark the top of the Stack clearly on the exam paper and cross off nodes strictly from that end.
  • Exam Trap: Pushing Neighbours in Alphabetical Order Instead of Reverse
    When a node has multiple new neighbours to push, the order they enter the Stack determines which one gets processed first. Since DFS pops from the top, the neighbour that should be explored first must be the last one pushed — meaning neighbours must be pushed in reverse alphabetical order. If A, B, and C are all neighbours, push C first, then B, then A. A ends up on top and gets popped next. Pushing in straight alphabetical order puts the wrong node on top and produces a trace that will not match the grading rubric.
  • Exam Trap: Stalling When a Popped Node Has No New Neighbours
    Students often freeze when they pop a node and find that every one of its neighbours is already in the `closedList`. This feels like a dead end, but it is not an error — it is backtracking working exactly as intended. There is nothing to calculate and no decision to make. Simply move the dead-end node to the `closedList` and pop whatever is sitting on top of the Stack next. The Stack automatically returns the algorithm to the most recent unfinished branching point.
  • Exam Trap: Forgetting the Closed List Check and Looping Forever
    On any graph with cycles — where edges can lead back to a node already visited — DFS will get trapped in an infinite loop if the `closedList` is not checked before pushing a neighbour. Without that check, the algorithm will keep rediscovering and re-pushing the same nodes indefinitely. Before pushing any neighbour onto the Stack, always verify it is not already in the `closedList`. If it is already there, skip it completely and move to the next neighbour.

Strengths, Weaknesses & When To Use It

When to use it:Reach for DFS whenever the goal is exhaustive exploration rather than optimal routing. It is the right tool for cycle detection, topological sorting, finding all connected components in a network, and navigating deep structures where any valid path is acceptable. DFS is also the memory champion — if a graph is too wide for BFS to fit its entire frontier into RAM, DFS can traverse the same graph using a fraction of the memory. The one scenario where DFS should never be used is shortest-path problems — it has no mechanism to guarantee the first path it finds is the cheapest or most efficient one.

Advantages

  • Extreme Memory Efficiency: While BFS must store every node on the current level in its queue simultaneously, DFS only ever holds the nodes along the single active path from the start to the current node. The Stack height never exceeds the maximum depth of the graph, making DFS dramatically more memory-friendly on wide or sprawling graphs where BFS would exhaust available RAM long before finishing.
  • Exhaustive Exploration — Perfect for Cycles and Connectivity: DFS commits fully to every branch before moving on, which makes it ideal for tasks that require visiting every node in the graph without exception. Finding all connected components, detecting cycles, performing topological sorts, and solving constraint-satisfaction puzzles all benefit from DFS's willingness to plunge into every corner of the graph that BFS might reach last or miss entirely under memory pressure.

Disadvantages

  • No Shortest Path Guarantee — Easily Distracted: DFS returns the first complete path it stumbles upon, not the shortest one. If a long, winding branch leads to the goal after twenty steps and a direct two-step path exists on a sibling branch, DFS will happily hand back the twenty-step route without ever discovering the alternative. On any problem where path cost or edge count matters, DFS is the wrong algorithm and will consistently produce suboptimal answers.
  • Infinite Loop Risk and Incompleteness on Deep Graphs: On any graph with cycles, a DFS that does not maintain a `closedList` will loop between the same nodes indefinitely and never terminate. Even with cycle protection, DFS can plunge down an infinitely deep branch on certain graph structures and never reach the goal — even when the goal is sitting just one branch over at a shallow depth. BFS cannot have this problem because it processes all shallow nodes first.

Depth-First Search vs. Breadth-First Search

Breadth-First Search and DFS are opposite personalities built from the same skeleton. Both maintain an `openList` and a `closedList`, both expand neighbours one at a time, and both terminate when the goal is found. The entire difference between them comes down to a single data structure swap: BFS uses a Queue to sweep cautiously outward level by level, while DFS uses a Stack to plunge recklessly down one branch at a time. One prioritizes finding the shortest path; the other prioritizes getting somewhere deep with as little memory as possible.

  • The Mechanics — Queue vs. Stack: BFS uses a First-In-First-Out Queue, so the oldest discovered node always gets processed next — this creates the level-by-level ripple. DFS uses a Last-In-First-Out Stack, so the most recently discovered node always gets processed next — this creates the deep plunge. Swap the data structure and the traversal order changes completely. The rest of the algorithm is identical.
  • Optimality Guarantee — Shortest Path vs. Any Path: BFS guarantees the shortest unweighted path because it reaches every node via the fewest possible edges before going deeper. DFS makes no such guarantee — it returns the first complete path it finds, which is often a long, winding route that ignores a much shorter alternative sitting on a nearby branch. DFS is memory-optimal, not path-optimal.
  • Memory Complexity — O(bd)O(b^d) vs. O(bm)O(bm): BFS must store every node on the current level in memory simultaneously, giving it O(bd)O(b^d) space complexity that explodes exponentially on wide graphs. DFS only stores the active path from root to the current node, giving it O(bm)O(bm) space complexity where mm is the maximum depth. On a graph too wide for BFS to fit in RAM, DFS can traverse the same structure using a fraction of the memory.
  • Exam Strategy — What the Grader Actually Wants: When asked to trace both algorithms on the same graph, the examiner is checking two things. First, can the Stack and Queue mechanics be correctly applied without mixing them up? Second, is there an explicit written statement identifying that BFS found the shortest path while DFS found the first path — and that the reason they differ is the data structure, not arbitrary chance? Writing that explanation is often the difference between full marks and partial marks.

Implementation Pseudocode

// DFS maintains two structures throughout the entire search
// openList  = a strict LIFO Stack — the most recently added node is always popped first
// closedList = a visited set — nodes here are permanently processed and never revisited
// There are no scores, no heuristics, and no math — just Stack order

function DFS(startNode, goalNode):

    // ── INITIALIZATION ──────────────────────────────────────────────────

    // Push the start node onto the top of the Stack to begin the search
    // The closedList starts empty — nothing has been visited yet
    openList   = [startNode]
    closedList = []


    // ── MAIN LOOP ────────────────────────────────────────────────────────

    while openList is not empty:

        // EXAM TRAP — Always pop from the TOP of the Stack
        // Popping from the FRONT or BOTTOM turns the Stack into a Queue — that is BFS, not DFS
        // The most recently added node always gets processed next
        current = openList.popTop()

        // Goal check happens HERE, immediately after popping from the Stack
        // This keeps termination logic consistent with BFS, A*, UCS, and Greedy Search
        if current == goalNode:
            return reconstruct_path(current)  // trace parent pointers back to start

        // Move current to the closedList — it is permanently processed
        // It will never be pushed onto the Stack again
        add current to closedList


        // ── EXPAND NEIGHBOURS ───────────────────────────────────────────

        // EXAM TRAP — Add neighbours in REVERSE alphabetical order
        // Because the Stack is LIFO, the last node pushed ends up on top
        // Pushing Z before A means A sits on top and gets processed first
        // This ensures the alphabetically first neighbour is always explored next
        for each neighbour in getNeighbours(current) in REVERSE alphabetical order:

            // Skip neighbours already fully processed — they are permanently settled
            if neighbour is in closedList:
                continue

            // Skip neighbours already waiting in the Stack — no updates needed
            // Unlike A* or UCS, there is no g(n) to recalculate — DFS is completely blind to cost
            if neighbour is in openList:
                continue

            // Brand new node — record its parent and push it onto the TOP of the Stack
            // Pushing to the top is what forces DFS to plunge deep into this branch immediately
            neighbour.parent = current
            openList.pushTop(neighbour)

    // openList is empty and goal was never popped — no path exists in this graph
    return failure


// ── INITIAL CALL ─────────────────────────────────────────────────────
// DFS(startNode, goalNode)

Time & Space Complexity

ScenarioTime ComplexitySpace ComplexityNotes
State-Space / Tree Search (AI & Exams)O(bm)O(b^m)O(bm)O(bm)Here bb is the branching factor and mm is the maximum depth of the graph. DFS must potentially visit every node in the worst case, giving it O(bm)O(b^m) time. The critical advantage is space: DFS only stores the active path from root to the current node, never the entire frontier. That O(bm)O(bm) space complexity is what makes DFS the memory champion over BFS on wide graphs.
Standard Graph Traversal (Data Structures)O(V+E)O(V + E)O(V)O(V)Here VV is the number of vertices and EE is the number of edges. This notation is used in data structures courses when the graph is given as an adjacency list. It simply means DFS visits every node once and checks every edge once — no node or connection is ever processed twice as long as the `closedList` is maintained correctly.
Best Case (Lucky Find)O(1)O(1)O(1)O(1)The best case occurs when the start node is the goal itself, or when the goal is the very first neighbour pushed onto the Stack. The search terminates almost instantly with virtually no memory usage. This scenario is worth knowing for theory questions but will almost never appear on a realistic exam graph.

Summary

DFS is not a fair, cautious ripple — it is a reckless, committed plunge. Every concept on this page reduces to one mechanical truth: the LIFO Stack forces the most recently discovered node to the front of the line, which drives the search deeper into one branch before it ever considers looking sideways. That single data structure choice is the complete explanation for why DFS behaves so differently from BFS on the exact same graph. If the LIFO pop is always from the top, neighbours are always pushed in reverse alphabetical order, and the `closedList` is always checked before pushing — the trace will be correct every time. Master those three rules and DFS becomes one of the fastest algorithms to trace cleanly under exam pressure, while also being the go-to choice whenever a graph is too wide to fit BFS into memory.

DFS Exam Questions Students Always Get Wrong

  • I pulled the node from the front of my openList instead of the top. Does that matter?

    It matters enormously — that single mistake transforms DFS into BFS. DFS is defined by the LIFO Stack: the most recently added node must always be the first one removed. Pulling from the front processes the oldest node first, which is exactly what a Queue does. Every subsequent step of the trace will follow BFS traversal order, and every mark awarded for DFS behaviour will be lost.

  • Why do I have to push neighbours in reverse alphabetical order? It feels backwards.

    Because the Stack is LIFO, whatever goes on last comes off first. If A, B, and C all need to be pushed, pushing C first then B then A leaves A sitting on top — so A gets popped and processed next. Pushing in straight alphabetical order puts C on top instead, which produces a different traversal order that will not match the grading rubric.

  • I popped a node and none of its neighbours are new — they are all in the closedList. Did I break something?

    Nothing is broken — this is backtracking working exactly as designed. When a popped node has no new neighbours to push, simply move it to the `closedList` and pop whatever is sitting on top of the Stack next. The Stack automatically returns the algorithm to the most recent unfinished branching point. There is no manual calculation required and nothing to fix.

  • What actually happens if I forget to use a closedList on a graph that has cycles?

    The algorithm enters an infinite loop and never terminates. Without a `closedList`, DFS will pop a node, push its neighbours including one it already visited, and then pop that visited node again — rediscovering the same cycle endlessly. On an exam, forgetting the `closedList` on a cyclic graph means the trace can never reach a valid termination condition.

  • Does DFS find the shortest path on a weighted graph if I follow the edge weights?

    No — DFS is completely blind to edge weights regardless of what values are written on the graph. It follows Stack order, not cost order, which means the first path it finds is determined entirely by traversal sequence. That path is almost never the cheapest one. For shortest paths on weighted graphs, the correct tools are Uniform Cost Search or A*.

Core University Curriculum

This algorithm and its manual calculation methods are foundational requirements in leading Computer Science and Software Engineering programs worldwide. You will find this topic heavily featured in the syllabi of these standard AI courses:

Explore Related Algorithms