DSA Tutorial
🔍

Depth First Search (DFS)

What Is DFS on a Graph?

DFS explores a graph by going as deep as possible along each branch before backtracking. It uses a stack (or the call stack via recursion) to track the current exploration path.

Reference graph (undirected):

     0 ─── 1 ─── 3
     │     │
     2 ─── 4 ─── 5

DFS from vertex 0 (neighbour order: sorted ascending):

  Visit 0  → push 0's unvisited neighbours [1, 2]
  Visit 1  → push 1's unvisited neighbours [3, 4]
  Visit 3  → no unvisited neighbours → backtrack
  Visit 4  → push 4's unvisited neighbour [5]  (2 already visited? no yet)
  Visit 5  → no unvisited neighbours → backtrack
  Backtrack to 1 → all done
  Backtrack to 0 → visit 2 → 2's neighbours: 0 (visited), 4 (visited) → done

DFS order: 0, 1, 3, 4, 5, 2

Compare with BFS order: 0, 1, 2, 3, 4, 5
DFS dives deep; BFS fans wide.

Recursive vs Iterative DFS

RECURSIVE DFS:
  Uses the CALL STACK to remember which vertex to return to.
  Clean and concise — mirrors the natural recursive structure.
  Risk: stack overflow for very deep graphs (>~10,000 levels in Python).

ITERATIVE DFS:
  Uses an EXPLICIT STACK data structure.
  Same logic but manually managed.
  Avoids call stack overflow.
  Note: iterative DFS with push(right then left) visits left first
        — mirror the recursive order carefully.

Core DFS Implementation

1import java.util.*; 2 3public class GraphDFS { 4 5 // RECURSIVE DFS — O(V + E) 6 public static List<Integer> dfsRecursive(List<List<Integer>> adj, int V) { 7 List<Integer> order = new ArrayList<>(); 8 boolean[] visited = new boolean[V]; 9 10 for (int start = 0; start < V; start++) { 11 if (!visited[start]) { 12 dfsHelper(adj, start, visited, order); 13 } 14 } 15 return order; 16 } 17 18 private static void dfsHelper(List<List<Integer>> adj, int u, 19 boolean[] visited, List<Integer> order) { 20 visited[u] = true; 21 order.add(u); 22 23 for (int v : adj.get(u)) { 24 if (!visited[v]) { 25 dfsHelper(adj, v, visited, order); 26 } 27 } 28 } 29 30 // ITERATIVE DFS — same result as recursive (same neighbour order) 31 public static List<Integer> dfsIterative(List<List<Integer>> adj, int V) { 32 List<Integer> order = new ArrayList<>(); 33 boolean[] visited = new boolean[V]; 34 35 for (int start = 0; start < V; start++) { 36 if (visited[start]) continue; 37 38 Deque<Integer> stack = new ArrayDeque<>(); 39 stack.push(start); 40 41 while (!stack.isEmpty()) { 42 int u = stack.pop(); 43 if (visited[u]) continue; // May be pushed multiple times 44 45 visited[u] = true; 46 order.add(u); 47 48 // Push neighbours in REVERSE order to match recursive visit order 49 List<Integer> neighbours = adj.get(u); 50 for (int i = neighbours.size() - 1; i >= 0; i--) { 51 int v = neighbours.get(i); 52 if (!visited[v]) stack.push(v); 53 } 54 } 55 } 56 return order; 57 } 58 59 // DFS with path finding — returns path from src to dst, or empty if none 60 public static List<Integer> findPath(List<List<Integer>> adj, int src, 61 int dst, int V) { 62 boolean[] visited = new boolean[V]; 63 List<Integer> path = new ArrayList<>(); 64 dfsPath(adj, src, dst, visited, path); 65 return path; 66 } 67 68 private static boolean dfsPath(List<List<Integer>> adj, int u, int dst, 69 boolean[] visited, List<Integer> path) { 70 visited[u] = true; 71 path.add(u); 72 73 if (u == dst) return true; // Found destination 74 75 for (int v : adj.get(u)) { 76 if (!visited[v]) { 77 if (dfsPath(adj, v, dst, visited, path)) return true; 78 } 79 } 80 81 path.remove(path.size() - 1); // Backtrack — remove u from path 82 return false; 83 } 84 85 public static void main(String[] args) { 86 int V = 6; 87 List<List<Integer>> adj = new ArrayList<>(); 88 for (int i = 0; i < V; i++) adj.add(new ArrayList<>()); 89 int[][] edges = {{0,1},{0,2},{1,3},{1,4},{2,4},{4,5}}; 90 for (int[] e : edges) { 91 adj.get(e[0]).add(e[1]); adj.get(e[1]).add(e[0]); 92 } 93 94 System.out.println("Recursive DFS: " + dfsRecursive(adj, V)); 95 // [0, 1, 3, 4, 2, 5] (order depends on adjacency list) 96 97 System.out.println("Iterative DFS: " + dfsIterative(adj, V)); 98 // Same order as recursive 99 100 System.out.println("Path 0→5: " + findPath(adj, 0, 5, V)); 101 // [0, 1, 4, 5] 102 } 103}
Output:
Recursive DFS: [0, 1, 3, 4, 2, 5]
Iterative DFS: [0, 1, 3, 4, 2, 5]
Path 0→5:      [0, 1, 4, 5]

Dry Run: DFS on a Graph

Graph (undirected):
     0 ─── 1 ─── 3
     │     │
     2 ─── 4 ─── 5

Adjacency list:
  0: [1, 2]
  1: [0, 3, 4]
  2: [0, 4]
  3: [1]
  4: [1, 2, 5]
  5: [4]

DFS from 0 (recursive — visit first unvisited neighbour):

Call dfs(0): visited={0}, order=[0]
  Neighbour 1: unvisited → Call dfs(1): visited={0,1}, order=[0,1]
    Neighbour 0: visited → skip
    Neighbour 3: unvisited → Call dfs(3): visited={0,1,3}, order=[0,1,3]
      Neighbour 1: visited → skip
      No more → RETURN (backtrack to 1)
    Neighbour 4: unvisited → Call dfs(4): visited={0,1,3,4}, order=[0,1,3,4]
      Neighbour 1: visited → skip
      Neighbour 2: unvisited → Call dfs(2): visited={0,1,2,3,4}, order=[0,1,3,4,2]
        Neighbour 0: visited → skip
        Neighbour 4: visited → skip
        No more → RETURN (backtrack to 4)
      Neighbour 5: unvisited → Call dfs(5): visited={0,1,2,3,4,5}, order=[0,1,3,4,2,5]
        Neighbour 4: visited → skip
        No more → RETURN (backtrack to 4)
      No more → RETURN (backtrack to 1)
    No more → RETURN (backtrack to 0)
  Neighbour 2: visited → skip
  No more → RETURN

Final DFS order: [0, 1, 3, 4, 2, 5]

Application 1: Cycle Detection — Undirected Graph

UNDIRECTED CYCLE DETECTION:
  A cycle exists if DFS visits a node that is already visited
  AND that node is NOT the direct parent of the current node.

     0 ─── 1
     │     │
     2 ─── 3    ← cycle: 0-1-3-2-0

DFS from 0 with parent tracking:
  dfs(0, parent=-1):
    Visit 1 (parent=0): dfs(1, parent=0)
      Visit 3 (parent=1): dfs(3, parent=1)
        Visit 2 (parent=3): dfs(2, parent=3)
          Neighbour 0: visited AND 0 ≠ parent(3) → CYCLE DETECTED!
1public class CycleDetection { 2 3 static class Graph { 4 List<List<Integer>> adj; 5 int V; 6 Graph(int V) { 7 this.V = V; 8 adj = new ArrayList<>(); 9 for (int i = 0; i < V; i++) adj.add(new ArrayList<>()); 10 } 11 void addEdge(int u, int v) { adj.get(u).add(v); adj.get(v).add(u); } 12 } 13 14 // UNDIRECTED: track parent to distinguish back edges from tree edges 15 public static boolean hasCycleUndirected(Graph g) { 16 boolean[] visited = new boolean[g.V]; 17 18 for (int start = 0; start < g.V; start++) { 19 if (!visited[start]) { 20 if (dfsCycleUndirected(g, start, -1, visited)) return true; 21 } 22 } 23 return false; 24 } 25 26 private static boolean dfsCycleUndirected(Graph g, int u, int parent, 27 boolean[] visited) { 28 visited[u] = true; 29 30 for (int v : g.adj.get(u)) { 31 if (!visited[v]) { 32 if (dfsCycleUndirected(g, v, u, visited)) return true; 33 } else if (v != parent) { 34 // Visited AND not the parent → back edge → cycle! 35 return true; 36 } 37 } 38 return false; 39 } 40 41 public static void main(String[] args) { 42 // Cyclic graph: 0─1─3─2─0 43 Graph cyclic = new Graph(4); 44 cyclic.addEdge(0, 1); cyclic.addEdge(1, 3); 45 cyclic.addEdge(3, 2); cyclic.addEdge(2, 0); 46 System.out.println("Cyclic: " + hasCycleUndirected(cyclic)); // true 47 48 // Acyclic graph: 0─1─2, 1─3 49 Graph acyclic = new Graph(4); 50 acyclic.addEdge(0, 1); acyclic.addEdge(1, 2); acyclic.addEdge(1, 3); 51 System.out.println("Acyclic: " + hasCycleUndirected(acyclic)); // false 52 } 53}
Output:
Cyclic:   true
Acyclic:  false

Application 2: Cycle Detection — Directed Graph

DIRECTED CYCLE DETECTION:
  For directed graphs, we need an additional inStack[] array.
  inStack[u] = true if u is currently on the active DFS path.

  Why visited[] alone is insufficient:
     A ──→ B
     ↑     │
     C ←───┘

    DFS: A → B → C → A?  No: C→A is a directed cycle.
    But consider:  A → B → C and D → C
    When visiting D→C, C is already visited (from A→B→C path).
    But C is NOT on the current stack (D's path).
    So this is NOT a cycle — D can reach C via a separate path.

  RULE: cycle exists if we reach a node currently in inStack (gray node).
        Reaching a fully-finished node (not in stack) is NOT a cycle.
1public class DirectedCycleDetection { 2 3 public static boolean hasCycleDirected(List<List<Integer>> adj, int V) { 4 boolean[] visited = new boolean[V]; 5 boolean[] inStack = new boolean[V]; // Recursion stack 6 7 for (int start = 0; start < V; start++) { 8 if (!visited[start]) { 9 if (dfsCycleDirected(adj, start, visited, inStack)) return true; 10 } 11 } 12 return false; 13 } 14 15 private static boolean dfsCycleDirected(List<List<Integer>> adj, int u, 16 boolean[] visited, 17 boolean[] inStack) { 18 visited[u] = true; 19 inStack[u] = true; // Mark as part of current DFS path 20 21 for (int v : adj.get(u)) { 22 if (!visited[v]) { 23 if (dfsCycleDirected(adj, v, visited, inStack)) return true; 24 } else if (inStack[v]) { 25 // v is on the current path → back edge → DIRECTED CYCLE! 26 return true; 27 } 28 } 29 30 inStack[u] = false; // Remove from current path (backtrack) 31 return false; 32 } 33 34 public static void main(String[] args) { 35 int V = 4; 36 List<List<Integer>> adj = new ArrayList<>(); 37 for (int i = 0; i < V; i++) adj.add(new ArrayList<>()); 38 39 // Cyclic directed: 0→1→2→0 40 adj.get(0).add(1); adj.get(1).add(2); adj.get(2).add(0); 41 System.out.println("Directed cycle: " + hasCycleDirected(adj, V)); // true 42 43 // Reset — DAG: 0→1→2→3 44 List<List<Integer>> dag = new ArrayList<>(); 45 for (int i = 0; i < V; i++) dag.add(new ArrayList<>()); 46 dag.get(0).add(1); dag.get(1).add(2); dag.get(2).add(3); 47 System.out.println("Directed no cycle: " + hasCycleDirected(dag, V)); // false 48 } 49}
Output:
Directed cycle:    true
Directed no cycle: false
Cross edges:       false

Application 3: Topological Sort (DFS Post-Order)

Topological sort orders vertices so that for every directed edge u→v, u comes before v. Only works on DAGs.

DAG:
     5 ──→ 0
     5 ──→ 2
     4 ──→ 0
     4 ──→ 1
     2 ──→ 3
     3 ──→ 1

DFS POST-ORDER (add vertex AFTER all descendants are finished):

  dfs(5): → dfs(0): leaf → add 0 to result
          → dfs(2): → dfs(3): → dfs(1): leaf → add 1
                              → add 3
                    → add 2
          → add 5
  dfs(4): → 0 visited
          → 1 visited
          → add 4

Result (post-order, added at finish): [0, 1, 3, 2, 5, 4]
REVERSE to get topological order:     [4, 5, 2, 3, 1, 0]

VERIFY: every edge u→v has u before v in [4, 5, 2, 3, 1, 0]:
  5→0: 5 before 0 ✓    5→2: 5 before 2 ✓
  4→0: 4 before 0 ✓    4→1: 4 before 1 ✓
  2→3: 2 before 3 ✓    3→1: 3 before 1 ✓  All correct!
1import java.util.*; 2 3public class TopologicalSortDFS { 4 5 public static List<Integer> topologicalSort(List<List<Integer>> adj, int V) { 6 boolean[] visited = new boolean[V]; 7 Deque<Integer> stack = new ArrayDeque<>(); // Result stack (post-order) 8 9 for (int start = 0; start < V; start++) { 10 if (!visited[start]) { 11 topoHelper(adj, start, visited, stack); 12 } 13 } 14 15 // Pop stack to get topological order (reversed post-order) 16 List<Integer> result = new ArrayList<>(); 17 while (!stack.isEmpty()) result.add(stack.pop()); 18 return result; 19 } 20 21 private static void topoHelper(List<List<Integer>> adj, int u, 22 boolean[] visited, Deque<Integer> stack) { 23 visited[u] = true; 24 25 for (int v : adj.get(u)) { 26 if (!visited[v]) { 27 topoHelper(adj, v, visited, stack); 28 } 29 } 30 31 stack.push(u); // Add AFTER all descendants finished (post-order) 32 } 33 34 public static void main(String[] args) { 35 // DAG: 5→0, 5→2, 4→0, 4→1, 2→3, 3→1 36 int V = 6; 37 List<List<Integer>> adj = new ArrayList<>(); 38 for (int i = 0; i < V; i++) adj.add(new ArrayList<>()); 39 adj.get(5).add(0); adj.get(5).add(2); 40 adj.get(4).add(0); adj.get(4).add(1); 41 adj.get(2).add(3); adj.get(3).add(1); 42 43 System.out.println("Topological order: " + topologicalSort(adj, V)); 44 // [4, 5, 2, 3, 1, 0] (one valid ordering) 45 } 46}
Output:
Topological order: [4, 5, 2, 3, 1, 0]

Application 4: DFS Timestamps

Assign discovery d[u] and finish f[u] times to each vertex. Useful for edge classification and strongly connected components.

DFS TIMESTAMPS — timer increments at each event:

Graph (directed): 0→1→2→3, 0→3

DFS from 0:
  d[0]=1, enter 0
    d[1]=2, enter 1
      d[2]=3, enter 2
        d[3]=4, enter 3
        f[3]=5, finish 3
      f[2]=6, finish 2
    f[1]=7, finish 1
    3 already visited → edge 0→3 is a FORWARD EDGE
  f[0]=8, finish 0

Timestamps: d=[1,2,3,4], f=[8,7,6,5]

EDGE CLASSIFICATION using timestamps (directed graph):
  Tree edge:    d[u] < d[v] AND f[v] < f[u]  (v first discovered via this edge)
  Forward edge: d[u] < d[v] AND f[v] < f[u]  (v already in subtree of u)
  Back edge:    d[v] < d[u] AND f[u] < f[v]  (u is in subtree of v — CYCLE!)
  Cross edge:   f[v] < d[u]                   (v fully done before u started)

KEY PROPERTY: Back edges exist ←→ graph has directed cycle
1public class DFSTimestamps { 2 3 private static int timer = 0; 4 5 public static void computeTimestamps(List<List<Integer>> adj, int V) { 6 int[] d = new int[V]; // Discovery time 7 int[] f = new int[V]; // Finish time 8 boolean[] visited = new boolean[V]; 9 timer = 0; 10 11 for (int start = 0; start < V; start++) { 12 if (!visited[start]) { 13 dfsTime(adj, start, visited, d, f); 14 } 15 } 16 17 System.out.println("Discovery: " + java.util.Arrays.toString(d)); 18 System.out.println("Finish: " + java.util.Arrays.toString(f)); 19 } 20 21 private static void dfsTime(List<List<Integer>> adj, int u, 22 boolean[] visited, int[] d, int[] f) { 23 visited[u] = true; 24 d[u] = ++timer; // Discovery time 25 26 for (int v : adj.get(u)) { 27 if (!visited[v]) { 28 dfsTime(adj, v, visited, d, f); 29 } 30 } 31 32 f[u] = ++timer; // Finish time (all descendants done) 33 } 34 35 public static void main(String[] args) { 36 int V = 4; 37 List<List<Integer>> adj = new ArrayList<>(); 38 for (int i = 0; i < V; i++) adj.add(new ArrayList<>()); 39 adj.get(0).add(1); adj.get(1).add(2); 40 adj.get(2).add(3); adj.get(0).add(3); 41 computeTimestamps(adj, V); 42 // Discovery: [1, 2, 3, 4] Finish: [8, 7, 6, 5] 43 } 44}
Output:
Discovery: [1, 2, 3, 4]
Finish:    [8, 7, 6, 5]

DFS vs BFS — When to Use Which

USE DFS WHEN:                         USE BFS WHEN:
  Cycle detection                       Shortest path (unweighted)
  Topological sort                      Level-by-level processing
  Finding ANY path (not shortest)       Minimum steps / hops
  Exploring all paths                   Nearest something (multi-source)
  Connected components                  Social graph distance
  Strongly connected components         0-1 BFS for 0/1 weights
  Backtracking / exhaustive search      Checking bipartiteness
  Maze solving (any solution)           Finding all nodes at distance k
  DFS tree / timestamps                 BFS spanning tree
  Detecting back edges

BOTH WORK FOR:
  Connected component count (O(V+E))
  Graph traversal
  Cycle detection (different techniques)
  Reachability queries

MEMORY:
  DFS: O(h) stack space where h = max DFS depth
       (O(V) worst case for skewed graphs / long paths)
  BFS: O(w) queue space where w = max level width
       (O(V) worst case for star graphs with all nodes at level 1)

Complexity Summary

OperationTimeSpaceNotes
DFS traversalO(V + E)O(V)visited array + call stack
Cycle detection (undirected)O(V + E)O(V)Track parent
Cycle detection (directed)O(V + E)O(V)Track inStack
Topological sortO(V + E)O(V)Post-order + reverse
DFS timestampsO(V + E)O(V)d[] and f[] arrays
Connected componentsO(V + E)O(V)Outer loop + DFS
Path findingO(V + E)O(V)Backtracking path

Common Mistakes

Using visited[] alone for directed cycle detection. In directed graphs, a visited node in the global visited set does not mean a cycle — it may have been visited via a completely different DFS path. Only a node currently on the ACTIVE recursion stack (inStack[v] = true) indicates a back edge and a directed cycle. Always use a separate inStack array for directed cycle detection.

Forgetting inStack[u] = false when backtracking. After exploring all of u's neighbours, u must be removed from the recursion stack (inStack[u] = false). Forgetting this causes false cycle detections — future DFS paths will incorrectly see u as "currently on stack."

Topological sort: adding vertex to result at DFS entry instead of exit. Adding a vertex to the topological result when first visiting it (pre-order) gives the wrong order. The vertex must be added AFTER all of its descendants are fully explored (post-order). Then reverse the result. Pre-order gives DFS traversal order, not topological order.

Iterative DFS not matching recursive DFS order. Iterative DFS with a stack and pushing neighbours left-to-right will visit right-to-left (LIFO). To match recursive DFS, push neighbours in reverse order (right-to-left push, so left is on top and visited first). If exact order doesn't matter (for connectivity), this is fine.

Applying topological sort to a cyclic graph. If the graph has a cycle, no topological ordering exists. Always verify the graph is a DAG first (run cycle detection), or check the output: if the topological sort didn't include all V vertices (some were never added), a cycle prevented them from being finished.

Interview Questions

Q: What is the key difference between cycle detection in undirected vs directed graphs?

Undirected: a cycle is detected when DFS reaches a visited node that is NOT the direct parent of the current node. The parent check is needed because in undirected graphs, the edge from child back to parent is not a cycle — it's the same undirected edge traversed backwards. Directed: a cycle is detected only when DFS reaches a node currently in the active recursion stack (inStack = true). Reaching a visited node NOT in the stack means it was explored in a previous DFS path — no cycle.

Q: Why is topological sort impossible on a cyclic graph?

Topological sort requires every edge u→v to have u before v in the ordering. In a cycle A→B→C→A: A must come before B (edge A→B), B before C, C before A. But A before B and C before A means A comes both before and after B simultaneously — impossible. There is no linear ordering that satisfies all constraints simultaneously. Kahn's algorithm detects this when not all vertices are processed; DFS-based detects it via back edges.

Q: What are DFS timestamps used for beyond edge classification?

DFS timestamps (discovery d[u] and finish f[u]) enable: (1) Edge classification — back/forward/cross/tree edges in O(1) per edge using timestamp comparison. (2) Kosaraju's SCC algorithm — the finish order from DFS on the original graph determines the DFS order on the reversed graph. (3) Identifying articulation points and bridges in undirected graphs. (4) Parenthesis structure — the intervals [d[u], f[u]] for any two nodes are either disjoint or one contains the other, mirroring the parenthesis nesting of function calls.

Summary

DFS explores a graph by going as deep as possible along each branch using a stack (or recursion). It visits each vertex once and each edge once — O(V + E).

Two implementations:

  • Recursive — uses call stack; clean, concise, risk of stack overflow for very deep graphs
  • Iterative — uses explicit stack; push neighbours in reverse order to match recursive visit order

Four key applications:

ApplicationUnique technique
Cycle detection (undirected)Track parent; cycle if visited neighbour ≠ parent
Cycle detection (directed)Track inStack; cycle if visited neighbour is in current stack
Topological sortAdd vertex at DFS exit (post-order); reverse the result
DFS timestampsd[u] = entry time, f[u] = exit time; classify edges by interval containment

DFS vs BFS choice:

  • Path finding, cycle detection, topological sort, backtracking → DFS
  • Shortest path, level-by-level, nearest node → BFS

In the next topic you will explore Shortest Path Algorithms — Dijkstra's, Bellman-Ford, and Floyd-Warshall for weighted graphs.

Suggested Quiz

DFS uses a stack (or recursion). How does this differ from BFS in exploration order?

1/6