Understanding algorithms is important for every software developer to have. Algorithms are a common part of any coding interview, whether you are solving problems in JavaScript, Java, or Python. You can utilize algorithms to ace coding interviews, land a job, or solve peculiar problems at work.
Today’s tutorial would focus on algorithms for traversing elements in a tree. Tree traversal looks different in different programming languages; we will be using JavaScript. We start by refreshing our knowledge about trees. Next, we will learn some useful algorithms and look at some common interview questions.
This guide at a glance:
In this curated learning path, you’ll cover everything from Data Structures to System Design.
Ace the JavaScript Coding Interview
In computer science, trees are non-linear data structures represented as a collection of nodes connected by edges. Each node stores data of any type, and all nodes in a tree are the same data type.
Tree data structures are typically used to store information while preserving its hierarchy, for implementing quick searches, networking, and inheritance.
The basic components of a tree includes:
Not all trees are binary — many real-world trees (like DOM structures or file systems) have multiple children per node. Traversing these general trees requires slightly different logic.
Here’s a basic recursive traversal in JavaScript:
function traverseGeneralTree(root) {
if (!root) return;
console.log(root.value); // Pre-order position
for (const child of root.children) {
traverseGeneralTree(child);
}
}
To make it postorder, simply move the console.log after traversing the children.
Use a queue to traverse the tree level by level:
function breadthFirstGeneral(root) {
const queue = [root];
const result = [];
while (queue.length) {
const node = queue.shift();
result.push(node.value);
if (node.children) {
for (const c of node.children) queue.push(c);
}
}
return result;
}
Mastering traversal in general (N-ary) trees broadens your problem-solving ability beyond binary trees. It’s directly applicable to:
These traversal patterns are building blocks for many real-world algorithms.
A traversed tree is one in which every node on the tree has been visited. Traversal is a process involving iterating over all nodes in a certain manner.
Unlike linear data structures (such as Arrays, Linked Lists, or Queues) which have only one logical way to traverse them with recursion, trees can be traversed in different ways. Generally there are 2 iterative ways for traversing trees:
Depth-first traversal involves traversing a tree from top to bottom. They are implemented in a FILO manner (First In Last Out), like the stack data structure. The left sub-trees are traversed first, then the right sub-trees.
This is commonly seen when traversing a binary tree. Binary Trees are trees where each node has at most 2 child nodes. In the tree below, the node with the value 1 would be the first to be pushed into a stack, followed by the nodes with values 2, 3, and 4, then it goes back to the top to the node with the value 5 down.
When the end of the tree is reached, the values are popped off the stack back into the tree.
There are 3 types of Depth-first traversal:
In in-order traversal, we traverse the left child and its sub-tree(s), then we visit the root and then traverse the right child and its sub-tree(s). It takes a “left-root-right” order.
Before we take a look at a code sample for this algorithm, let’s try to outline the steps involved:
inOrderprint(currentNode) {//if the currentNode IS NOT EQUAL to nullif (currentNode!==null) {//make recursive call to the left subtreethis.inOrderPrint(currentNode.leftChild);//print the value of the currentNodeconsole.log(currentNode.val);//make recursive call to the right subtreethis.inOrderPrint(currentNode.rightChild);}}
class Node {constructor(value) {this.val = value;this.leftChild = null;this.rightChild = null;}}class BinarySearchTree {constructor(rootValue) {this.root = new Node(rootValue);}insert(currentNode, newValue) {if (currentNode === null) {currentNode = new Node(newValue);} else if (newValue < currentNode.val) {currentNode.leftChild = this.insert(currentNode.leftChild, newValue);} else {currentNode.rightChild = this.insert(currentNode.rightChild, newValue);}return currentNode;}insertBST(newValue) {if(this.root==null){this.root=new Node(newValue);return;}this.insert(this.root, newValue);}preOrderPrint(currentNode) {if (currentNode!==null) {console.log(currentNode.val);this.preOrderPrint(currentNode.leftChild);this.preOrderPrint(currentNode.rightChild);}}inOrderPrint(currentNode) {if (currentNode!==null) {this.inOrderPrint(currentNode.leftChild);console.log(currentNode.val);this.inOrderPrint(currentNode.rightChild);}}}var BST = new BinarySearchTree(6);console.log("The root val for BST : ", BST.root.val)BST.insertBST(4);BST.insertBST(9);BST.insertBST(5);BST.insertBST(2);BST.insertBST(8);BST.insertBST(12);BST.inOrderPrint(BST.root);
In our implementation, we create an object named BST of the BinarySearchTree class and insert some values into it. We will then pass the object’s root to the inOrderPrint() function as a starting point to the traversal.
If the node passed into the function is not null, this function calls inOrderPrint() recursively on the left child first, i.e., the left subtree, and then prints the value at the node. Then it calls the inOrderPrint() recursively on the right child, i.e., the right subtree.
The node will print its own value once all the subsequent calls to the left subtree have been executed.
When the recursive calls will hit the null nodes, they will return from the function without doing anything.
Pre-Order traversal reads the nodes in a tree in a level-by-level order, left-child before right-child. The elements are read in a “root-left-right” order.
preOrderPrint(currentNode) {//if the currentNode IS NOT EQUAL to nullif (currentNode!==null) {//print its valueconsole.log(currentNode.val);//make recursive call to the left subtreethis.preOrderPrint(currentNode.leftChild);//make recursive call to the right subtreethis.preOrderPrint(currentNode.rightChild);}//if the currentNode IS EQUAL to null, then//we simply return}
class Node {constructor(value) {this.val = value;this.leftChild = null;this.rightChild = null;}}class BinarySearchTree {constructor(rootValue) {this.root = new Node(rootValue);}insert(currentNode, newValue) {if (currentNode === null) {currentNode = new Node(newValue);} else if (newValue < currentNode.val) {currentNode.leftChild = this.insert(currentNode.leftChild, newValue);} else {currentNode.rightChild = this.insert(currentNode.rightChild, newValue);}return currentNode;}insertBST(newValue) {if(this.root==null){this.root=new Node(newValue);return;}this.insert(this.root, newValue);}preOrderPrint(currentNode) {if (currentNode!==null) {console.log(currentNode.val);this.preOrderPrint(currentNode.leftChild);this.preOrderPrint(currentNode.rightChild);}}}var BST = new BinarySearchTree(6);console.log("The root val for BST : ", BST.root.val)BST.insertBST(4);BST.insertBST(9);BST.insertBST(5);BST.insertBST(2);BST.insertBST(8);BST.insertBST(12);BST.insertBST(10);BST.insertBST(14);BST.preOrderPrint(BST.root);
The algorithm for pre-order traversal, starting from the root node, is as follows:
currentNode and display the datacurrentNode recursivelycurrentNode recursivelyNow let’s review the code for pre-order traversal.
We create a new object of the BinarySearchTree class and insert values into it. We then pass the root node to the preOrderPrint() function.
This function prints the value of the node, then it is recursively invoked for the left child. When the nodes in the left sub-tree have been visited, it is recursively invoked for the right child and its sub-trees.
When the recursive calls reach the null nodes, they will return from the function without doing anything.
The last strategy for traversing a tree is the Post-Order traversal. Here, we traverse the left sub-tree, then the right subtree before we visit the root node. It takes a “left-right-root” order.
postOrderPrint(currentNode) {//if the currentNode IS NOT EQUAL to nullif (currentNode!==null) {//make recursive call to the left subtreethis.postOrderPrint(currentNode.leftChild);//make recursive call to the right subtreethis.postOrderPrint(currentNode.rightChild);//print its valueconsole.log(currentNode.val);}}
class Node {constructor(value) {this.val = value;this.leftChild = null;this.rightChild = null;}}class BinarySearchTree {constructor(rootValue) {this.root = new Node(rootValue);}insert(currentNode, newValue) {if (currentNode === null) {currentNode = new Node(newValue);} else if (newValue < currentNode.val) {currentNode.leftChild = this.insert(currentNode.leftChild, newValue);} else {currentNode.rightChild = this.insert(currentNode.rightChild, newValue);}return currentNode;}insertBST(newValue) {if(this.root==null){this.root=new Node(newValue);return;}this.insert(this.root, newValue);}preOrderPrint(currentNode) {if (currentNode !== null) {console.log(currentNode.val);this.preOrderPrint(currentNode.leftChild);this.preOrderPrint(currentNode.rightChild);}}inOrderPrint(currentNode) {if (currentNode !== null) {this.inOrderPrint(currentNode.leftChild);console.log(currentNode.val);this.inOrderPrint(currentNode.rightChild);}}postOrderPrint(currentNode) {if (currentNode !== null) {this.postOrderPrint(currentNode.leftChild);this.postOrderPrint(currentNode.rightChild);console.log(currentNode.val);}}}var BST = new BinarySearchTree(6);console.log("The root val for BST : ", BST.root.val)BST.insertBST(4);BST.insertBST(9);BST.insertBST(5);BST.insertBST(2);BST.insertBST(8);BST.insertBST(12);BST.postOrderPrint(BST.root);
The steps involved for post-order traversal are as follows:
currentNode recursivelycurrentNode recursivelycurrentNode and print its valueAgain, we create an object named BST and insert values into it. We will then pass the root node to the postOrderPrint() function as a starting point to the traversal.
If the node passed into the function is not null, this function calls postOrderPrint() on the left child first and then on the right child. Lastly, it prints the value of the currentNode.
The value of a parent node or the root node will only be printed once all the calls have been executed.
While recursion is elegant, JavaScript’s call stack is limited, and for deep or skewed trees, recursion can lead to stack overflow. To handle such cases efficiently, you can use an explicit stack to perform preorder, inorder, or postorder traversals iteratively.
function iterativeInOrder(root) {
const stack = [];
let current = root;
const result = [];
while (current !== null || stack.length) {
while (current !== null) {
stack.push(current);
current = current.left;
}
current = stack.pop();
result.push(current.value);
current = current.right;
}
return result;
}
Using iterative approaches helps your implementation:
These patterns are especially useful in interview problems or production systems handling large or unpredictable tree structures.
If you want to push optimization further, you can traverse a binary tree in in-order without recursion and without extra memory (no stack). This is called Morris traversal. The idea is to temporarily “thread” null right pointers to their in-order successor, then revert those changes as you go.
Outline:
Initialize current = root.
If current.left is null, visit current and move to current.right.
Else, find the predecessor (rightmost node in current.left subtree).
If predecessor.right is null, set predecessor.right = current (thread), then current = current.left.
Else predecessor.right == current (thread exists), revert it (set predecessor.right = null), visit current, then current = current.right.
This gives you in-order traversal with O(1) extra space (apart from variables), though it mutates pointers temporarily.
With level-order traversal, trees are traversed level-wise. This means that we visit every node on a level from left to right before going to a lower level. All child nodes are traversed before the next level of grandchildren nodes.
This is similar to breadth-first search from graph algorithms.
traverseBFS() {if (!this.root) return;this.queue = [];this.queue.push(this.root);this.output = [];while (this.queue.length) {const node = this.queue.shift();if (node.left) {this.queue.push(node.left);}if (node.right) {this.queue.push(node.right);}this.output.push(node.data);}return this.output;}
In level-order traversal, we utilize a queue to store a reference to the node(s) we want to visit. This ensures that we can traverse the tree left-to-right on the same level.
When traversing the tree above, the root node would be the first to be placed in a queue, then the node with the value 12, then 18, 14, 16 and 20. The output would be:
[10,12,18,14,16,20]
Knowing how to implement tree traversal in JavaScript is foundational, but knowing when and how well it works differentiates a good engineer.
Time & Space Complexity: All standard traversals run in O(n) time (visit each node once). Recursive DFS uses O(h) auxiliary space (call stack depth), where h is tree height. Iterative stack methods also use O(h). Morris uses O(1) extra space (aside from output).
Skewed Trees and Depth: If your tree is very unbalanced (degenerates to a list), recursion depth may lead to stack overflow. Prefer iterative or Morris in such cases.
Handling nulls / missing children: Always check for null children before recursing or enqueueing.
Mutable trees: If your traversal mutates the tree (as in Morris), be careful to revert changes so future operations aren’t broken.
Use in algorithms: Traversals power tasks like validating BSTs (inorder must be sorted), serializing/deserializing trees, evaluating expression trees, searching hierarchies like DOM, or algorithms that depend on postorder calculations (e.g. computing subtree sizes).
By combining multiple techniques and understanding complexity and limitations, your toolkit for how to implement Tree Traversal in JavaScript becomes much stronger.
That concludes what we need to know about tree traversal algorithms. The following are common coding interview questions about tree traversals that you should study:
You can continue learning about algorithms in JavaScript by visiting Educative’s learning path Ace the JavaScript Coding Interview. JavaScript continues to This path will take you through all that you need to know to crack your JavaScript interviews with confidence.
Happy learning!