Interview Kickstart has enabled over 21000 engineers to uplevel.
A tree is a non-linear data structure, which consists of nodes and edges that represent a hierarchical structure. It is a connected graph with no cycles. A tree with “n” nodes should always contain “n-1” edges.
Tree traversal is one of the most basic requirements for dealing with different types of tree
problems. Tree traversal refers to the process of visiting each node of the tree and updating the node if required.
Learning about trees is a must for software engineers as trees are frequently used in computer science fields such as XML parsers, databases, domain name explorers, and many other fields. Problems on trees are often asked in technical interviews and coding rounds of FAANG companies and other tier-1 tech companies.
To help you in your prep, we’ll cover the following topics in this article:
The depth-first search algorithm is used for traversing or searching a tree or any other graph data structure. The key idea behind DFS is backtracking. Let’s understand with the help of an example.
A family tree is an example of a tree representing parent-child relationships. Let’s say we have been given a family tree, and we need to return the number of children in the family.
Definitions:
Here’s what we see in the image:
Our problem statement is counting the number of leaf nodes in the above tree. Let's say we start with the root node, “Michael,” in our case.
So the key idea is to visit the child of the current node until we reach a leaf node, then we backtrack and repeat the process for the next child node, which is not visited yet. This is depth-first traversal. Always start from an arbitrary node and go as deep as possible along each branch one after another before backtracking.
An n-ary tree is a tree that allows us to have “n” number of children of a particular node. Hence the name n-ary. Let’s see how to traverse this tree using DFS.
The algorithm starts at the root node, marks it as visited, explores any adjacent unvisited node, and continues this process until no unvisited nodes are left. Then, it backtracks to the previous nodes and checks again for any unvisited node. This process is continued until no unvisited node is left in the tree.
For the recursive DFS traversal on a tree, we don’t need to maintain track of visited nodes as a tree does not contain any cycle. We can just keep track of the parent node of the current node so that we don’t visit the node’s parent as it is already visited. This approach will be discussed in detail in the next section.
The DFS traversal can be implemented either recursively or non-recursively. The recursive implementation uses the call stack, while the iterative traversal uses a user-defined stack.
Let T denote the tree. The following pseudocode describes the recursive implementation of DFS traversal on a tree.
DFS(T, u)
visited[u] = true
for each v ∈ T.Adj[u]
if visited[v] == false
DFS(T,v)
init() {
For each v ∈ T
visited[v] = false
//u denotes the root node
//if root node is not defined, we can select any
//arbitrary node as root node
DFS(T, u)
}
We know that in a tree data structure, we cannot have cycles. This idea will be used here.
We keep track of the parent node and the current node while calling the DFS function. We always want to go down the depth until all of the current node’s children are visited and then backtrack to its parent. We never want to go to the parent node at an intermediate step when some children remain unvisited for the current node.
DFS(T, u , parent)
for each v ∈ T.Adj[u]
if v != parent
// for node v its parent is u and this information
// is passed in dfs
DFS(T, v , u)
init() {
//u denotes the root node
//if root node is not defined, we can select any
//arbitrary node as root node
//since root cannot have a parent so we pass -1 as its parent
DFS(T, u , -1)
}
For example, consider the tree below. Suppose we select 1 as the tree’s root node and call DFS(1); the figures below simulate the order in which the nodes are visited.
First, the DFS(1) is called. We pick any one of the adjacent unvisited nodes of node 1. Suppose we pick node 2 and hence call DFS(2). The DFS(2) calls DFS(3), which in turn calls DFS(4). Once we reach node 4, there is no adjacent unvisited node. Hence, we backtrack to node 3. Here too, there is no adjacent unvisited node, so we backtrack again to node 2, then node 1.
Once we reach node 1, we pick node 5 and explore this branch as deeply as possible before backtracking. Similarly, we continue this process until no unvisited node is left in the tree.
Hence, one of the DFS traversals is:
Note: The order of visiting adjacent nodes does not matter in the case of DFS traversal. There can be many possible DFS traversals of the above tree, such as 1,5,6,7,8,9,10,2,3,4 or 1,5,6,7,8,2,3,4,9,10.
Tree traversal is itself a form of graph traversal. The depth-first search traversal on a graph is very similar to the depth-first search on a tree. The main difference between the two traversals lies in the fact that the tree is itself a special type of connected graph with no cycles.
Hence, in the case of DFS traversal on a graph, we require additional memory to store the visited nodes so that we don’t visit them again. The graph can have cycles and may not be connected, so we need to keep track of visited nodes.
Based on the order of visiting the nodes, DFS traversal on a binary tree can be divided into the following three types: Inorder traversal, preorder traversal, and postorder traversal.
Example:
#include <iostream>
using namespace std;
//each node of the binary tree has a data , pointer to the left and
//the right child
struct Node {
int data;
struct Node *left, *right;
Node(int data)
{
this->data = data;
left = right = NULL;
}
};
void inorderTraverse(struct Node* node)
{
if (node == NULL)
return;
/* first recur on left child */
inorderTraverse(node->left);
/* then access the data of current node */
cout << node->data << " ";
/* now recur on right child */
inorderTraverse(node->right);
}
int main() {
struct Node* root = new Node(1);
root->left = new Node(2);
root->left->left = new Node(3);
root->left->right = new Node(4);
root->right = new Node(5);
root->right->right = new Node(8);
root->right->left = new Node(6);
root->right->left->left = new Node(7);
inorderTraverse(root);
return 0;
}
Output:
3 2 4 1 7 6 5 8
In a binary search tree, where for each node, the nodes in its left subtree are less than it, and the nodes in the right subtree are bigger than it, the inorder traversal allows us to access the elements in sorted order.
Example:
#include <iostream>
using namespace std;
//each node of the binary tree has a data , pointer to the left and
//the right child
struct Node {
int data;
struct Node *left, *right;
Node(int data)
{
this->data = data;
left = right = NULL;
}
};
void preorderTraverse(struct Node* node)
{
if (node == NULL)
return;
/* access the data of current node */
cout << node->data << " ";
/* then recur on left child */
preorderTraverse(node->left);
/* now recur on right child */
preorderTraverse(node->right);
}
int main() {
struct Node* root = new Node(1);
root->left = new Node(2);
root->left->left = new Node(3);
root->left->right = new Node(4);
root->right = new Node(5);
root->right->right = new Node(8);
root->right->left = new Node(6);
root->right->left->left = new Node(7);
preorderTraverse(root);
return 0;
}
Output:
1 2 3 4 5 6 7 8
With the help of the preorder traversal of a binary search tree, we can reconstruct the tree back. Preorder traversal is used to get the prefix expression of an expression tree.
Example:
#include <iostream>
using namespace std;
//each node of the binary tree has a data , pointer to the left and
//the right child
struct Node {
int data;
struct Node *left, *right;
Node(int data)
{
this->data = data;
left = right = NULL;
}
};
void postorderTraverse(struct Node* node)
{
if (node == NULL)
return;
/* first recur on left child */
postorderTraverse(node->left);
/* now recur on right child */
postorderTraverse(node->right);
/* then access the data of current node */
cout << node->data << " ";
}
int main() {
struct Node* root = new Node(1);
root->left = new Node(2);
root->left->left = new Node(3);
root->left->right = new Node(4);
root->right = new Node(5);
root->right->right = new Node(8);
root->right->left = new Node(6);
root->right->left->left = new Node(7);
postorderTraverse(root);
return 0;
}
Output:
3 4 2 7 6 8 5 1
The time complexity can be calculated from the number of nodes visited in the traversal of the tree. First, we analyze the inorder traversal function on a binary tree. By breaking down our function, we can see that for each time the function is called, these four steps occur:
For each call of the function, a maximum of four operations are performed, and this function can be called only one time for each node of the tree.
If we consider the number of nodes in the tree to be n, approximately 4*n operations will be performed. This bound remains the same for the inorder, preorder, and postorder traversal. Hence, the time complexity in terms of Big-O is O(n) for each of the traversal techniques.
Now, let us consider an n-ary tree. We visit all the nodes during the DFS traversal, and hence, the time complexity is O(N). We recursively call DFF for each node and mark them as visited once and do not revisit it.
In the case of DFS traversal of a rooted tree using a visited array, the space complexity is O(n).
In the case of DFS traversal of a rooted tree without using a visited array, if we don’t consider the stack for the function calls, then the space complexity is O(1). But if we consider the stack, then the space complexity will be O(h), where h is the tree’s height.
Let n denote the number of nodes in the tree. The best-case height of a binary tree is log(n); hence the best-case space complexity will be O(log(n)).
The height of a skewed binary tree will be O(n). Hence the worst-case space complexity will be O(n) because the depth of the recursion will be the same as the height of the tree. So stack memory consumed is equal to the height of the tree.
Strength:
Weaknesses:
Check out the Learn and Problems pages to continue learning about Tree, Graphs, and other data structure and algorithm concepts.
Question 1: In which cases does DFS traversal of the tree work better than the BFS traversal and vice versa?
Answer: Although DFS and BFS traversals have the same worst-case scenario time complexity, there may be some cases in which it is preferable to use one over the other.
Question 2: What are the applications of depth-first search?
Nailing tech interviews at FAANG and Tier-1 tech companies can be challenging even for seasoned software engineers. It requires a deep understanding of data structure and algorithms as well as systems design.
If you’re looking for guidance and help to nail these questions and more, sign up for our free webinar. As pioneers in the field of technical interview prep, we have trained thousands of software engineers to crack the toughest coding interviews and land jobs at their dream companies, such as Google, Facebook, Apple, Netflix, Amazon, and more!
----------
Article contributed by Problem Setters Official
Attend our webinar on
"How to nail your next tech interview" and learn