Level Order (Breadth First Search) Traversal of Binary Tree

Introduction to level order traversal

In DFS traversal of a binary tree, we access nodes in three different orders :  preorder, postorder and inorder. Now there is another traversal that can access nodes in level by level order. This is called level order traversal or breadth-first search traversal. In the short form, we also call it BFS traversal.

A binary tree is organized in different levels where root node is at the topmost level (0th level). So the idea of level order traversal is: We start by processing root node, then process all nodes at the first level, second level, and so on. In other words, we explore all nodes at the current level before moving on to nodes at the next level.

BFS traversal of a binary tree example

The Recursive Approach of BFS Traversal

This is a brute force idea, where we move from top to bottommost level using a loop and process nodes at each level using recursion. The idea looks simple, but implementation would be little tricky.

The objective to discuss this approach is related to problem-solving. In recursive solution of a few tree problems, sometimes we pass extra parameters or use helper functions to generate the correct output.

Let’s think about the implementation of recursive BFS traversal:

  • The number of levels is equal to the height of tree. So we first calculate the height of tree h using function height(root).
  • We run a loop from l = 0 to h  -  1 and access each level in the tree. Inside the loop, we use function processCurrentLevel(root, l) to visit and process nodes at the current level l.

How we implement function processCurrentLevel(root, l)? Let’s think! We can split the problem into two parts:

  • We process all nodes at level l in the left subtree. For this, we call the same function with root->left and l  -  1 as a function parameter. Why? Because, if current level is l distance from the root, then it would be l - 1 distance away from root->left.
  • We process all nodes at level l in the right subtree. For this, we call the same function with root->right and l  -  1 as a function parameter. Why? Because, if current level is l distance from the root, then it would be l  - 1 distance away from root->right.
  • When l == 0 during recursive calls, we arrived at a node which is distance l from root. So we process this node. In such a way, we can access all nodes at the level l recursively.

Pseudocode Implementation

class TreeNode
{
    int data
    TreeNode left 
    TreeNode right
}
 
void recursiveLevelOrder(TreeNode root)
{
    int h = height(root)
    for (int l = 0; l < h; l = l + 1)
        processCurrentLevel(root, l)
}

Processing nodes at the level l

void processCurrentLevel(TreeNode root, int l)
{
    if (root == NULL)
        return
    if (l == 0)
        process(root->data)
    else if (l > 0)
    {
        processCurrentLevel(root->left, l - 1)
        processCurrentLevel(root->right, l - 1)
    }
}

Calculating the height of tree

int height(TreeNode root)
{
    if (root == NULL)
        return 0
    else
    {
        int leftHeight = height(root->left)
        int rightHeight = height(root->right)
        
        if (leftHeight > rightHeight)
            return (leftHeight + 1)
        else 
            return(rightHeight + 1)
    }
}

Time and space complexity analysis

For each recursive call processCurrentLevel(), we are going l  - 1 level down in the left and right subtree. In other words, we are accessing each node from level 0 to level l and checking if l == 0 or not. So one idea is clear: Total number of operations depends on the height of the tree. Think!

The worst-case scenario would be the skewed tree where one node is present at each level. In this case, processCurrentLevel() takes O(n) time for the last level, O(n-1) time for the second last level, and so on. Here n is number of nodes in the tree. Time complexity = O(n) + O(n - 1) + .. + O(1) = O(n + (n-1) + ... + 1) = O(n^2). Critical question: What would be the best case analysis? Think!

Space complexity = O(n) in the worst case. For a skewed tree, processCurrentLevel() will use O(n) space for the call stack. For a balanced tree, the call stack uses O(log n) space. Note: The size of call stack is equal to the height of the tree.

An efficient approach: BFS traversal using queue

Now critical questions are: Can we optimize the time complexity of BFS traversal? Can we traverse tree level by level in O(n) time complexity? Think!

Let's observe the order of nodes in level order traversal.

  • We first process root node at level 0, and then we process the left and right child at level 1 (assuming left to right order).
  • Similarly, at the second level, we first process children of the left child of root then process children of right child. This process goes on for all levels in the tree.

So one idea is clear: At any given level, node which will be processed first, children of that node will be processed first at the next level. This is First In First Out Order (FIFO Order) of processing nodes! So we can use queue data structure to simulate the BFS traversal. Think!

Implementation steps: BFS traversal using queue

  1. We take an empty queue treeQueue and initialize it with root node.
  2. Now we run a loop till treeQueue is not empty.
  3. Inside the loop, we declare a currNode to track current node during the traversal.
  4. We start loop by removing front node from treeQueue and assigning it with currNode. We process the currNode and insert left and right child into treeQueue.

    TreeNode currNode = treeQueue.dequeue()
    process (currNode->data)
  5. If left child of currNode is not NULL, we insert left child into treeQueue.

    if (currNode->left != NULL)
      treeQueue.enqueue(currNode->left)
  6. If right child of currNode is not NULL, we insert right child into treeQueue.

    if (currNode->right != NULL)
      treeQueue.enqueue(currNode->right)
  7. After processing the rightmost node at the last level, there are no nodes inside the queue to process further. So we come out of the loop, and our level order traversal is done at this point.

Pseudocode  of BFS traversal using queue

void iterativeLevelOrder(Treenode root)
{
    if (root == NULL)  
        return
    Queue<TreeNode> treeQueue
    treeQueue.enqueue(root)
    while (treeQueue.empty() == false)
    {
        TreeNode currNode = treeQueue.dequeue()
        process(currNode->data) 
        
        if (currNode->left != NULL)
            treeQueue.enqueue(currNode->left)
 
        if (currNode->right != NULL)
            treeQueue.enqueue(currNode->right)
    }
}

Time complexity analysis of the BFS traversal

  • Suppose n number of nodes are given in the input. 
  • The time complexity of enqueue and dequeue operations = O(1)
  • We are doing two queue operations for each node inside the loop: Inserting once into the queue and deleting once from the queue. So total queue operations = 2n.
  • Overall time complexity = Total queue operations * Time complexity of each queue operation = 2n * O(1) = O(n)

The space complexity analysis of the BFS traversal

Space complexity is equal to the queue size. We process nodes level by level, so max queue size depends on the level with maximum number of nodes or max-width of binary tree. If maximum width of binary tree is w, then space complexity = O(w). The idea is simple: w depends on the structure of given binary tree. How? Let’s think!

Worst case: When tree is balanced

When tree is balanced, the last level will have maximum width or maximum number of nodes, which will be 2^h (where h is the height of the tree). For balanced tree, h = logn and required size of the queue = O(2^h) = O(2 ^ (log n)) = O(n). Space complexity = O(n)

Best case: When tree is skewed

In such case, every level will have maximum of one node, and thus at any point, there will be at most one node in the queue. So required size of the queue = O(1). Space complexity = O(1)

BFS vs DFS traversal of binary tree

  • Both traversals require O(n) time as they visit every node exactly once.
  • Depth-first traversal starts from the root, goes to the depth as far as possible, and then backtracks from there. In other words, it visits nodes from bottom of the tree. But in breadth-first search, we explore nodes level by level i.e. in order of their distance from the root node. So if our problem is to search for something closer to the root, we would prefer BFS. And if we need to search for something in the depth of tree or node closer to leaf, we would prefer DFS.
  • In BFS traversal, we use queue data structure to store nodes of different levels. But in DFS traversal, we use the stack (If recursive, system use call stack) to store all ancestors of a node.
  • The memory taken by both BFS and DFS depends on the structure of tree. Extra space required for BFS Traversal is O(w), but additional space needed for DFS Traversal is O(h). Here w = maximum width of binary tree and h = maximum height of binary tree. In the worst case, both require O(n) extra space, but worst cases occur for different types of trees. For example, space needed for BFS Traversal is likely to be more when a tree is more balanced, and extra space for DFS Traversal is likely to be more when a tree is less balanced.
  • Sometimes, when node order is not required in problem-solving, we can use both BFS and DFS traversals. But in some cases, such things are not possible. We need to identify the use case of traversal to solve the problems efficiently.

Problems to practice using BFS traversal

  • Level order traversal in spiral form
  • Reverse Level Order Traversal
  • Left View of Binary Tree
  • Maximum width of a binary tree
  • Min Depth of Binary Tree
  • Level with the maximum number of nodes
  • Count the number of nodes at a given level
  • Convert a binary tree into a mirror tree

Enjoy learning, Enjoy coding, Enjoy algorithms!

Share feedback with us

More blogs to explore

Our weekly newsletter

Subscribe to get weekly content on data structure and algorithms, machine learning, system design and oops.

© 2022 Code Algorithms Pvt. Ltd.

All rights reserved.