Tree data structure part 2/5: balanced/unbalanced Binary Search Tree

*Prerequisite: Tree data structure part 1/5: Tree and Binary Tree.

This tutorial is a part of the Data Structures and Algorithms class:

Binary Search Tree (BST) properties

  • Node constraint: max 2 child nodes + left node < parent node < right node
  • Insert: smaller -> go left, bigger -> go right. Farthest left node is min, farthest right node is max.
  • Search: drop a side of the tree in each iteration
  • Delete = search + unlink + link.

BST high level vs on ram

⬆️ BST/AVL Tree search operation. Try it yourself

Binary Search Tree (BST) without self-balancing mechanism

  • The order of node insertion/deletion decides the shape of the tree.
  • Will become unbalanced after one (or a few) insert/delete operation(s).
  • In the worst case scenario, BST looks like a Linked List.

⬆️ Binary Search Tree data insertion without self-balancing 1 (worst case). Try it yourself

⬆️ Binary Search Tree data insertion without self-balancing 2. Try it yourself

Binary Search Tree (BST) with self-balancing mechanism

  • Balanced Binary Tree: left and right subtrees of every node differ in height by no more than 1.
  • Automatically run balancing method (O(logn)) after changes on insert/delete.
  • Tree is always balanced.
  • 2 popular BST balancing algorithms: AVL, Red-Black (AVL tree, Red-Black tree).
  • Both AVL and Red-Black trees have: O(logn) on insert/delete/search operation.

⬆️ Binary Search Tree data insertion with AVL self-balancing (AVL tree). Try it yourself

⬆️ AVL Tree delete operation. Try it yourself

Build Binary Search Tree (BST) data structure

lecture-11/binary-search-tree-data-structure.js
// structure of a node
class Node {
  constructor(data) {
    this.data = data
    this.left = null
    this.right = null
  }
}

// Binary Search Tree, without self-balancing
class BinarySearchTree {
  constructor(data) {
    this.root = data ? new Node(data) : null
  }

  // insert data to tree
  insert(data) {
    const node = this.root

    if(this.root === null) {
      this.root = new Node(data)
    } else {
      searchLeaf(node)

      function searchLeaf(node) {
        if(data < node.data) {
            if(node.left === null) {
              return node.left = new Node(data)
            }
            searchLeaf(node.left)
        } else if(data > node.data) {
            if(node.right === null) {
              node.right = new Node(data)
            }
            searchLeaf(node.right)
        } else {
          return null
        }
      }
    }
  }

  // find min value in binary search tree
  findMin() {
    let current = this.root
    while(current.left !== null) {
      current = current.left
    }
    return current.data
  }

  // find max value in binary search tree
  findMax() {
    let current = this.root
    while(current.right !== null) {
      current = current.right
    }
    return current.data
  }

  // check if data is in binary search tree
  exist(data) {
    let current = this.root
    while(current) {
      if(current.data === data) {
        return true
      } else if(current.data > data) {
        current = current.left
      } else {
        current = current.right
      }
    }
    return false
  }

  // remove a node from binary tree
  remove(data) {
    this.root = removeNode(this.root, data)

    function removeNode(node, data) {
      // root node/tree has nothing in it
      if(node == null) {
        return null
      }

      if(data < node.data) { // move left
        node.left = removeNode(node.left, data)
      } else if(data > node.data) { // move to right
        node.right = removeNode(node.right, data)
      } else { // found matching node
        if(node.left === null && node.right === null) { return null } // no children
        if(node.left === null) { return node.right } // has right child
        if(node.right === null) { return node.left } // has left child
        let rightThenFarthestLeft = node.right; // has both left, right child
        while(rightThenFarthestLeft.left !== null) {
            rightThenFarthestLeft = rightThenFarthestLeft.left;
        }
        node.data = rightThenFarthestLeft.data;
        removeNode(node.right, rightThenFarthestLeft.data);
      }
      return node;
    }
  }

  // preorder traversal
  preOrder() {
    if (this.root == null) {
      return null
    } else {
      var result = []
      traversePreOrder(this.root)
      return result

      function traversePreOrder(node) {
        result.push(node.data)
        node.left && traversePreOrder(node.left)
        node.right && traversePreOrder(node.right)
      }
    }
  }

  // inorder traversal (print min -> max)
  inOrder() {
    if (this.root == null) {
      return null
    } else {
      var result = []
      traverseInOrder(this.root)
      return result

      function traverseInOrder(node) {
        node.left && traverseInOrder(node.left)
        result.push(node.data)
        node.right && traverseInOrder(node.right)
      }
    }
  }

  // postorder traversal
  postOrder() {
    if (this.root == null) {
      return null
    } else {
      var result = []
      traversePostOrder(this.root)
      return result

      function traversePostOrder(node) {
        node.left && traversePostOrder(node.left)
        node.right && traversePostOrder(node.right)
        result.push(node.data)
      }
    }
  }

  printMaxToMin() {
    if (this.root == null) {
      return null
    } else {
      var result = []
      traverseInOrder(this.root)
      return result

      function traverseInOrder(node) {
        node.right && traverseInOrder(node.right)
        result.push(node.data)
        node.left && traverseInOrder(node.left)
      }
    }
  }
}

const bst = new BinarySearchTree()
console.log(bst) // BinarySearchTree { root: null }

// insert data to tree
bst.insert(6)
bst.insert(4)
bst.insert(8)
bst.insert(1)
bst.insert(5)
bst.insert(10)
console.log(JSON.stringify(bst, null, 4))

// tree operations
console.log(bst.findMin()) // 1
console.log(bst.findMax()) // 10
console.log(bst.exist(100)) // false
console.log(bst.exist(8)) // true
console.log(bst.remove(8)) // undefined
console.log(JSON.stringify(bst, null, 4))

console.log(bst.exist(8)) // false
console.log(bst.inOrder()) // [ 1, 4, 5, 6, 10 ]
console.log(bst.printMaxToMin()) // [ 10, 6, 5, 4, 1 ]
console.log(bst.preOrder()) // [ 6, 4, 1, 5, 10 ]
console.log(bst.postOrder()) // [ 1, 5, 4, 10, 6 ]
$ node binary-search-tree-data-structure.js
BinarySearchTree { root: null }
{
    "root": {
        "data": 6,
        "left": {
            "data": 4,
            "left": {
                "data": 1,
                "left": null,
                "right": null
            },
            "right": {
                "data": 5,
                "left": null,
                "right": null
            }
        },
        "right": {
            "data": 8,
            "left": null,
            "right": {
                "data": 10,
                "left": null,
                "right": null
            }
        }
    }
}
1
10
false
true
preOrder(): [ 6, 4, 1, 5, 8, 10 ]
inOrder(): [ 1, 4, 5, 6, 8, 10 ]
printMaxToMin(): [ 10, 8, 6, 5, 4, 1 ]
postOrder(): [ 1, 5, 4, 10, 8, 6 ]

Real life interview questions

  1. Solve Leetcode #21: Merge Two Sorted Lists.
  2. Solve Leetcode #95: Unique Binary Search Trees II.
  3. Solve Leetcode #230: Kth Smallest Element in a BST.
  4. Try this npm package to use pre-built AVL tree operations.