Algorithm

Binary Tree Paths

Trees Pattern

Binary Tree Paths

Given the root of a binary tree, return all root-to-leaf paths, each rendered as a string of node values joined by "->", for example "1->2->5". A leaf is a node with both children null; a path must end at a leaf and may not stop partway. The paths may be returned in any order. The tree has at least one node, so the result is never empty.

CONSTRAINTS
  • The number of nodes in the tree is in the range [1, 100]
  • -100 <= Node.val <= 100
  • Every returned path starts at the root and ends at a leaf
  • Order of the returned paths does not matter
EXAMPLE 1
Input: root = [1,2,3,null,5]
Output: ["1->2->5", "1->3"]
There are exactly two leaves — 5 and 3 — and therefore exactly two paths. Node 2 is not a leaf (it has a right child), so "1->2" is not an answer even though it is a valid downward route.
EXAMPLE 2
Input: root = [1]
Output: ["1"]
The root is itself a leaf, so the single path contains one node and no arrow separator at all.
EXAMPLE 3
Input: root = [1,2,null,3]
Output: ["1->2->3"]
A chain has only one leaf, so only one path exists. Nodes with a single child are passed through, never treated as endpoints.
EXAMPLE 4
Input: root = [-1,2,-3]
Output: ["-1->2", "-1->-3"]
Negative values are rendered with their minus sign, which sits next to the arrow without any special handling — the string is just the values as written.
What exactly counts as a leaf?
A node whose left and right are both null. A node with one child is not a leaf, and treating it as one is the single most common bug here.
Does the order of the returned paths matter?
No, any order is accepted. Natural depth-first order comes out left-to-right, which is usually what a grader expects anyway.
Can the tree be empty?
Not per the constraints — at least one node is guaranteed. Worth confirming, since an empty tree would have to return an empty list rather than a list holding an empty string.
Should the values be separated exactly by '->' with no spaces?
Yes, and the separator appears only *between* values, never before the first or after the last.

Everything so far has computed a single number by letting information travel upward: a node asked its children for their answers and combined them. This problem is the first that cannot work that way, and seeing why is the whole point.

A leaf cannot say anything useful about "the path from the root to me." It has no idea what the root is, or which turns were taken to reach it. The information a leaf needs — the sequence of values above it — exists only in its ancestors. So it has to be handed down, as an extra argument travelling with the recursive call. That is the other half of the pair introduced in Maximum Depth: results come up as return values, context goes down as parameters.

Carrying history down

Give the recursive function a second argument: the path built so far, not yet including the current node. Then one node's job is small.

- Extend the inherited history with your own value.
- If you are a leaf — both children null — that extended history is a complete root-to-leaf path, so record it.
- Otherwise you are just a waypoint; hand the extended history to each child that exists and let them carry on.

Because the extension has to happen before the children are called (they need it), the work sits in the slot before the recursion — a pre-order shape. That is not a style choice: children literally cannot run without the value.

python
out = []

def walk(node, path):
    if node is None:
        return
    path = path + [node.val]                 # extend — a NEW list, see the note below
    if node.left is None and node.right is None:
        out.append("->".join(map(str, path)))   # a leaf: this path is complete
        return
    walk(node.left, path)
    walk(node.right, path)

walk(root, [])
return out
Crucial Notethe leaf test is left is None and right is None, never "the recursion returned nothing" and never a check on one side only. A node with exactly one child looks like a dead end from the empty side, and if you record a path whenever a child is null you will emit "1->2" for a node 2 that still has a right child — a path that stops in mid-air. The null-node base case at the top and the leaf test are two different things doing two different jobs: the first stops us walking off the tree, the second decides when an answer is complete.
Sharing one list instead: backtracking

The line path = path + [node.val] builds a brand-new list at every node. That is safe — each call owns its own copy, so a sibling can never see another sibling's additions — but it copies up to H values per node, so the copying alone costs O(N × H).

The alternative is to keep one list for the entire traversal, shared by every call. Then it must be repaired on the way out, because after finishing the left subtree that shared list still has all the left subtree's values sitting in it, and the right child would inherit garbage. So: append your value before descending, and remove it after both children return.

python
path = []

def walk(node):
    if node is None:
        return
    path.append(node.val)                   # entering: I am on the route
    if node.left is None and node.right is None:
        out.append("->".join(map(str, path)))
    else:
        walk(node.left)
        walk(node.right)
    path.pop()                              # leaving: undo, so siblings start clean

This add-on-entry, remove-on-exit discipline is called backtracking, and the invariant it maintains is worth saying out loud: at any instant, path holds exactly the nodes on the route from the root to the node currently being processed — nothing more. The pop is what makes that true; without it the list only ever grows and every recorded path is polluted with branches that were abandoned. Notice that the append happens before the calls and the pop after them, so the same function has work in both the pre-order and post-order slots. That combination is the signature of backtracking, and you will meet it again throughout the backtracking section with subsets and permutations.

Worked example:root 1, with left child 2 and right child 3, where 2's only child is 5 on the right
- At 1: path becomes [1]. Not a leaf, so descend left.
- At 2: path becomes [1, 2]. Its left is null and returns immediately; its right is 5, so it is not a leaf and nothing is recorded here.
- At 5: path becomes [1, 2, 5]. Both children null — record "1->2->5". On the way out, pop 5.
- Back at 2: both children handled, pop 2. The path is [1] again — exactly the state node 3 needs.
- At 3: path becomes [1, 3]. Both children null — record "1->3". Pop 3.
- Result: ["1->2->5", "1->3"].

Watch the moment after 5 is recorded: if the pops were missing, node 3 would have inherited [1, 2, 5] and emitted "1->2->5->3", a route that does not exist in the tree. The undo step is not tidiness, it is correctness.

What it costs

Each node is visited once, so the traversal itself is O(N). But the output is not a single number — it is one string per leaf, each up to H characters, so simply writing the answer takes O(N × H) in the worst case, and no algorithm can be faster than its own output. Space is O(H) for the recursion and the shared path, plus the output itself. With the copying version, the copies also cost O(N × H), which is why backtracking is preferred once paths get long.

The generalisation to carry forward: decide what each node needs, then decide which direction that information flows. Needs facts about its own subtree (height, sum, validity) → return values upward, post-order. Needs facts about its ancestry (the route taken, the depth, an accumulated total) → pass parameters downward, pre-order. Path Sum is the same downward flow with a running total instead of a list; Root to Node Path is the same backtracking with an early exit as soon as the target is found; Sum Root to Leaf Numbers carries a number down instead of a sequence. Once you can spot the direction, these stop being separate problems.

Interactive Strategy Visualization

Root-to-Leaf Paths

DFS Path Serialization

1235
Start at Root. Current path: '1'
Execution State
Path Buffer
"1"
Global Results List
Pending...

"When recursing, concatenate node val to path. When back from a child, the old string is restored."

Strategy

DFS explores every path down to the leaves. If `!node.left && !node.right`, the current string is complete.

"Leaf Check: node.left === node.right === null"

O(N × H) Time · O(N × H) Space Copying Paths
O(N × H) Time · O(H) Working Space Backtracking