257. Binary Tree Paths
Problem Description
You are given the root of a binary tree. Your task is to find all paths from the root node to any leaf node and return them as a list of strings.
A leaf node is defined as a node that has no children (both left and right children are null).
Each path should be represented as a string where:
- Node values are connected by the arrow symbol
"->"
- The path starts from the root and ends at a leaf
For example, if you have a path from root with value 1, to a node with value 2, to a leaf with value 5, the path string would be "1->2->5"
.
The solution uses a depth-first search (DFS) approach with backtracking:
- DFS Traversal: The algorithm explores each path from root to leaves recursively
- Path Building: As we traverse down the tree, we add each node's value to a temporary list
t
- Leaf Detection: When we reach a leaf node (both left and right children are
None
), we convert the current path int
to a string with"->"
as the separator and add it to our answer list - Backtracking: After exploring a node's subtrees, we remove the node's value from
t
usingt.pop()
to backtrack and explore other paths - Result: All collected root-to-leaf paths are returned as a list of strings
The time complexity is O(n)
where n is the number of nodes, as we visit each node once. The space complexity is O(n)
for the recursion stack and storing the paths.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure, which is a hierarchical data structure with a root node and parent-child relationships.
DFS
- Yes: Since we identified this as a tree problem, the flowchart directs us to use DFS (Depth-First Search).
Conclusion: The flowchart suggests using a DFS approach for finding all root-to-leaf paths in the binary tree.
This makes perfect sense for our problem because:
- We need to explore each complete path from root to leaf before moving to the next path
- DFS naturally follows paths to their completion (reaching leaves) before backtracking
- The recursive nature of DFS aligns well with the recursive structure of trees
- We can easily build the path string as we traverse down and use backtracking to explore alternative paths
The DFS pattern is ideal here as it allows us to:
- Maintain the current path as we traverse
- Detect when we've reached a leaf node
- Backtrack to explore all possible root-to-leaf paths
- Collect all complete paths in our result list
Intuition
When we need to find all paths from root to leaves, we naturally think about exploring the tree systematically. Imagine you're walking through a maze and need to record every possible route from the entrance to any exit. You'd walk down one path, record it when you reach an exit, then backtrack to explore other paths.
The key insight is that we need to:
- Keep track of our current path as we traverse
- Know when we've reached a destination (leaf node)
- Be able to "undo" our steps to explore other paths
This naturally leads us to DFS with backtracking. As we traverse down the tree, we build our path by adding each node's value. When we reach a leaf (a node with no children), we've found a complete path and can save it. Then we need to backtrack - remove the current node from our path - so we can explore other branches.
Think of it like building a string of beads: we add a bead (node value) as we go down, and when we need to try a different path, we remove the last bead and try another direction. The temporary list t
acts as our bead string, growing as we go deeper and shrinking as we backtrack.
The beauty of this approach is that the recursive call stack naturally handles the backtracking for us. Each recursive call represents a decision point in the tree, and when a call returns, we automatically return to the previous decision point. By adding t.pop()
after the recursive calls, we ensure our path list accurately reflects our current position in the tree traversal.
Solution Approach
The solution implements a DFS traversal with backtracking using a helper function. Let's walk through the implementation step by step:
Core Data Structures:
ans
: A list to store all complete root-to-leaf paths as stringst
: A temporary list that acts as a stack to build the current path
The DFS Helper Function:
-
Base Case: If the current node is
None
, we simply return (nothing to process) -
Path Building: Add the current node's value to our temporary path list
t
by converting it to a string:t.append(str(root.val))
-
Leaf Detection: Check if the current node is a leaf by verifying both children are
None
:if root.left is None and root.right is None: ans.append("->".join(t))
When we find a leaf, we join all values in
t
with"->"
to create the path string and add it to our answer list -
Recursive Exploration: If not a leaf, recursively explore both subtrees:
dfs(root.left) dfs(root.right)
-
Backtracking: After exploring both children (or determining it's a leaf), remove the current node's value from the path:
t.pop()
This crucial step ensures that when we return to the parent node, our path list
t
accurately reflects the path up to the parent, allowing us to explore other branches
Main Function Flow:
- Initialize empty lists for
ans
(final results) andt
(temporary path) - Call
dfs(root)
to start the traversal from the root - Return
ans
containing all root-to-leaf paths
The algorithm visits each node exactly once, making it O(n)
in time complexity. The space complexity is O(n)
for the recursion stack in the worst case (skewed tree) and for storing the output paths.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the DFS with backtracking solution works.
Consider this binary tree:
1 / \ 2 3 \ 5
We want to find all root-to-leaf paths: ["1->2->5", "1->3"]
Step-by-step execution:
-
Start at root (1)
t = []
- Add 1 to path:
t = ["1"]
- Node 1 has children, so continue exploring
-
Go left to node 2
- Add 2 to path:
t = ["1", "2"]
- Node 2 has a right child, so continue
- Add 2 to path:
-
Go right to node 5
- Add 5 to path:
t = ["1", "2", "5"]
- Node 5 is a leaf (no children)!
- Join path with "->":
"1->2->5"
- Add to answer:
ans = ["1->2->5"]
- Backtrack: Remove 5 from path:
t = ["1", "2"]
- Add 5 to path:
-
Back at node 2
- Left child is None (already explored)
- Right child is done (already explored)
- Backtrack: Remove 2 from path:
t = ["1"]
-
Back at root (1), go right to node 3
- Add 3 to path:
t = ["1", "3"]
- Node 3 is a leaf (no children)!
- Join path with "->":
"1->3"
- Add to answer:
ans = ["1->2->5", "1->3"]
- Backtrack: Remove 3 from path:
t = ["1"]
- Add 3 to path:
-
Back at root (1)
- Both children explored
- Backtrack: Remove 1 from path:
t = []
- Return
ans = ["1->2->5", "1->3"]
The key insight is how the temporary list t
grows and shrinks:
- It grows as we traverse down:
[] → ["1"] → ["1","2"] → ["1","2","5"]
- It shrinks as we backtrack:
["1","2","5"] → ["1","2"] → ["1"] → ["1","3"] → ["1"] → []
This backtracking mechanism ensures we can explore all paths while maintaining the correct current path at each step of the traversal.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import List, Optional
9
10class Solution:
11 def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
12 """
13 Find all root-to-leaf paths in a binary tree.
14
15 Args:
16 root: The root node of the binary tree
17
18 Returns:
19 A list of strings representing all root-to-leaf paths
20 """
21
22 def dfs(node: Optional[TreeNode]) -> None:
23 """
24 Depth-first search to traverse the tree and collect paths.
25
26 Args:
27 node: Current node being processed
28 """
29 # Base case: if node is None, return
30 if node is None:
31 return
32
33 # Add current node's value to the path
34 current_path.append(str(node.val))
35
36 # Check if current node is a leaf node
37 if node.left is None and node.right is None:
38 # Found a complete root-to-leaf path, add it to results
39 result_paths.append("->".join(current_path))
40 else:
41 # Continue traversing left and right subtrees
42 dfs(node.left)
43 dfs(node.right)
44
45 # Backtrack: remove current node from path before returning
46 current_path.pop()
47
48 # Initialize result list to store all root-to-leaf paths
49 result_paths = []
50
51 # Initialize temporary list to build current path during traversal
52 current_path = []
53
54 # Start DFS traversal from root
55 dfs(root)
56
57 return result_paths
58
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 // List to store all root-to-leaf paths
18 private List<String> allPaths = new ArrayList<>();
19
20 // List to store the current path being explored
21 private List<String> currentPath = new ArrayList<>();
22
23 /**
24 * Finds all root-to-leaf paths in a binary tree
25 * @param root The root of the binary tree
26 * @return A list of strings representing all root-to-leaf paths
27 */
28 public List<String> binaryTreePaths(TreeNode root) {
29 depthFirstSearch(root);
30 return allPaths;
31 }
32
33 /**
34 * Performs depth-first search to find all root-to-leaf paths
35 * Uses backtracking to explore all possible paths
36 * @param node The current node being processed
37 */
38 private void depthFirstSearch(TreeNode node) {
39 // Base case: if node is null, return
40 if (node == null) {
41 return;
42 }
43
44 // Add current node value to the path
45 currentPath.add(String.valueOf(node.val));
46
47 // Check if current node is a leaf node
48 if (node.left == null && node.right == null) {
49 // Found a complete root-to-leaf path, add it to results
50 allPaths.add(String.join("->", currentPath));
51 } else {
52 // Continue exploring left and right subtrees
53 depthFirstSearch(node.left);
54 depthFirstSearch(node.right);
55 }
56
57 // Backtrack: remove current node from path before returning
58 currentPath.remove(currentPath.size() - 1);
59 }
60}
61
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 /**
15 * Finds all root-to-leaf paths in a binary tree
16 * @param root The root node of the binary tree
17 * @return A vector of strings representing all root-to-leaf paths
18 */
19 vector<string> binaryTreePaths(TreeNode* root) {
20 vector<string> result; // Stores all root-to-leaf paths
21 vector<string> currentPath; // Stores the current path being explored
22
23 // Define recursive DFS function to traverse the tree
24 function<void(TreeNode*)> dfs = [&](TreeNode* node) {
25 // Base case: if node is null, return
26 if (!node) {
27 return;
28 }
29
30 // Add current node's value to the path
31 currentPath.push_back(to_string(node->val));
32
33 // If it's a leaf node, add the complete path to result
34 if (!node->left && !node->right) {
35 result.push_back(buildPathString(currentPath));
36 }
37 // Otherwise, continue exploring left and right subtrees
38 else {
39 dfs(node->left);
40 dfs(node->right);
41 }
42
43 // Backtrack: remove current node from path before returning
44 currentPath.pop_back();
45 };
46
47 // Start DFS traversal from root
48 dfs(root);
49 return result;
50 }
51
52private:
53 /**
54 * Joins vector elements into a single string with separator
55 * @param pathElements Vector of string elements to join
56 * @param separator The separator string (default: "->")
57 * @return A string with all elements joined by the separator
58 */
59 string buildPathString(const vector<string>& pathElements, const string& separator = "->") {
60 string pathString;
61
62 for (size_t i = 0; i < pathElements.size(); ++i) {
63 // Add separator before each element except the first one
64 if (i > 0) {
65 pathString += separator;
66 }
67 pathString += pathElements[i];
68 }
69
70 return pathString;
71 }
72};
73
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Finds all root-to-leaf paths in a binary tree
17 * @param root - The root node of the binary tree
18 * @returns An array of strings representing all root-to-leaf paths
19 */
20function binaryTreePaths(root: TreeNode | null): string[] {
21 // Array to store all root-to-leaf paths as strings
22 const allPaths: string[] = [];
23
24 // Temporary array to track the current path being explored
25 const currentPath: number[] = [];
26
27 /**
28 * Depth-first search helper function to traverse the tree and collect paths
29 * @param node - The current node being visited
30 */
31 const traverseAndCollectPaths = (node: TreeNode | null): void => {
32 // Base case: if node is null, return immediately
33 if (!node) {
34 return;
35 }
36
37 // Add current node's value to the path
38 currentPath.push(node.val);
39
40 // Check if current node is a leaf node (no left or right children)
41 if (!node.left && !node.right) {
42 // Convert the current path to string format and add to results
43 allPaths.push(currentPath.join('->'));
44 } else {
45 // Recursively explore left subtree
46 traverseAndCollectPaths(node.left);
47
48 // Recursively explore right subtree
49 traverseAndCollectPaths(node.right);
50 }
51
52 // Backtrack: remove current node from path before returning
53 currentPath.pop();
54 };
55
56 // Start DFS traversal from the root
57 traverseAndCollectPaths(root);
58
59 return allPaths;
60}
61
Time and Space Complexity
Time Complexity: O(N²)
in the worst case, where N
is the number of nodes in the binary tree.
- The DFS traversal visits each node exactly once, which takes
O(N)
time. - At each leaf node, we create a string by joining the path elements using
"->".join(t)
. In the worst case (a skewed tree), the path length can beO(N)
, and string concatenation takesO(N)
time. - The number of leaf nodes can be at most
O(N)
(in the case of a skewed tree, there's only 1 leaf, but in a balanced tree, it could beO(N/2)
). - Therefore, the total time complexity is
O(N)
for traversal +O(N)
for string construction at each leaf node in the worst case =O(N²)
.
Space Complexity: O(N)
in the worst case.
- The recursion stack depth can be at most
O(N)
in the case of a skewed tree (orO(log N)
for a balanced tree). - The temporary list
t
stores the current path, which can have at mostO(N)
elements in the worst case. - The output list
ans
stores all root-to-leaf paths. In the worst case (a complete binary tree), there can beO(N/2)
leaf nodes, and each path has an average length ofO(log N)
, resulting inO(N log N)
total characters stored. However, since we're typically counting space complexity in terms of the number of nodes, this is consideredO(N)
. - The dominant factor is
O(N)
for the recursion stack and path storage.
Common Pitfalls
1. Forgetting to Backtrack (Missing pop()
operation)
The Pitfall:
One of the most common mistakes is forgetting to remove the current node from the path after processing its subtrees. Without the current_path.pop()
line, the path list keeps accumulating nodes incorrectly, leading to wrong path representations.
Incorrect Code Example:
def dfs(node):
if node is None:
return
current_path.append(str(node.val))
if node.left is None and node.right is None:
result_paths.append("->".join(current_path))
else:
dfs(node.left)
dfs(node.right)
# MISSING: current_path.pop() ← This line is crucial!
What Goes Wrong: Without backtracking, if you have a tree like:
1 / \ 2 3
After visiting path 1->2, the current_path
would still contain [1, 2]. When you then visit node 3, the path becomes [1, 2, 3] instead of [1, 3], resulting in incorrect output: ["1->2", "1->2->3"] instead of ["1->2", "1->3"].
The Solution: Always remember to pop the current node from the path after exploring all its children:
def dfs(node):
if node is None:
return
current_path.append(str(node.val))
if node.left is None and node.right is None:
result_paths.append("->".join(current_path))
else:
dfs(node.left)
dfs(node.right)
current_path.pop() # ← Critical for backtracking!
2. Creating a New List Instead of Using Backtracking
The Pitfall: Some might try to pass a new copy of the path list to each recursive call instead of using a shared list with backtracking:
def dfs(node, path):
if node is None:
return
# Creating a new list for each recursive call
new_path = path + [str(node.val)]
if node.left is None and node.right is None:
result_paths.append("->".join(new_path))
else:
dfs(node.left, new_path)
dfs(node.right, new_path)
What Goes Wrong: While this approach works correctly, it's less efficient in terms of space complexity. Creating new lists for each recursive call uses O(n * h) extra space where h is the height of the tree, compared to O(h) for the backtracking approach.
The Solution: Use a single shared path list with proper backtracking for optimal space efficiency. The backtracking approach reuses the same list throughout the traversal, making it more memory-efficient.
3. Not Converting Node Values to Strings
The Pitfall: Forgetting to convert node values to strings before adding them to the path:
current_path.append(node.val) # Wrong: should be str(node.val)
What Goes Wrong:
When you try to join the path with "->".join(current_path)
, Python will throw a TypeError because join()
expects an iterable of strings, not integers.
The Solution: Always convert node values to strings before adding them to the path:
current_path.append(str(node.val))
How does merge sort divide the problem into subproblems?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Want a Structured Path to Master System Design Too? Don’t Miss This!