Tutorial

Tree Traversal via JavaScript

Published on March 2, 2020
Default avatar

By Joshua Hall

Tree Traversal via JavaScript

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.

Trees are basically just fancy linked lists and creating and deleting nodes on a tree is incredibly simple. Searching on the other hand is a bit more tricky when they’re unsorted, so we’re going to look into a few different ways to handle searching through an entire tree.

Prerequisites

You will need a basic understanding of what trees are and how they work. Our particular example with be utilizing a Binary Search Tree, but these are more of techniques and patterns than exact implementations and can be easily adapted for any type of tree.

Concepts

With binary search trees we could use the same system to create a new node as to find one. Standard trees, like your file system, don’t follow any particular rules and force us to look at every item through a tree or subtree to find what we want. This is why running a search for a particular file can take so long.

There aren’t many ways to optimize past O(n) but there are two main “philosophies” for searching through the entire tree, either by searching breadth-first (horizontally between siblings) or depth-first (vertically between parents and children).

Diagram: anatomy of a tree

Tree

Since a binary search tree is the easiest to set up, we can just throw together a quick one that can only add nodes.

class Node {
  constructor(val) {
    this.val = val;
    this.right = null;
    this.left = null;
  };
};

class BST {
  constructor() {
    this.root = null;
  };
  create(val) {
    const newNode = new Node(val);
    if (!this.root) {
      this.root = newNode;
      return this;
    };
    let current = this.root;

    const addSide = side => {
      if (!current[side]) {
        current[side] = newNode;
        return this;
      };
      current = current[side];
    };

    while (true) {
      if (val === current.val) return this;
      if (val < current.val) addSide('left');
      else addSide('right');
    };
  };
};

const tree = new BST();
tree.create(20);
tree.create(14);
tree.create(57);
tree.create(9);
tree.create(19);
tree.create(31);
tree.create(62);
tree.create(3);
tree.create(11);
tree.create(72);

Breadth-first search is characterized by the fact that it focuses on every item, from left to right, on every level before moving to the next.

There are three main parts to this, the current node, our list of visited nodes, and a basic queue for keeping track of which nodes we need to look at (we’ll just use an array since it’ll never get very long).

Binary Search Tree Diagram

Let’s work through how it would look on this tree.

Whatever is our current, we’ll push its children (from left to right) into our queue, so it’ll look like [20, 14, 57]. Then we’ll change current to the next item in the queue and add its left and right children to the end of the queue, [14, 57, 9, 19].

Our current item can now be removed and added to visited while we move on to the next item, look for its children, and add them to the queue. This will repeat until our queue is empty and every value is in visited.

BFS() {
  let visited = [],
      queue = [],
      current = this.root;

  queue.push(current);
  while (queue.length) {
    current = queue.shift();
    visited.push(current.val);

    if (current.left) queue.push(current.left);
    if (current.right) queue.push(current.right);
  };

    return visited;
}

console.log(tree.BFS()); //[ 20, 14, 57, 9, 19, 31, 62, 3, 11, 72 ]

Depth-first searches are more concerned with completing a traversal down the whole side of the tree to the leafs than completing every level.

There are three main ways to handle this, preOrder, postOrder, and inOrder but they’re just very slight modifications of each other to change the output order. Better yet, we don’t even need to worry about a queue anymore.

Starting from the root, we’ll use a short recursive function to log our node before moving down to the left as far as possible, logging its path along the way. When the left side is done it’ll start working on the remaining right values until the whole tree has been logged. By the end visited should look like [24, 14, 9, 3, 11, 19, ...].

preOrder() {
  let visited = [],
      current = this.root;

  let traverse = node => {
    visited.push(node.val);
    if (node.left) traverse(node.left);
    if (node.right) traverse(node.right);
  };

  traverse(current);
  return visited;
}

console.log(tree.preOrder()); // [ 20, 14, 9, 3, 11, 19, 57, 31, 62, 72 ]

As you may have guessed, postOrder is the opposite of preOrder, we’re still working vertically but instead of moving from the root to leafs, we’ll search from the bottom to top.

We’ll start from the bottommost left node and log it and its siblings before moving up to their parent. The first half of visited should look like this, [3, 11, 9, 19, 14, ...], as it works it bubbles up the tree.

We can easily achieve this by pushing our nodes into visited after both traversals are completed.

postOrder() {
  let visited = [],
      current = this.root;

  let traverse = node => {
    if (node.left) traverse(node.left);
    if (node.right) traverse(node.right);
    visited.push(node.val);
  };

  traverse(current);
  return visited;
}

console.log(tree.postOrder()); // [ 3, 11, 9, 19, 14, 31, 72, 62, 57, 20 ]

Similar to postOrder, preOrder visits works from the bottom up but it just visits the parent before any siblings.

Instead of the beginning or end we can push onto our list after we traverse the left side and before the right. Our results will look something like this, [3, 9, 11, 14, 19, 20, ...].

inOrder() {
  let visited = [],
      current = this.root;

  let traverse = node => {
    if (node.left) traverse(node.left);
    visited.push(node.val);
    if (node.right) traverse(node.right);
  };

  traverse(current);
  return visited;
}

console.log(tree.inOrder()); // [ 3, 9, 11, 14, 19, 20, 31, 57, 62, 72 ]

Closing Thoughts

Of course all of these algorithms will be O(n) since the point is to look at every node, there isn’t much room for cutting corners or clever tricks.

Keep in mind that these aren’t exact implementations that need to be memorized but general patterns for solving problems and building more valuable algorithms. Once you understand the underlining concepts, they can easily be adapted for any language or framework.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Joshua Hall

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
1 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

It would be nice if these examples had an explanation of why one might be preferred over another.

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel