DSA sheet · Graphs · Traversal pattern (DFS)

DFS Traversal of a Graph

The second traversal. The question and the graph are the same as in BFS (Problem 3), but now we must go deep first: from a node, pick one neighbour and keep going down that path until you get stuck, then come back and try the next neighbour. The teacher writes it with recursion and explains it through the call stack. At the end she shows the one line that makes the difference between BFS and DFS. DFS is the base of islands, provinces, all-paths, cycle detection and topological sort, so this page matters a lot.

Every problem below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Graph words used on this page

Recursion and the call stack, in 4 lines

BFS (queue, level by level) was Problem 3. Grids as graphs, with 4 directions, start in Problem 6.

Part A · DFS with recursion

GFG: "DFS of Graph"

1The question in simple words

You get a connected, undirected graph with V nodes (0 … V−1) as an adjacency list. Start at node 0 and return the nodes in depth-first order, taking each node's neighbours left to right as listed in adj.

               (0)
             /  |  \
          (2)  (3)  (1)
           |   / \    \
          (4)(5) (6)  (7)

adj[0] = [2, 3, 1]     adj[4] = [2]
adj[1] = [0, 7]        adj[5] = [3]
adj[2] = [0, 4]        adj[6] = [3]
adj[3] = [0, 5, 6]     adj[7] = [1]

Expected: [0, 2, 4, 3, 5, 6, 1, 7]. Compare BFS on the same graph: [0, 2, 3, 1, 4, 5, 6, 7].

2What the constraints tell us

3Intuition: walk a maze with one hand on the wall

Stand at 0. You have three corridors: 2, 3, 1. Take the first one, 2. Don't look at 3 and 1 yet. From 2, take its first new corridor, 4. From 4 there's nowhere new to go: a dead end. Walk back to 2 (nothing new there either), back to 0, and only now take the next corridor, 3. And so on.

The teacher sees the result as paths from the start: 0 → 2 → 4, then 0 → 3 → 5 and 3 → 6, then 0 → 1 → 7. Each path is followed all the way down before the next one starts.

Nodes like 4, 5, 6, 7 have no new neighbours; she calls them leaf nodes, like in a tree. But a graph with a cycle may have no leaves at all. There you would go round and round forever, which is why we need the visited array.

4Building the logic step by step

Step 1: what does the function need? (the teacher's checklist)

In BFS everything lived in one loop. Here we call a function again and again, and a function only knows what it is given. So she lists what each call needs:

Doubt: in Python, do I really have to pass all four?
→ You can do it the teacher's way (pass them as parameters, as in the code below), or write the helper inside dfs so it can already see adj, visited and ans. Either way it's the same list objects being changed. No copies are made, so every call sees the marks made by the others.

Step 2: what is the base case?

Every recursion needs a way to stop. BFS stopped when the queue was empty. The teacher asks: what do we do the moment we are on a node? Two things, before leaving for the children, because once we go deeper we won't be "standing" here again:

  1. add the node to ans (just node, not node.val: a graph node here is simply an int);
  2. mark visited[node] = True, so nobody comes here again.
Doubt: so where is the "if … return" base case?
→ There isn't a separate one. The recursion stops by itself: we only call into unvisited nodes, and each call marks one more node. After at most V calls, no unvisited neighbour is left. Then every for loop runs out, and each call returns on its own after its last line. The visited check is the stopping rule.

Step 3: go to the children, but only one at a time

Loop over adj[node]. In BFS, we pushed every child into the queue in this loop. In DFS we take one child and call the function on it straight away. The rest of the loop waits, paused on the stack, until that whole branch is finished.

Step 4: the bug when we don't check visited

Say we call DFS on every child without checking. dfs(0) → dfs(2). Node 2's children are [0, 4], so it calls dfs(0) again → 0 is added to ans a second time, which calls dfs(2) again… We'd go back and forth along the edge 0–2 forever. Every node must be printed once, and from 2 we want to go on to 4, not back to 0.

RuleCall dfs(nei) only if not visited[nei].

Step 5: start, and the size of visited

adj has one sub-list per node (index 0 = children of 0, …, index 7 = children of 7), so V = len(adj) = 8, and visited = [False] * 8. The main function calls the helper with 0 because the question says "start from vertex 0". If it said start from 1, we'd pass 1. When the helper is done, ans is full, and we return it. The visited array was only a helper; nobody asks for it.

5Approach steps

  1. V = len(adj); visited = [False] * V; ans = [].
  2. Call dfs(0).
  3. Inside dfs(node): mark it visited and append it to ans.
  4. For every neighbour in adj[node], left to right: if not visited, call dfs(nei) and wait for it to finish.
  5. When the loop ends, the call returns by itself. After the first call returns, return ans.

6Code (Python)

DFS traversal with recursion
import sys

class Solution:
    def dfs(self, adj):
        sys.setrecursionlimit(max(1000, 2 * len(adj) + 100))   # depth can reach V
        n = len(adj)
        visited = [False] * n
        ans = []
        self.go(0, adj, visited, ans)    # the question says: start at 0
        return ans

    def go(self, node, adj, visited, ans):
        visited[node] = True             # never come back here
        ans.append(node)                 # do the node's work first
        for nei in adj[node]:            # children, left to right
            if not visited[nei]:
                self.go(nei, adj, visited, ans)   # go deep; the loop waits
        # loop finished: every child is done, so return to the caller

7Code line by line

linewhat it means
sys.setrecursionlimit(...)Python stops at about 1000 nested calls by default. A line-shaped graph needs V of them, so we allow a bit more than V. (Not in the teacher's Java code; Java's stack is fine for this size.)
visited = [False] * n ans = []Made once here and shared by every call.
self.go(0, adj, visited, ans)Start the walk at node 0, as the question says.
visited[node] = TrueThe first thing on arrival: claim this node so no other call enters it.
ans.append(node)Record it now, before going deeper: this gives the "visit order".
for nei in adj[node]:Each neighbour in the given order. This loop is paused while a child's call runs.
if not visited[nei]: self.go(nei, ...)Only unvisited children. The call returns only after that child's whole branch is finished.
(end of function)No return needed: when the loop ends, Python returns automatically and the caller's loop continues.
return ansAll calls are done; ans holds every node in depth-first order.

8Dry run with the call stack

  1. go(0): mark 0, ans = [0]. Children [2, 3, 1]: 2 is unvisited → call go(2). go(0) waits at "child 2".
  2. go(2): mark 2, ans = [0,2]. Children [0, 4]: 0 is visited, skip. 4 is new → call go(4).
  3. go(4): mark 4, ans = [0,2,4]. Children [2]: visited. The loop ends → go(4) returns and leaves the stack.
  4. Back in go(2): its loop has no more children → returns.
  5. Back in go(0): next child 3 is new → go(3): mark, ans = [0,2,4,3]. Children [0, 5, 6]: 0 visited; 5 new → go(5): ans = [0,2,4,3,5]. Its only child 3 is visited → returns.
  6. go(3) continues: 6 is new → go(6): ans = […,5,6]. Child 3 visited → returns. go(3)'s loop is over → returns.
  7. go(0) continues: last child 1 is new → go(1): ans = […,6,1]. Children [0, 7]: 0 visited; 7 new → go(7): ans = […,1,7]. Child 1 visited → returns. go(1) returns.
  8. go(0) has run its loop over all 3 children → returns. The stack is empty. Answer [0, 2, 4, 3, 5, 6, 1, 7] ✓
step 3 (deepest on the left path)
go(0)go(2)go(4)
step 5
go(0)go(3)go(5)
step 7
go(0)go(1)go(7)

The newest call is on top (red). go(0) stays at the bottom the whole time, because it can only finish after all three of its branches are done.

stepnode enteredneighbours checkedstack AFTERvisited afterans so far
102 → go0, 2{0}[0]
220 seen, 4 → go0, 2, 4{0,2}[0,2]
342 seen → return0, 2 → 0{0,2,4}[0,2,4]
430 seen, 5 → go0, 3, 5+3[0,2,4,3]
553 seen → return0, 3+5[0,2,4,3,5]
663 seen → return; 3 is done → return0+6[0,2,4,3,5,6]
710 seen, 7 → go0, 1, 7+1[…,6,1]
871 seen → return all the wayemptyall 8[0,2,4,3,5,6,1,7]
after step 3: left path done (✓)
        (0✓)
      /  |   \
   (2✓) (3)  (1)
    |   / \     \
  (4✓)(5) (6)  (7)
after step 6: middle path done
        (0✓)
      /  |   \
   (2✓) (3✓) (1)
    |    / \     \
  (4✓)(5✓)(6✓) (7)

Compare with BFS: after its first round, BFS had already marked 2, 3 and 1. DFS hasn't touched 3 or 1 at all until the 2-branch is completely finished.

9Complexity & remember

Remember DFS (recursive) go(node): mark, record, then for each neighbour if not visited → go(nei). No explicit base case: the visited check stops it. In Python, raise the recursion limit.

Part B · The same DFS with an explicit stack

This part is not in the video. It is here because Python's recursion limit is a real trap, and because "DFS = a stack" is something interviewers like to ask. The question, constraints and answer are the same as Part A. Only the way we remember the pending work changes: our own list used as a stack, instead of Python's call stack.

1Intuition

A stack is last in, first out. If we push a node's neighbours and then pop, we get the most recently found node next, so we keep going deeper, just like recursion. Two details make the order match Part A exactly:

  1. Push the neighbours in reverse. The stack gives back the last thing pushed. To visit adj[0] = [2, 3, 1] in the order 2, 3, 1, we push 1, 3, 2, so 2 sits on top.
  2. Mark when popping, not when pushing. A node can sit in the stack more than once; when it's popped a second time we simply skip it.
Doubt: in BFS we marked on push. Why not here?
→ Marking on push would "reserve" a node for the parent that pushed it, even if a deeper path reaches it first. Example: triangle 0–1–2 plus edge 1–3, with adj = [[1,2],[0,2,3],[0,1],[1]]. Recursion goes 0 → 1 → 2 (2 is reached through 1), then back to 1 → 3: [0, 1, 2, 3]. Mark-on-push marks 2 while handling 0, so from 1 it skips 2 and goes to 3 first: [0, 1, 3, 2], which is not the DFS order. It still visits everything, so it's fine for counting components, but this question wants the exact order. (The test file checks this example.)

2Approach steps

  1. Push 0 onto the stack.
  2. While the stack is not empty: pop a node. If it's already visited, skip it.
  3. Otherwise mark it and append it to ans.
  4. Push its unvisited neighbours in reverse order.

3Code (Python)

DFS traversal with an explicit stack
class Solution:
    def dfs(self, adj):
        n = len(adj)
        visited = [False] * n
        ans = []
        stack = [0]                          # a Python list used as a stack
        while stack:
            node = stack.pop()               # the most recent node
            if visited[node]:
                continue                     # an old copy, already done
            visited[node] = True             # mark when popped
            ans.append(node)
            for nei in reversed(adj[node]):  # reversed: first child ends on top
                if not visited[nei]:
                    stack.append(nei)
        return ans
linewhat it means
stack = [0]append and pop() at the end of a list are both O(1).
if visited[node]: continueA node can be pushed by several neighbours; only its first pop counts.
visited[node] = True ans.append(node)Exactly the two "arrival" jobs of go(node) in Part A.
for nei in reversed(adj[node]):Push the last child first, so the first child is popped first.

4Dry run (first few steps)

steppoppedpushed (reversed, unvisited)stack AFTER (top on the right)ans
101, 3, 2[1, 3, 2][0]
224 (0 is visited)[1, 3, 4][0,2]
34– (2 visited)[1, 3][0,2,4]
436, 5[1, 6, 5][0,2,4,3]
55–[1, 6][…,3,5]
66–[1][…,5,6]
717[7][…,6,1]
87–[][0,2,4,3,5,6,1,7]

The stack holds the same "waiting branches" the call stack held in Part A: after step 2, [1, 3] are the branches of 0 still to do.

5Complexity & remember

Remember iterative DFSpop → skip if visited → mark, record → push neighbours reversed. Mark on pop (DFS), mark on push (BFS).

Part C · BFS vs DFS: the one difference

The teacher puts the two codes side by side. Both do the same things at a node: record it, mark it, look at its children. The difference is what happens inside the children loop:

BFS (Problem 3)DFS (this page)
inside the children loopputs all unvisited children into the queue, then moves ontakes one child and calls DFS on it right away; the rest of the loop waits
who becomes the "current" node nextthe oldest waiting node (the next one on the same level)the child just entered; it becomes the new parent
pending work kept ina queue (deque)the call stack (or our own stack)
order on this graph0 2 3 1 4 5 6 70 2 4 3 5 6 1 7
stops whenthe queue is emptyevery call's loop has run out
time / spaceO(V + E) / O(V)O(V + E) / O(V) incl. the call stack

Her test: if you call the function from inside the children loop, it is DFS; if you clear the whole children list into a queue first, it is BFS. Which one to use depends on the question. From the patterns video: BFS for "fewest steps / nearest / level-wise", DFS for "explore all paths / connectivity / go deep before backtracking". The coming problems will train this choice.


Part D · Revision page

recursive DFSstack DFS
arrive at a nodemark + record, at the top of go(node)pop; skip if visited; mark + record
childrenfor each unvisited → go(nei)push unvisited, in reversed order
stoppingno explicit base case: the visited checkstack empty
Python gotcharecursion limit ≈ 1000 → raise itnone
If you remember only 5 lines 1. DFS = pick one child and go all the way down before trying the next.
2. On arrival: mark visited, add to the answer.
3. Recurse only into unvisited neighbours; that's also what stops the recursion.
4. Recursion waits in the call stack; iterative DFS uses a list as a stack (push reversed, mark on pop).
5. O(V + E) time; O(V) space, including the call stack.
Mistakes to avoid ✗ calling go(nei) without the visited check (0 ↔ 2 forever)
✗ marking visited after the children loop (children come back to you)
✗ making a fresh visited/ans inside the helper instead of sharing one
✗ forgetting the recursion limit in Python for V up to 10⁴
✗ stack DFS without reversing, or marking on push, when the exact order matters
test it yourself (paste under either Solution above)
adj = [[2, 3, 1], [0, 7], [0, 4], [0, 5, 6], [2], [3], [3], [1]]
print(Solution().dfs(adj))                       # [0, 2, 4, 3, 5, 6, 1, 7]
print(Solution().dfs([[1, 2], [0, 2, 3], [0, 1], [1]]))   # [0, 1, 2, 3]
print(Solution().dfs([[]]))                      # [0]
line = [[1]] + [[i - 1, i + 1] for i in range(1, 9999)] + [[9998]]
print(Solution().dfs(line)[-3:])                 # [9997, 9998, 9999]

Based on this video: DFS Traversal of Graph