Data structures in JavaScript: Binary Search Trees
By: Software Musings
Tags: Web Development, Javascript Development, JavaScript, Software Engineering, Software Development

Basic Terminology

There are some basic terms that should be understood before working with BSTs.

Root: The very top node of a tree.

The very top node of a tree. Child: A node that is directly connected to a node above. In other words, a node that is connected to a node when when moving away from the root.

A node that is directly connected to a node above. In other words, a node that is connected to a node when when moving away from the root. Parent: A node that is connected to a node moving in towards the root.

A node that is connected to a node moving in towards the root. Siblings: A group of nodes that have the same parent.

A group of nodes that have the same parent. Leaf: A node that has no children

A node that has no children Edge: The connection between two nodes is called an edge.

Brief Introduction

Let’s discuss the technical definition of a BST.

A binary search tree contains nodes where each node can have a maximum of two children nodes. In the left subtree of a node, all values must be less than the node’s value. In the right subtree of a node, all values must be greater than the node value.

In addition, the left subtree and the right subtree of each node must also meet these conditions.

An example of a BST is shown in the diagram below. Go through each node slowly and ensure that you can verify that each of the conditions of BST are met.

Applications of Binary Search Trees

The major advantage of a BST relies on its ability to efficiently search, insert and delete data. Compared to arrays or linked list where searching, inserting or deleting a specific piece of data can cost O(n), BST offers us a way to perform these operations in O(logn) time which is more efficient.

One caveat to understand here is that the BSTs can also give worst case performance of O(n) if the tree is highly unbalanced (aka looks like a linked list). In real life, we use BSTs with self balancing functionality which guarantees that we can keep the operations efficient.

Due to this efficient performance , some applications of BSTs include:

Implementation of indexing in database systems

Creation of structures like a dictionary (efficient storage and retrieval).

Implementation of various efficient searching algorithms

Setting up our classes

In order to create our BST data structure, we will need to create two classes.

Node class: node class will be used to create new nodes. Each node can contain a left, right or a value property. BST class: BST class will encapsulate our data structure. We will create and store reference to our root node in this class and add all our methods here as well.

class Node {

constructor(value) {

this.value = value;

this.left = null;

this.right = null;

}

} class BinarySearchTree {

constructor() {

this.root = null;

}

}

Searching a BST

Explanation

Searching is fairly simple to understand in a BST due to its properties. To begin, we have to compare the value we are looking for with the root node. If the target value is less than the root node, we can search the left subtree from the root node otherwise we search the right subtree.

We can end our search through the tree if:

We have found our target value in one of the node

Or we have run out of nodes to search (null node)

Notice that at each node, we are reducing our area of search by half. When we decide to search left or right subtrees, we can confirm that our target value cannot exist in the other subtree. This feature of the BST gives us the efficient time complexity of O(logn).

Code

search(value) {

if (this.root === null)

return false; var current = this.root;

var found = false; while (current && !found) {

if (value < current.value) {

current = current.left

} else if (value > current.value) {

current = current.right;

} else {

found = true;

}

} if (!found)

return false; return current;

}

Inserting a node in a BST

Explanation

The process for inserting a node is quite similar to searching. We compare the value we want to insert with each node of the tree. If the insert value is less than node value, we look to insert it in the left subtree otherwise the right subtree.

Note that if we reach a point where the subtree that we look for does not exist (i.e. is null) , we know that we have found the correct spot to insert our new value.

To handle a base case where we have an empty tree. We can simply set the root to be the new value that is being inserted.

Code

insert(value) {

var newNode = new Node(value); if (this.root === null) {

this.root = value;

return this;

} var current = this.root; while (true) {

if (value === current.value)

return undefined;

if (value < current.value) {

if (current.left === null) {

current.left = newNode;

return this;

}

current = current.left;

} else if (value > current.value) {

if (current.right === null) {

current.right = newNode;

return this;

}

current = current.right;

}

}

}

Deleting a node from BST

Explanation

Deleting a node from BST is the most involved function in our data structure.

In order to remove a node, we need to consider three different cases

Removing a node that has no children

- If we are removing a leaf node, we can simply remove the connection of the parent to the leaf node. Removing a node that has one child

- If we are removing a node that has single child, we can make a connection between the node’s parent and the node’s child.

- If node has left child, then we will connect parent node with the left child.

- If node has right child, then we will connect parent node with the right child. Removing a node that has two children

- To remove a node with two children, find the left most node in the right subtree. Remove this node.

- Replace node’s value with the value with value of left most node in the right subtree.

Code

remove(value) {

this.root = this.removeHelper(this.root, value);

}



findMinValue(node) {

// Find the left most node in the tree

// Leftmost node will be the smallest node while (node.left) {

node = node.left;

} return node.value;

}

removeHelper(node, value) {

if (!node)

return; if (value < node.value) {

// Remove node from the left subtree

node.left = this.removeHelper(node.left, value);

return node;

} else if (value > node.value) {

// Remove node from the right subtree

node.right = this.removeHelper(node.right, value);

return node;

} else {

// Found the node

if (!node.left && !node.right) {

// Case 1: No children

return null;

} else if (!node.left) {

// Case 2: Single right child

return node.right;

} else if (!node.right) {

// Case 2: Single left child

return node.left;

} else {

// Case 3: Two children

node.value = this.findMinValue(node.right);

node.right = this.removeHelper(node.right,

node.value);

return node;

}

}

}

Conclusion

Hopefully that overview was helpful in understanding the fundamentals of a BST. In order to truly understand the concepts, make sure to practice writing the data structure and all its methods from scratch.