Binary Tree Traversals
The goal of this document is to present a collection of things about binary tree traversals to get the reader up to speed while solving simple tree related problems. (Be it interview prep or competitive programming or application programming; This document will try to act as a quick intro/revision)
Trees are actually a kind of graph. Hence it could be traversed/searched in two ways (most of the times, this viewpoint is not presented in textbooks because we are used to learning graphs after trees.)
- Depth-first
- Breadth-first
Traversal = Visiting each node of the tree exactly once
Search = Given a value, search if a node in the tree contains that value
Both Traversal and search on a tree could be done via both depth-first and breadth-first manner.
Structure
Before solving the traversal problems, I would like to present the structure of the binary tree.
1 | struct TreeNode { |
This is the structure used by the leetcode, but it will be similar elsewhere.
Depth-first
Here is a simple image from Wikipedia for a warm up.
Various types of depth-first traversal/search are
- Pre-Order (NLR)
- In-Order (LNR)
- Post-Order (LRN)
Traversals are usually classified based on the order in which the root node is visited.
If the root node is visited before other nodes, then it is called PRE-order.
If the root node is visited after other nodes, then it is called POST-order.
If the root node is visited in-between the visits of other nodes, then it is called IN-order.
Usually, the order in which the children nodes are visited does not matter i.e. left before right and right before left. But most materials and implementations do left before right.
Remember that the key to solving Depth-first traversal/search would be to use a stack - call stack in case of recursion and a stack implementation in case of an iterative approach. Don’t worry much about this, but this going to be your take-home of the day and if you don’t get this pattern in the first go, don’t be worried; you will eventually get it!
Before moving on, I would like to state a very useful resource - Wikipedia page, that contains crisp and clear pseudocode for most of the code written here. So do check it out!
Pre-order
The root node is visited before visiting the children.
Pre-order for the example tree given in the above figure would be FBADCEGIH
Recursive Solution
1 | class Solution { |
Iterative Solution
1 | class Solution { |
In-order
Nodes are visited in left, root and right order.
I just noticed the term “Out-order” in the Wikipedia page, for which the nodes are visited in right, root and left order. Some textbooks do not mention this explicitly.
This traversal seems to be useful especially for binary search trees. Example, in-order traversal of a BST gives ascending order of values present in it, whereas the out-order gives the descending order.
In-order for the example tree given in the above figure would be ABCDEFGHI
Out-order for the example tree given in the above figure would be IHGFEDCBA
Recursive Solution
1 | class Solution { |
Iterative Solution
1 | class Solution { |
The iterative of in-order could seem complex at first. So, I suggest you to go through videos that explain it by tracing out on a whiteboard and eventually tracing out by yourself on a piece of paper.
Post-order
The root node is visited after visiting the children.
Post-order for the example tree given in the above figure would be ACEDBHIGF
Recursive Solution
1 | class Solution { |
Iterative Solution
This could be done in a couple of ways. One is to use two stacks and other is to build upon the algorithm used for in-order.
To be simple, I will list out the two stacks method here.
1 | class Solution { |
Breadth-first
Breadth-first traversal/search, on the other hand, looks like
Answer: FBGADICEH
I love doing this so much that I often try to solve any tree problem thrown at me with this first. haha. Why? This is probably one of the algorithms where a queue is used elegantly to solve the problem and you could use this technique to solve a lot of tree problems.
This is also referred to as level-order traversal sometimes.
I would like to show a few variations of the solution. Because these variations might often help you in implementing level-order and tweaking it a bit to arrive at your custom solution.
Using one queue and get a result back as a simple list of visited values1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class Solution {
public:
vector<int> levelOrder(TreeNode* root) {
vector<int> vec;
if (root == nullptr)
return vec;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
auto curr = q.front();
q.pop();
vec.push_back(curr->val);
if (curr->left != nullptr)
q.push(curr->left);
if (curr->right != nullptr)
q.push(curr->right);
}
return vec;
}
};
Using two queues for level-order traversal. (Not so elegant. hmmm, yeah!)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> vec;
if (root == nullptr)
return vec;
queue<TreeNode*> q;
q.push(root);
while (true) {
queue<TreeNode*> children;
vector<int> level;
while (!q.empty()) {
auto curr = q.front();
q.pop();
level.push_back(curr->val);
if (curr->left != nullptr)
children.push(curr->left);
if (curr->right != nullptr)
children.push(curr->right);
}
vec.push_back(level);
swap(q, children);
if (q.empty() && children.empty())
break;
}
return vec;
}
};
The following code does the same thing as above but avoids a while(true)
.
1 | class Solution { |
Complexity
All the above solutions have
Time Complexity = O(N)
Space Complexity = O(N)
Because you visit every node and allocate call-stack/stack/queue for N nodes.
If you want a more space efficient solution, check out Morris Traversal.
What to choose?
With all these solutions for solving a problem put in front of you, you might be wondering which way to choose. So here is a small piece on it.
Most of the times, go with iterative stack/queue implementation.
Recursive is clean and help you to think about a lot of tree problems. But for larger trees, you might face call-stack overflow. On the other hand, the iterative stack/queue is allocated on the heap and often times heap is big enough to accommodate your nodes.
At the same time, while interviewing don’t try to solve by Morris traversal unless explicitly mentioned by the interviewer. Because it is time-consuming and often not clear. Even while writing real-world code, you might go with iterative implementation and then refactor to Morris (when you have time for it).
Closing Notes
- If Depth-first, think in stacks
- If Breadth-first, think in queues
- All solutions above have time complexity = O(N) because each node is visited once.
- All solutions above have space complexity = O(N) because of call-stack/stack/queue space used.
- For a more efficient (but complex) solution, check out Morris traversal.
References
- Tree Traversals - https://en.wikipedia.org/wiki/Tree_traversal
- Post-order two stacks - https://www.youtube.com/watch?v=qT65HltK2uE
Image Attributions