144. Binary Tree Preorder Traversal
Problem Description
You are given the root of a binary tree. Your task is to return a list containing the values of all nodes in the tree following a preorder traversal pattern.
In preorder traversal, you visit nodes in this specific order:
- First, visit the root node (current node)
- Then, traverse the entire left subtree
- Finally, traverse the entire right subtree
For example, if you have a binary tree like this:
1 / \ 2 3 / \ 4 5
The preorder traversal would visit nodes in this sequence: 1 → 2 → 4 → 5 → 3
, resulting in the output [1, 2, 4, 5, 3]
.
The solution uses a recursive depth-first search (DFS) approach. The dfs
helper function:
- Checks if the current node is
None
(base case) - If not, it adds the current node's value to the result list
ans
- Recursively calls itself on the left child
- Recursively calls itself on the right child
This naturally follows the preorder pattern: root → left → right. The recursion handles the tree traversal automatically by maintaining the call stack, ensuring all nodes are visited in the correct order.
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 no cycles.
DFS
- Since we answered "yes" to the tree question, the flowchart leads us directly to DFS (Depth-First Search).
Conclusion: The flowchart suggests using DFS (Depth-First Search) for traversing the binary tree. This perfectly aligns with the preorder traversal requirement, as preorder traversal is inherently a depth-first approach where we:
- Visit the current node first
- Recursively explore the left subtree completely
- Then recursively explore the right subtree completely
DFS is the natural choice for tree traversal problems, especially when we need to visit nodes in a specific order like preorder (root → left → right). The recursive nature of DFS makes it straightforward to implement the traversal pattern by processing the current node and then recursively calling the function on the left and right children.
Intuition
When we need to traverse a binary tree and collect node values in a specific order, we need to think about how we naturally explore the tree structure. Preorder traversal follows a "root-first" pattern - we want to process the current node before exploring its children.
Think of it like reading a book's table of contents: you read the chapter title first, then all its subsections on the left, and finally the subsections on the right. This natural top-down, left-to-right exploration is exactly what preorder traversal captures.
The recursive approach mirrors this intuitive process perfectly. At each node, we perform three simple actions in sequence:
- Record the current node's value (visit the root)
- Explore everything in the left subtree
- Explore everything in the right subtree
This pattern naturally emerges when we think recursively: "What do I do at this node?" The answer is always the same - process myself first, then handle my left child's entire subtree, then my right child's entire subtree. The recursion stack automatically keeps track of where we are and ensures we return to parent nodes after fully exploring their children.
The beauty of this approach is that each recursive call handles a smaller version of the same problem - traversing a subtree in preorder. The base case (when we hit a None
node) stops the recursion, and as the call stack unwinds, we naturally visit all nodes in the correct preorder sequence.
By using a shared list ans
that persists across all recursive calls, we can collect the values as we visit each node, building our final result incrementally as we traverse the tree.
Solution Approach
The solution implements a recursive traversal using Depth-First Search (DFS) to visit all nodes in preorder sequence.
Key Components:
-
Helper Function (
dfs
): We define an inner recursive function that handles the traversal logic. This function:- Takes a node as input
- Checks if the node is
None
(base case) - if so, returns immediately - Processes the current node by appending its value to the result list
- Recursively traverses the left subtree
- Recursively traverses the right subtree
-
Result Container (
ans
): We initialize an empty list before starting the traversal. This list is accessible within the nesteddfs
function and accumulates node values as we traverse.
Step-by-Step Execution:
def dfs(root):
if root is None: # Base case: empty node
return
ans.append(root.val) # Visit root (preorder)
dfs(root.left) # Traverse left subtree
dfs(root.right) # Traverse right subtree
The algorithm follows these steps:
- Start with the root node
- If the current node exists, add its value to
ans
- Recursively process the entire left subtree (this will add all left subtree values to
ans
in preorder) - Recursively process the entire right subtree (this will add all right subtree values to
ans
in preorder) - Return the completed list
Time Complexity: O(n)
where n
is the number of nodes, as we visit each node exactly once.
Space Complexity: O(h)
where h
is the height of the tree, representing the maximum recursion depth. In the worst case (skewed tree), this could be O(n)
.
The recursive approach naturally maintains the call stack, ensuring we return to parent nodes after completing their subtrees, thus achieving the desired preorder traversal pattern: root → left → right.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with a small binary tree to see how the preorder traversal works step by step.
Consider this tree:
1 / \ 2 3 / 4
Initial Setup:
ans = []
(empty result list)- Start with
dfs(root)
where root has value 1
Execution Trace:
Step 1: Call dfs(node 1)
- Node is not None, so continue
- Append 1 to ans:
ans = [1]
- Call
dfs(node 1.left)
→ This is node 2
Step 2: Call dfs(node 2)
- Node is not None, so continue
- Append 2 to ans:
ans = [1, 2]
- Call
dfs(node 2.left)
→ This is node 4
Step 3: Call dfs(node 4)
- Node is not None, so continue
- Append 4 to ans:
ans = [1, 2, 4]
- Call
dfs(node 4.left)
→ This is None
Step 4: Call dfs(None)
- Node is None, so return immediately
- Back to Step 3, now call
dfs(node 4.right)
→ This is also None
Step 5: Call dfs(None)
- Node is None, so return immediately
- Node 4 is complete, return to Step 2
Step 6: Back in dfs(node 2)
, now call dfs(node 2.right)
→ This is None
- Node is None, so return immediately
- Node 2 is complete, return to Step 1
Step 7: Back in dfs(node 1)
, now call dfs(node 1.right)
→ This is node 3
Step 8: Call dfs(node 3)
- Node is not None, so continue
- Append 3 to ans:
ans = [1, 2, 4, 3]
- Call
dfs(node 3.left)
→ This is None - Call
dfs(node 3.right)
→ This is None - Node 3 is complete, return to Step 1
Step 9: All recursive calls complete
- Final result:
ans = [1, 2, 4, 3]
Notice how the values are added to the result list in the exact order we visit them: root first (1), then entire left subtree (2, 4), then entire right subtree (3). The recursion stack naturally handles returning to parent nodes after exploring their children completely.
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 Optional, List
9
10class Solution:
11 def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
12 """
13 Performs preorder traversal of a binary tree.
14
15 Args:
16 root: The root node of the binary tree.
17
18 Returns:
19 A list containing the values of nodes in preorder sequence.
20 """
21 # Initialize result list to store traversal values
22 result = []
23
24 def traverse_preorder(node: Optional[TreeNode]) -> None:
25 """
26 Helper function to recursively traverse the tree in preorder.
27 Preorder: Root -> Left -> Right
28
29 Args:
30 node: Current node being processed.
31 """
32 # Base case: if node is None, return
33 if node is None:
34 return
35
36 # Process current node (Root)
37 result.append(node.val)
38
39 # Recursively traverse left subtree (Left)
40 traverse_preorder(node.left)
41
42 # Recursively traverse right subtree (Right)
43 traverse_preorder(node.right)
44
45 # Start traversal from the root
46 traverse_preorder(root)
47
48 return result
49
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 the result of preorder traversal
18 private List<Integer> result = new ArrayList<>();
19
20 /**
21 * Performs preorder traversal of a binary tree
22 * @param root The root node of the binary tree
23 * @return List containing values in preorder sequence (root -> left -> right)
24 */
25 public List<Integer> preorderTraversal(TreeNode root) {
26 performPreorderDFS(root);
27 return result;
28 }
29
30 /**
31 * Helper method to recursively traverse the tree in preorder
32 * @param node Current node being processed
33 */
34 private void performPreorderDFS(TreeNode node) {
35 // Base case: if node is null, return
36 if (node == null) {
37 return;
38 }
39
40 // Process current node (preorder: root first)
41 result.add(node.val);
42
43 // Recursively traverse left subtree
44 performPreorderDFS(node.left);
45
46 // Recursively traverse right subtree
47 performPreorderDFS(node.right);
48 }
49}
50
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 * Performs preorder traversal of a binary tree
16 * @param root - The root node of the binary tree
17 * @return A vector containing node values in preorder sequence
18 */
19 vector<int> preorderTraversal(TreeNode* root) {
20 // Vector to store the traversal result
21 vector<int> result;
22
23 // Lambda function for recursive depth-first search
24 // Captures result vector by reference to modify it
25 function<void(TreeNode*)> dfs = [&](TreeNode* node) {
26 // Base case: if node is null, return
27 if (!node) {
28 return;
29 }
30
31 // Preorder: process current node first
32 result.push_back(node->val);
33
34 // Recursively traverse left subtree
35 dfs(node->left);
36
37 // Recursively traverse right subtree
38 dfs(node->right);
39 };
40
41 // Start the traversal from root
42 dfs(root);
43
44 return result;
45 }
46};
47
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 * Performs preorder traversal of a binary tree
17 * @param root - The root node of the binary tree
18 * @returns An array containing the values of nodes in preorder sequence
19 */
20function preorderTraversal(root: TreeNode | null): number[] {
21 // Array to store the traversal result
22 const result: number[] = [];
23
24 /**
25 * Helper function to perform depth-first search in preorder
26 * @param node - Current node being processed
27 */
28 const traversePreorder = (node: TreeNode | null): void => {
29 // Base case: if node is null, return
30 if (!node) {
31 return;
32 }
33
34 // Process current node (root)
35 result.push(node.val);
36
37 // Recursively traverse left subtree
38 traversePreorder(node.left);
39
40 // Recursively traverse right subtree
41 traversePreorder(node.right);
42 };
43
44 // Start the traversal from the root
45 traversePreorder(root);
46
47 return result;
48}
49
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node in the tree is visited exactly once during the traversal. For each node visit, the algorithm performs a constant amount of work - appending the node's value to the result list and making recursive calls to its children. Since we visit all n
nodes exactly once and perform O(1)
work at each node, the total time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of two components:
- Recursive call stack: In the worst case (a skewed tree where each node has only one child), the recursion depth can reach
n
, requiringO(n)
stack space. In the best case (a balanced tree), the recursion depth would beO(log n)
, but we consider the worst-case scenario for complexity analysis. - Output array: The
ans
list stores alln
node values, requiringO(n)
space. However, this is often not counted as auxiliary space since it's required for the output.
The dominant factor is the recursive call stack space, which in the worst case is O(n)
. Therefore, the overall space complexity is O(n)
.
Common Pitfalls
1. Modifying the Result List Inside Recursive Calls
A common mistake is passing the result list as a parameter to the recursive function and trying to return it, which can lead to confusion about when and how to return values.
Incorrect Approach:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
def traverse(node, result):
if node is None:
return result # Problematic: multiple return points
result.append(node.val)
result = traverse(node.left, result) # Unnecessary assignment
result = traverse(node.right, result) # Unnecessary assignment
return result
return traverse(root, [])
Why it's problematic: While this might work, it's unnecessarily complex and can cause confusion about the return value. The assignments are redundant since lists are mutable and passed by reference.
Better Solution: Use the closure pattern (as shown in the original solution) where the result list is defined in the outer scope and modified by the inner function without needing to pass or return it.
2. Forgetting to Handle Empty Tree
Another pitfall is not properly handling the case when the root itself is None
.
Problematic Code:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
result = [root.val] # Error if root is None!
def traverse(node):
if node.left:
result.append(node.left.val)
traverse(node.left)
if node.right:
result.append(node.right.val)
traverse(node.right)
traverse(root)
return result
Solution: Always check if the root is None
before accessing its properties. The original solution handles this correctly by checking if node is None
at the beginning of the recursive function.
3. Using Global Variables Instead of Closure
Problematic Approach:
result = [] # Global variable - bad practice!
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
global result
result = [] # Need to reset for each call
self.traverse(root)
return result
def traverse(self, node):
global result
if node:
result.append(node.val)
self.traverse(node.left)
self.traverse(node.right)
Why it's problematic:
- Global variables persist between function calls, causing incorrect results if the function is called multiple times
- Makes the code harder to test and debug
- Violates encapsulation principles
Solution: Keep the result list local to the function and use either closure (nested function) or pass it as a parameter properly.
4. Iterative Solution Memory Management
When implementing an iterative solution using a stack, a common mistake is pushing None
nodes onto the stack:
Problematic Iterative Approach:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
stack = [root]
result = []
while stack:
node = stack.pop()
result.append(node.val)
stack.append(node.right) # Could be None!
stack.append(node.left) # Could be None!
return result
Better Iterative Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
stack = [root]
result = []
while stack:
node = stack.pop()
result.append(node.val)
# Only push non-None nodes
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
This prevents unnecessary iterations and potential errors when trying to access properties of None
nodes.
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Want a Structured Path to Master System Design Too? Don’t Miss This!