AniUI Academy

Tree Traversal: BFS and DFS

Two different ways to visit every node in a tree — breadth-first with a queue, depth-first with recursion or a stack — and how to actually decide which one a problem calls for.

10 min read

Every tree problem that isn't a simple search boils down to one question: in what order do you visit the nodes? This lesson covers the two fundamental answers — breadth-first and depth-first — and, more importantly, how to recognize which one a given problem actually needs.

Depth-first search (DFS): dive, then backtrack

DFS goes as deep as possible down one branch before backing up to try the next one. The natural implementation is recursive, because recursion's call stack is exactly the "remember where to backtrack to" mechanism DFS needs:

function dfs(node, visit) {
  if (!node) return;
  visit(node.value);
  for (const child of node.children) {
    dfs(child, visit);
  }
}

For a tree shaped like root → [A → [A1, A2], B → [B1]], this visits root, A, A1, A2, B, B1 — it fully exhausts A's entire subtree before ever looking at B.

An iterative version replaces the call stack with an explicit array stack — exactly the technique from the recursion-vs-iteration lesson:

function dfsIterative(root, visit) {
  const stack = [root];
  while (stack.length > 0) {
    const node = stack.pop();
    visit(node.value);
    for (let i = node.children.length - 1; i >= 0; i--) {
      stack.push(node.children[i]); // reverse order so leftmost child pops first
    }
  }
}

Breadth-first search (BFS): level by level

BFS visits every node at the current depth before moving to the next depth. It cannot be written recursively in the natural way DFS can — it needs a queue (first-in-first-out), not a stack:

function bfs(root, visit) {
  const queue = [root];
  while (queue.length > 0) {
    const node = queue.shift(); // remove from the FRONT
    visit(node.value);
    for (const child of node.children) {
      queue.push(child); // add to the BACK
    }
  }
}

For the same tree as above, this visits root, A, B, A1, A2, B1 — everything at depth 1 (A, B) before anything at depth 2. That single difference — stack (LIFO) for DFS, queue (FIFO) for BFS — is the entire mechanical distinction between the two, and it's worth tracing by hand once until it's automatic.

  1. Step 1

    DFS with a stack

    Pop the most recently added node — dives all the way down one branch before backtracking.

  2. Step 2

    DFS visits: root, A, A1, A2, B, B1

    A's entire subtree finishes before B is ever touched.

  3. Step 3

    BFS with a queue

    Remove the earliest-added node — exhausts everything at one depth before going deeper.

  4. Step 4

    BFS visits: root, A, B, A1, A2, B1

    Everything at depth 1 (A, B) finishes before anything at depth 2.

Same tree, two traversal orders, determined entirely by stack versus queue.

(A production queue.shift() has a real cost worth flagging: shifting the front of a JS array is O(n), because every remaining element shifts down an index. For large queues, a proper ring-buffer-backed queue avoids this — but for typical tree sizes in frontend code, .shift() is a fine, readable default.)

Choosing between them: the question that decides it

Use BFS when you need the shortest path (in edges) from the root, or you specifically care about "everything at this depth." BFS explores in strictly increasing distance from the root — everything at distance 1 before anything at distance 2 — so the very first time it reaches a target node, that's guaranteed to be via the shortest possible path. DFS has no such guarantee: it might reach the target only after a long, unrelated detour down a different branch first.

Use DFS when you need to explore a whole branch's implications before moving on, or the problem is naturally recursive — validating a nested structure, computing something that depends on a subtree's full result (size, sum, max depth) before combining it with siblings. DFS's recursive form usually matches the natural recursive definition of the problem itself.

The three flavors of DFS, briefly

When DFS specifically visits binary trees, three orderings get names based on when the current node itself is processed relative to its children:

  • Preorder — node, then children (visit(node); recurse(left); recurse(right);)
  • Postorder — children, then node (recurse(left); recurse(right); visit(node);)
  • In-order — left child, node, right child — specific to binary trees, and notably, running in-order traversal on a binary search tree visits every value in fully sorted order, for free, as a direct consequence of the BST ordering property from the last lesson.
Try it yourself
Loading playground...

What to remember

  • DFS uses a stack (recursion's call stack, or an explicit one) and dives depth-first; BFS uses a queue and expands level by level. That single data structure choice is the entire mechanical difference.
  • Both are O(n) time for n nodes — the meaningful difference is auxiliary space, bounded by the tree's height for DFS and by its maximum width for BFS, which depends entirely on the tree's actual shape.
  • BFS guarantees the first time it reaches a target is via the shortest path in edges from the root — reach for it specifically when "shortest path" or "everything at this depth" is the question.
  • Preorder, postorder, and in-order are DFS orderings distinguished by when the current node is processed relative to its children; in-order on a BST visits values in fully sorted order.

Check yourself

4 questions · pass 3/4 to unlock The DOM Is a Tree

up to 50
  1. 1.What is the core structural difference between breadth-first search (BFS) and depth-first search (DFS) traversal of a tree?

  2. 2.Both BFS and DFS visit every node in a tree of n nodes exactly once. What is the time complexity of each, and what typically differs between them?

  3. 3.Why is 'find the shortest path from the root to a target node, in terms of number of edges' naturally suited to BFS rather than DFS?

  4. 4.A recursive DFS function that processes a value BEFORE recursing into its children (process(node); for (child of node.children) dfs(child);) is doing which traversal order?

4 left to answer