AlgoMaster Logo

Binary Tree Paths

easyFrequency7 min readUpdated June 23, 2026

Understanding the Problem

We need to find every path from the root of a binary tree down to a leaf node, and return each path as a string with node values separated by "->". A leaf is any node where both left and right children are null.

This maps directly to depth-first search. As the traversal descends from the root, it extends the current path. Every leaf it reaches marks a complete root-to-leaf path to record. The work of the problem is managing the path state: extending it on the way down and discarding the last node on the way back up before trying the other branch.

Key Constraints:

  • Number of nodes in [1, 100] → With at most 100 nodes, any traversal approach is fast enough. The constraint guarantees at least 1 node, so the tree is never empty, though defensive null checks cost nothing.
  • -100 <= Node.val <= 100 → Values can be negative, so a valid path can look like "-1->-2". Standard integer-to-string conversion produces the minus sign correctly, so no special casing is needed.

Approach 1: DFS with String Concatenation

Intuition

Walk the tree with DFS, building up the path string as we go. At each node, append the node's value to the current path. If the node is a leaf, that path is complete and goes into the result list. If not, recurse into the left and right children.

String concatenation creates a new string each time, so each recursive call works with its own copy of the path. When a call returns, the parent's path string is unchanged, which means no explicit undo step is needed.

The cost of that convenience is allocation. Copying the path at a node of depth d takes O(d) work, so total time grows with the sum of all node depths rather than the node count alone.

Algorithm

  1. Start DFS from the root with an empty path.
  2. At each node, append the node's value to the current path.
  3. If the node is a leaf (no left or right child), add the current path to the result list and return.
  4. Otherwise, append "->" to the path and recurse into both children. A null child returns immediately at the top of the call.

Example Walkthrough

1Start DFS at root (1), path="1". Not a leaf, recurse into children.
1visit253
1/5

Code

Approach 1 allocates a new path string at every node. The next approach keeps one mutable list for the whole traversal and builds a string only at the leaves.

Approach 2: DFS with Backtracking List

Intuition

Instead of creating a new string at every node, we can use a single mutable list to accumulate the path. We append the current node's value as we go down, and explicitly remove it when we backtrack. At a leaf, joining the list with "->" produces the path string, and that join is the only string allocation in the traversal.

The tradeoff is that the undo step is now ours to manage. With string concatenation, each call held a private copy of the path, so returning required no cleanup. With a shared mutable list, whatever a call appends it must remove before returning. This is the standard backtracking pattern.

Algorithm

  1. Start DFS from the root with an empty list to track the current path.
  2. At each node, add the node's value to the path list.
  3. If the node is a leaf, join the path list with "->" to form the path string and add it to the result.
  4. Recurse into the left child (if it exists).
  5. Recurse into the right child (if it exists).
  6. Remove the last element from the path list (backtrack).

Example Walkthrough

1Visit node 1, pathList=[1]. Not a leaf, recurse left.
1visit253
1/6

Code

Both approaches so far recurse, which limits the maximum tree depth to the size of the call stack. An explicit stack removes that limit.

Approach 3: Iterative DFS with Stack

Intuition

We can simulate the recursive DFS with an explicit stack. Instead of letting the call stack manage the traversal, we push pairs of (node, current path) onto our own stack. At each step, we pop a pair, and if the node is a leaf, we add the path to the result. Otherwise, we push the children with the extended path.

Each stack entry carries its own path string, so there is no shared mutable state and no explicit backtracking. The tradeoff is that path strings sit on the stack alongside the nodes, which uses more memory than the backtracking list, and the per-node concatenation brings back the copying cost of Approach 1.

Algorithm

  1. Initialize a stack with the root node and its value as the initial path.
  2. While the stack is not empty, pop a (node, path) pair.
  3. If the node is a leaf, add the path to the result.
  4. If the node has a right child, push (right child, path + "->" + right child's value) onto the stack.
  5. If the node has a left child, push (left child, path + "->" + left child's value) onto the stack.

Example Walkthrough

1Push root: stack=[(1, "1")]. Pop node 1, not a leaf.
1pop253
1/5

Code