visit
To understand recursion, you must understand recursion. I will show you 13 different ways to traverse a tree to compare recursive and iterative implementations. This way, we will kill two birds with one stone: recursion and .
“Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”
Recursion, iteration and how to traverse a tree are useful skills to have and common in interview questions. Do not risk failing your next job interview because you didn’t take the time to read one article.
I assume you have basic coding skills and are familiar with stacks, queues, recursion, and loops. If this seems too advanced for you, check where I have listed some resources to get you started.
For every problem, I will provide also a link to Leetcode so that you can play around with my solution or write your own in your preferred programming language.My code examples will be in C++. If you are not familiar with the APIs or syntax, that is fine. I am sure you will understand the ideas. That is the goal of this article.Trees are data structures that organize data hierarchically. They are defined recursively from a root node, that contains the data that needs to be stored and pointers to its children.
There are many types of trees well-suited for specific applications:There is no point in storing information if you do not know how to access it.
For simplicity, I will focus on binary trees. You will take the code and generalize to trees with up to N children per node.I will present you with 13 ways to traverse a tree and discuss them in detail. As usual, there will be challenges for you to cement your knowledge. I strongly recommend solving each problem on your own before reading my solution.You are going to learn infinitely more by doing than by reading or watching.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
}
For the complexity analysis, we will assume that we will traverse a tree of height H that contains N nodes.
Given the root of a binary tree, return the preorder traversal of its nodes’ values.
You can play around with this problem .Solution
The pre-order, in-order, and post-order traversals of a tree are defined by the order in which we visit the root, left, and right children of a tree. The pre in pre-order refers to the root. In a preorder traversal of a tree, we first visit the root node, then the left node, and finally the right node.You can translate this into recursive code very easily. Your function will receive a node, starting with the root of the tree. You need to define:class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
if(!root)
return {};
vector<int> sol;
helperA(root, sol);
// helperB(root, sol);
return sol;
}
void helperA(TreeNode* n, vector<int>& sol){
if(n){
sol.push_back(n->val);
helperA(n->left, sol);
helperA(n->right, sol);
}
}
void helperB(TreeNode* n, vector<int>& sol){
sol.push_back(n->val);
if(n->left)
helperB(n->left, sol);
if(n->right)
helperB(n->right, sol);
}
};
Option A. Every function call will test whether the node it receives is null or not. Then, it will trigger two recursive calls. This option will result in a larger stack.
Option B. The function assumes that the node is not null. It will generate a recursive call only if the left/right child is not null. The function will execute more if statements but make less recursive calls.
There is no right or wrong solution here without more context. In an interview setting, discuss both options. It will show that you care about your code and think of the implications of what you write.
Your priority should be writing readable and maintainable working code. Only focus on this level of detail when you have profiled your code and have proof that these lines of code make a big impact on the overall performance. Don’t guess. Remember.
“Premature optimization is the root of all evil.”
Complexity
This algorithm visits every node once and adds an element to the end of a vector, which is constant work – . Therefore, the time complexity is O(N).Can you try to figure out the space complexity? Try to visualize the algorithm on the tree above. How many nodes will be stored in the stack in the “worst-case”?I am sure you found the correct answer. The maximum number of nodes that you need to store will be equal to the height of the tree, H. Consequently, the space complexity is O(H).In the next problem, I will show you a non-recursive implementation of the same algorithm to traverse a tree. But before that, I have a little challenge for you.Challenge
Given an n-ary tree, return the preorder traversal of its nodes’ values.
You can code your solution .To make you think
A natural follow-up to this problem is to generalize it to trees with N children. Before opening the link to Leetcode, try to first model the nodes yourself. Some interesting questions are:
Solution
Now we will see that recursion is not the only way to traverse a tree.We need to mimic everything that happens under the hood when we use recursive functions. We know that:class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
if(!root)
return {};
vector<int> sol;
stack<TreeNode*> s;
s.push(root);
while(!s.empty()){
auto t = s.top(); s.pop();
sol.push_back(t->val);
if(t->right)
s.push(t->right);
if(t->left)
s.push(t->left);
}
return sol;
}
};
Complexity
The complexity analysis does not change with respect to the recursive version. We still need to visit the N nodes and do constant work per node. Therefore the time complexity is O(N).Regarding the space complexity, the stack will have to store at most a number of nodes proportional to the height of the tree, O(H).Challenge
It is time for you now to generalize this solution to work with a tree of N nodes. You can test your solution .Given the root of a binary tree, return the inorder traversal of its nodes’ values.
Give it a try .Solution
In-order traversal is a way to traverse a tree where you first visit the left child, then the root and then the right child. In a binary search tree (like the one above), it will visit the nodes in increasing order.The recursive solution is straightforward. The only difference with the pre-order recursive solution is the order in which we visit the root and the children. It is a minor change. Go ahead and implement it yourself.Everything we discussed about the extra if statements applies to this problem, as well as the discussion about stack vs RAM. Here is a solution with the same options A and B as before.class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
if(!root)
return {};
vector<int> sol;
helperA(root, sol);
//helperB(root, sol);
return sol;
}
void helperA(TreeNode* n, vector<int>& sol){
if(n){
helperA(n->left, sol);
sol.push_back(n->val);
helperA(n->right, sol);
}
}
void helperB(TreeNode* n, vector<int>& sol){
if(n->left)
helperB(n->left, sol);
sol.push_back(n->val);
if(n->right)
helperB(n->right, sol);
}
};
Complexity
The complexity analysis is the same as the previous recursive example. We are doing exactly the same, just in a different order.Traverse a tree using in-order traversal. No recursion allowed.
Test your solution .Solution
I know what you are thinking. You are right. We need a stack.Let’s visualize the in-order traversal of the previous tree.We need to mimic this with our stack. Let’s call it s.
From this algorithm, it looks like we need a stack and a pointer n to mark the node we are currently at. If n becomes null, it means we are done exploring a subtree and need to query the stack for the next node to process. If the stack is empty, we have processed all nodes.
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
if(!root)
return {};
vector<int> sol;
stack<TreeNode*> s;
TreeNode* n = root;
while(n || !s.empty()) {
const bool hasToGoRight = !n;
if(hasToGoRight){
auto top = s.top(); s.pop();
sol.push_back(top->val);
n = top->right;
} else {
s.push(n);
n = n->left;
}
}
return sol;
}
};
Complexity
The complexity analysis is exactly the same as before: O(N) time complexity and O(H) space complexity.Given the root of a binary tree, return the postorder traversal of its nodes’ values.
Check your implementation .Solution
In a post-order traversal, we first visit the left child, then the last child and finally the root of the tree, hence the name post-order. This algorithm requires a minimal change with respect to the pre-order and in-order traversals that we have just seen.
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if(!root)
return {};
vector<int> sol;
helperA(root, sol);
//helperB(root, sol);
return sol;
}
void helperA(TreeNode* n, vector<int>& sol){
if(n){
helperA(n->left, sol);
helperA(n->right, sol);
sol.push_back(n->val);
}
}
void helperB(TreeNode* n, vector<int>& sol){
if(n->left)
helperB(n->left, sol);
if(n->right)
helperB(n->right, sol);
sol.push_back(n->val);
}
};
Complexity
Same as pre-order’s and in-order’s.Challenge
Try to generalize this to work with N nodes. Code your solution .Traverse a binary tree using post-order traversal. No recursion allowed.
Try your solution .Solution
If you are thinking that we need a stack again, you are right.I am familiar with relatively complex solutions based on one stack. This solution can be implemented with two stacks or just one stack and one vector. This can be done with a second stack. The space complexity is still O(N).If you remember correctly, for iterative pre-order traversal we added the root to the solution and then pushed the left and right child. I know this comes out of nowhere, but let’s see what happens if we first push the left child and then the right child.class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if(!root)
return {};
stack<TreeNode*> s;
s.push(root);
vector<int> sol;
while(!s.empty()){
auto top = s.top(); s.pop();
sol.push_back(top->val);
if(top->left)
s.push(top->left);
if(top->right)
s.push(top->right);
}
for(auto x : sol)
cout<<x<<endl;
return sol;
}
};
Above all, it is worth noting is that sometimes it is useful to play with similar problems to try to get a solution to the problem you are trying to solve.
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if(!root)
return {};
stack<TreeNode*> s;
s.push(root);
vector<int> sol;
while(!s.empty()){
auto top = s.top(); s.pop();
sol.push_back(top->val);
if(top->left)
s.push(top->left);
if(top->right)
s.push(top->right);
}
reverse(sol.begin(), sol.end());
return sol;
}
};
Complexity
The complexity is linear both in time and space:Solution
You are right. You still need to store some information about the nodes to visit them. This algorithm modifies the nodes to be able to traverse the tree without explicit data structures. At the end of the algorithm, the nodes is back to their original state.Let’s focus on the in-order traversal. Pre-order just needs a minor modification.We will have two pointers: current and explorer. We will use explorer to move around the tree and build links from some leaves back to current. Current can only move left if we have previously built a link from its predecessor back to current. This is the only thing I have “memorized” about Morris. From this, I can draw a tree and rebuild the algorithm every time.
This image has all the links that we will need to build. However, we do not need to keep any of them: as soon as we are done using a bridge, we destroy it.
The algorithm goes as follows:Example
Let’s run the algorithm step by step on the tree above:class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> sol;
auto current = root;
while(current){
if(!current->left) {
sol.push_back(current->val);
current = current->right;
} else {
TreeNode* explorer = current->left;
//Find currents predecessor
while(explorer->right && explorer->right != current) {
explorer = explorer->right;
}
//Is it linking back to current?
if(explorer->right == current) {
explorer->right = nullptr;
sol.push_back(current->val);
current = current->right;
} else {
explorer->right = current;
current = current->left;
}
}
}
return sol;
}
};
//Is it linking back to current?
if(explorer->right == current) {
explorer->right = nullptr;
current = current->right;
} else {
sol.push_back(current->val);
explorer->right = current;
current = current->left;
}
Complexity
The time complexity is an exercise for you.The space complexity is O(1). We are implicitly using more since we are modifying the tree, but technically we do not need extra space to execute this algorithm.Challenge
Can you find some potential issues with this algorithm? For example, if multiple threads had to perform this algorithm on the same tree.Given a binary tree, return the level order traversal of its nodes’ values. (ie, from left to right, level by level).
For this tree, the solution looks like: [[15], [10, 21], [5, 20, 23], [6]].Try to solve it .Solution
This time I will start with the iterative solution because I find it simpler to code. We can solve this problem by applying . The only trick is how to know when to start a new level. We can do this in two ways:class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
if(!root)
return {};
vector<vector<int>> sol;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
int size = q.size();
vector<int> partial;
while(size-->0){
auto n = q.front();
partial.push_back({n->val});
q.pop();
if(n->left)
q.push({n->left});
if(n->right)
q.push({n->right});
}
sol.push_back(partial);
}
return sol;
}
};
Complexity
We visit N nodes and do constant work per node. In other words, the time complexity of this approach is O(N).The space complexity is the maximum size of the queue. Since we store elements by level, it is a function of the width of the tree.In the case of a full tree (every node has two children except for the leaves) of height H, the maximum number of nodes in a level will occur at the last level (the leaves), being this 2^H. For this type of tree, H = log2(N).
In conclusion, the space complexity is O(N) too.Challenge
Try to solve .Given a binary tree, return the level order traversal of its nodes’ values. (ie, from left to right, level by level).
For this tree, the solution looks like: [[15], [10, 21], [5, 20, 23], [6]].Try to solve the problem .Solution
Always keep in mind the three basic ways to traverse a tree: pre-order, in-order and post-order. This problem is a pre-order tree traversal in disguise.
The only extra is that you want to put nodes that are at the same depth together. This can be easily achieved using a variable to keep track of the current level you are exploring.
Using a hash table, you can store every node you process at the right depth. Since we are exploring the left child first, the relative order of the elements at each level will be preserved.class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
if(!root)
return {};
//Here is where the magic happens
map<int, vector<int>> levels;
helper(root, levels);
//Here we create the solution in the expected data structure
vector<vector<int>> sol;
for(auto it = levels.begin(); it != levels.end(); ++it){
sol.push_back(it->second);
}
return sol;
}
void helper (TreeNode* n, map<int, vector<int>> &levels, int depth = 0){
levels[depth].push_back(n->val);
if(n->left)
helper(n->left, levels, depth + 1);
if(n->right)
helper(n->right, levels, depth + 1);
}
};
Complexity
We visit N nodes doing constant work per node (adding an element to a hash table has constant amortized time). Therefore, the time complexity is O(N).This implementation stores all the nodes in a hash table. Consequently, the space complexity is O(N).Given a binary tree, return the zigzag level order traversal of its nodes’ values. (ie, from left to right, then right to left for the next level and alternate between).
For this tree, the solution looks like [[8], [14, 4], [2, 5,12,19]].Give it a try .Solution
This problem might seem challenging at first, but you have the tools to solve it. One of the problems above is very similar to this one. I will give you a minute to find it.You are right. This is a level-order traversal. The only extra is that we have to process some levels from left to right and others from right to left. We can achieve this easily with two stacks, s1 and s2.
Imagine we have completed a level from left to right. The last element we pushed to the stack s1 will be the rightmost element at that level of the tree.class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
if(!root)
return {};
vector<vector<int>> sol;
stack<TreeNode*> s1;
stack<TreeNode*> s2;
s1.push(root);
while(!s1.empty() || !s2.empty()){
vector<int> partial;
while(!s1.empty()){
auto top = s1.top();
partial.emplace_back(top->val);
s1.pop();
if(top->left)
s2.push(top->left);
if(top->right)
s2.push(top->right);
}
if(!partial.empty()){
sol.push_back(partial);
partial.clear();
}
while(!s2.empty()){
auto top = s2.top();
partial.emplace_back(top->val);
s2.pop();
if(top->right)
s1.push(top->right);
if(top->left)
s1.push(top->left);
}
if(!partial.empty()){
sol.push_back(partial);
}
}
return sol;
}
};
Complexity
Both the time and space complexity are linear. Can you see why?Challenges
Here I have several variants of this problem for you to keep practicing:Given a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom.
The solution for this tree is [16, 19, 18, 13]. Looking from the right side, you cannot see the other nodes because the nodes in the solution “block their view”.Try coding it .Solution
This may look like a challenging problem, but you have the tools you need to solve it. You need to print the rightmost node of each level. Some of the previous problems look very similar o this one.You can solve this problem by modifying the level-order traversal and printing only the last node at each level.This is one possible implementation.class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
if(!root)
return {};
queue<TreeNode*> q;
q.push(root);
vector<int> sol;
while(!q.empty()){
int size = q.size();
for(int i = 1; i <= size; ++i){
auto top = q.front();
if(i == size){
sol.push_back(top->val);
}
if(top->left)
q.push(top->left);
if(top->right)
q.push(top->right);
q.pop();
}
}
return sol;
}
};
Complexity
The analysis is the same as the level-order traversal’s.Challenges
Here are two variants you can try:Given a binary tree, return the vertical order traversal of its nodes’ values.
For each node at position (X, Y), its left and right children respectively will be at positions (X-1, Y-1) and (X+1, Y-1).Running a vertical line from X = -infinity to X = +infinity, whenever the vertical line touches some nodes, we report the values of the nodes in order from top to bottom (decreasing Y coordinates).If two nodes have the same position, then the value of the node that is reported first is the value that is smaller.Return a list of non-empty reports in order of X coordinate. Every report will have a list of values of nodes.The solution for this example is [[4], [6], [12], [21, 14, 23], [20], [24], [25]].Code your solution .Solution
I bet you are already seeing a solution.Every time you move left or right, you can keep track of your X position. This is easy to implement recursively. Every time you move to the left node, the X position decreases by 1. If you move to the right, the X position increases by 1.Using a hash table, you can store all the nodes at a certain X position to build the solution.class Solution {
public:
vector<vector<int>> verticalTraversal(TreeNode* root) {
if(!root)
return {};
map<int, vector<pair<int, int>>> distances;
helper(root, 0, 0, distances);
vector<vector<int>> sol;
for(auto x : distances){
//Using a lambda function to ensure nodes appear in the expected order
sort(x.second.begin(),
x.second.end(),
[] (pair<int,int> &a, pair<int,int> &b) {
if(a.first==b.first)
return a.second<b.second;
else
return a.first<b.first;
}
);
vector<int> v;
for(auto y : x.second){
v.push_back(y.second);
}
sol.push_back(v);
}
return sol;
}
void helper(TreeNode* root, int hd, int c, map<int,vector<pair<int,int>>> &distances){
if(!root)
return;
distances[hd].push_back({c, root->val});
helper(root->left, hd-1, c+1, distances);
helper(root->right, hd+1, c+1, distances);
}
};
Complexity
We visit every node once and do constant work per node. However, at the last step where we need to order the nodes at each level. This brings the complexity up. It is not trivial to calculate this new complexity but it is clearly bounded by O(NLogN).Since we store all nodes in a hash table, the space complexity is O(N).Given a binary tree, return the vertical order traversal of its nodes’ values.
For each node at position (X, Y), its left and right children respectively will be at positions (X-1, Y-1) and (X+1, Y-1).Running a vertical line from X = -infinity to X = +infinity, whenever the vertical line touches some nodes, we report the values of the nodes in order from top to bottom (decreasing Y coordinates).If two nodes have the same position, then the value of the node that is reported first is the value that is smaller.Return an list of non-empty reports in order of X coordinate. Every report will have a list of values of nodes.The solution for this example is [[4], [6], [12], [21, 14, 23], [20], [24], [25]].Test your solution .Solution
Based on my explanation for the previous problem and your understanding of BFS, you will be able to write the iterative version of the algorithm that solves the problem.class Solution {
public:
vector<vector<int>> verticalTraversal(TreeNode* root) {
map<int, vector<pair<int,int>>> distances;
queue<pair<TreeNode*, int>> q;
int level = 0;
q.push({root, 0});
distances[0].push_back({level, root->val});
while(!q.empty()) {
const int size = q.size();
level++;
for(int i = 0; i < size; i++) {
auto node = q.front().first;
int sd = q.front().second; q.pop();
if(node->left) {
distances[sd-1].push_back({level, node->left->val});
q.push({node->left,sd-1});
}
if(node->right) {
distances[sd+1].push_back({level, node->right->val});
q.push({node->right,sd+1});
}
}
}
vector<vector<int>> sol;
for(auto it : distances) {
vector<int> partial;
sort(it.second.begin(), it.second.end());
for(int i= 0; i < it.second.size(); i++) {
partial.push_back(it.second[i].second);
}
sol.push_back(partial);
}
return sol;
}
};
Complexity
I will leave this as an exercise for you. If you need hints, check the previous problems.As I have said before, it is not about solving a million problems or spending a million hours. What is important is what you get out of each hour. We have also seen that from a seemingly simple problem we have been able to extract a lot.
“No problem is too small or too trivial if we can really do something about it.”
The aim of this entry is to make you think about each problem. We have seen 13 ways to traverse a tree. We have work on recursive and iterative implementations. You have become smarter by going through this article. As the next logical step, I invite you to read my article on .
If you found this article helpful, please share it. Chances are more people will find it useful too. You can help someone pass their exams, a job interview, or get them unblocked at work.
PS: I hope you found this useful. If so, like and share this article, visit my blog , and let's connect on .