Binary Search Tree

Introduction

A Binary Search Tree (BST) is a tree data structure in which each node has at most two children, referred to as the left child and the right child. BSTs are used to implement efficient searching, insertion, and deletion operations.

Explanation

In a BST, the left subtree of a node contains only nodes with keys lesser than the node’s key, and the right subtree only nodes with keys greater than the node’s key. This property provides efficient access to elements and makes BSTs fundamental in applications where data is constantly being inserted and removed.

Basic Operations

  1. Searching:

    • To search for a value in a BST, start at the root and compare it to the value. If the value is found, return it. If it’s less, search the left subtree; if greater, search the right subtree.
    • Time Complexity: (O(\log n)) on average, (O(n)) in the worst case.
  2. Insertion:

    • Insertion begins like a search, to find the proper place to insert the new node. Once a leaf node is reached, the new node is added as a child of the leaf node.
    • Time Complexity: (O(\log n)) on average, (O(n)) in the worst case.
  3. Deletion:

    • There are three cases for deletion: deleting a leaf node, a node with one child, and a node with two children. The most complex case is when the node has two children.
    • Time Complexity: (O(\log n)) on average, (O(n)) in the worst case.

JavaScript Implementation

In this implementation, the BinarySearchTree class includes methods for inserting, searching, and deleting nodes in the tree. Each node of the tree is an instance of the Node class. The tree traversal methods (inOrder, preOrder, and postOrder) are implemented to explore the nodes of the tree in different orders.

javascript
	// Node class represents each node in the BST
	class Node {
	  constructor(data) {
	    this.data = data; // Value of the node
	    this.left = null; // Pointer to left child
	    this.right = null; // Pointer to right child
	  }
	}
	
	// BinarySearchTree class provides the functionality of a BST
	class BinarySearchTree {
	  constructor() {
	    this.root = null; // Root of the BST
	  }
	
	  // Method to insert a new node with a given data
	  insert(data) {
	    const newNode = new Node(data);
	    if (this.root === null) {
	      this.root = newNode; // If tree is empty, new node becomes root
	    } else {
	      this.insertNode(this.root, newNode); // Else, use helper method to find the right position
	    }
	  }
	
	  // Helper method to insert a node in the tree by recursively finding its correct position
	  insertNode(node, newNode) {
	    if (newNode.data < node.data) {
	      // If new node's data is less than current node's data
	      if (node.left === null) {
	        node.left = newNode; // Insert new node as left child
	      } else {
	        this.insertNode(node.left, newNode); // Continue searching in left subtree
	      }
	    } else {
	      if (node.right === null) {
	        node.right = newNode; // Insert new node as right child
	      } else {
	        this.insertNode(node.right, newNode); // Continue searching in right subtree
	      }
	    }
	  }
	
	  // Method to search for a node with a given data
	  search(node, data) {
	    if (node === null) {
	      return null; // Return null if node is not found
	    } else if (data < node.data) {
	      return this.search(node.left, data); // Search in left subtree
	    } else if (data > node.data) {
	      return this.search(node.right, data); // Search in right subtree
	    } else {
	      return node; // Node found, return it
	    }
	  }
	
	  // Helper method to find the node with minimum value (used in deletion)
	  findMinNode(node) {
	    if (node.left === null) {
	      return node; // Leftmost node will be the min node
	    } else {
	      return this.findMinNode(node.left); // Continue searching in left subtree
	    }
	  }
	
	  // Method to delete a node with a given data
	  delete(data) {
	    this.root = this.deleteNode(this.root, data); // Start deletion from the root
	  }
	
	  // Helper method to delete a node by recursively finding and removing it
	  deleteNode(node, key) {
	    if (node === null) {
	      return null; // Node not found, return null
	    } else if (key < node.data) {
	      node.left = this.deleteNode(node.left, key); // Continue searching in left subtree
	      return node;
	    } else if (key > node.data) {
	      node.right = this.deleteNode(node.right, key); // Continue searching in right subtree
	      return node;
	    } else {
	      // Node with only one child or no child
	      if (node.left === null) {
	        return node.right;
	      } else if (node.right === null) {
	        return node.left;
	      }
	
	      // Node with two children: Get the inorder successor (smallest in the right subtree)
	      let aux = this.findMinNode(node.right);
	
	      // Copy the inorder successor's content to this node
	      node.data = aux.data;
	
	      // Delete the inorder successor
	      node.right = this.deleteNode(node.right, aux.data);
	      return node;
	    }
	  }
	
	  // Helper methods for tree traversal
	
	  // In-order traversal: left, root, right
	  inOrder(node) {
	    if (node !== null) {
	      this.inOrder(node.left);
	      console.log(node.data);
	      this.inOrder(node.right);
	    }
	  }
	
	  // Pre-order traversal: root, left, right
	  preOrder(node) {
	    if (node !== null) {
	      console.log(node.data);
	      this.preOrder(node.left);
	      this.preOrder(node.right);
	    }
	  }
	
	  // Post-order traversal: left, right, root
	  postOrder(node) {
	    if (node !== null) {
	      this.postOrder(node.left);
	      this.postOrder(node.right);
	      console.log(node.data);
	    }
	  }
	}
	
	// Example Usage
	const BST = new BinarySearchTree();
	BST.insert(15);
	BST.insert(25);
	BST.insert(10);
	BST.insert(7);
	BST.insert(22);
	BST.insert(17);
	BST.insert(13);
	BST.insert(5);
	BST.insert(9);
	BST.insert(27);
	
	let root = BST.getRootNode(); // Note: getRootNode method should be defined in the class
	console.log('In-order Traversal:');
	BST.inOrder(root); // In-order Traversal
	console.log('Pre-order Traversal:');
	BST.preOrder(root); // Pre-order Traversal
	console.log('Post-order Traversal:');
	BST.postOrder(root); // Post-order Traversal
	
	console.log('Searching for 22:');
	let found = BST.search(root, 22);
	console.log(found);

Pros and Cons

  • Pros:
    • Offers efficient searching, insertion, and deletion operations.
    • Can be used to implement dynamic sets, priority queues, and associative arrays.
  • Cons:
    • Performance depends heavily on the tree’s balance; a degenerate tree (like a linked list) can lead to poor performance.

Practical Use Cases

  1. Implementing Various Data Structures:

    • BSTs are used in implementing sets, maps, and priority queues, where quick lookup, insertion, and deletion are required.
  2. Database Indexing:

    • Many database systems use BSTs for indexing, allowing quick data retrieval.

Conclusion

Binary Search Trees provide a balance between efficient insertion and quick search, making them highly versatile for various applications where data is continuously inserted and deleted. Understanding BST operations is crucial for applications where dynamic data management and fast access are required.