Mastering Binary Search Trees in TypeScript: A Programmer's Guide
Introduction
Hey there, fellow coders! Today, we're diving into the fascinating world of Binary Search Trees (BSTs) and how they come to life in TypeScript. Whether you're a seasoned pro or just starting, understanding BSTs is like having a Swiss Army knife in your programming toolkit. So, let's embark on this journey together and unravel the mysteries of BSTs in TypeScript.
What is a Binary Search Tree?
Definition
A Binary Search Tree is a node-based data structure where each node has at most two children. Here's the kicker: it's designed in such a way that the left child is always less than the parent node, and the right child is always greater. This unique property makes BSTs a go-to for efficient searching, insertion, and deletion operations.
Why Use a BST?
- Speedy Searches: Thanks to its structure, finding elements is a breeze.
- Dynamic Data Handling: It can easily grow or shrink, adapting to your data needs.
- Sorted Data: It keeps your data neatly sorted, making operations like finding the min or max value a walk in the park.
Starting with the Queue:
In the following article on a queue in typescript, we covered the queue. So for brevity, I will show the code here. But i'd encourage a read through if you are not familiar with it. Since we will be using the queue in our binary search tree, here it is:
export class Queue { items: any[keyof string | number] | any rear: number front: number constructor() { this.items = {} this.rear = 0 this.front = 0 } enqueue(element: any) { this.items[this.rear] = element this.rear++ } dequeue() { const item = this.items[this.front] delete this.items[this.front] this.front++ return item } isEmpty() { return this.front - this.rear === 0 } peek() { return this.items[this.front] } size() { return this.rear - this.front } print() { console.log(this.items) } clear() { this.items = {} this.rear = 0 this.front = 0 } }
Implementing a BST in TypeScript
The Node Class
Secondly, here is our Node. Will be using the node to track the node value, and the pointer to the connected nodes. The left node in the tree is always lower than the value of your node. And the right node is always higher.
class Node {
constructor(value: any) {
this.value = value
this.left = null
this.right = null
}
value: any
left: any
right: any
}
The BinarySearchTree Class
Now, let's create the BST class itself. In any file you want, in my case, I added it to it's own file called binarySearchTree.ts, I will export default the class. That way I can reuse this class in various areas of my projects.
export default class BinarySearchTree<T> {
root: Node<T> | null;
constructor() {
this.root = null;
}
// Methods will be added here
}
Core Operations
We will need a bunch of methods to make this class useful. To start, it may be handy to know if it is empty or not. We will do this with the isEmpty() method:
isEmpty() { return this.root === null }
Which is pretty self explanatory. It checks to see if the root has a node value. If not, then there is no node.
Insertion
Up next, we need the ability to add a node. We will do this by inserting a new value into our BST. Which is like a mini-maze. But we will do this with two methods. Insert, and insertNode (which will be private).
insert(value: any) {
const newNode = new Node(value)
if (this.isEmpty()) {
this.root = newNode
return this
} else {
this.insertNode(this.root!, newNode)
}
}
private insertNode(root: Node, newNode: Node) {
if (newNode.value < root.value) {
if (root.left === null) {
root.left = newNode
return
} else {
this.insertNode(root.left, newNode)
}
} else {
if (root.right === null) {
root.right = newNode
return
} else {
this.insertNode(root.right, newNode)
}
}
}
First we create the private insertNode method which takes the root node, and the new Node as values. It will compare the left and right nodes on the root. If it's null then that is the last node and we insert there. If the new value is less than the current value, it goes to the left, if larger it goes to the right. And it does this comparison all along the tree branches until we hit the null value, and insert.
So when we use this, we will use or insert method, which takes only the value. This will check if it's empty with our isEmpty() method. If it is, it applies the node right there. If not, it will use our insertNode() method we just created. And that will run recursively until the value is inserted. Pretty simple. Almost like magic.
Searching
Looking for a value? Let's see how we can find it.
search(root: Node | null, value: any): boolean {
if (!root) {
return false
} else {
if (root.value === value) {
return true
} else if (value < root.value) {
return this.search(root.left, value)
} else {
return this.search(root.right, value)
}
}
}
This is pretty simple. It will recursively compare the value you are searching for, checking if it equals the value of the current node, or if it is greater than or equal to the left or right nodes and moves along the branches until null or the value is found!
Deletion
Removing a value involves a bit of a dance, ensuring the BST remains valid. We will do a similar method to the insertion requiring two methods, one private and one public.
delete(value: number) {
this.deleteNode(this.root, value)
}
private deleteNode(root: Node | null, value: number) {
if (root === null) {
return root
}
if (value < root.value) {
root.left = this.deleteNode(root.left, value)
} else if (value > root.value) {
root.right = this.deleteNode(root.right, value)
} else {
if (!root.left && !root.right) {
return null
}
if (!root.left) {
return root.right
} else if (!root.right) {
return root.left
}
root!.value = this.minValue(root!.right)
root!.right = this.deleteNode(root!.right, root!.value)
}
return root
}
Advanced Concepts
Traversals
Exploring a BST can be done in various ways, each with its unique flavor.
In-Order Traversal
inOrderTraversal(root: Node | null) {
if (root) {
this.inOrderTraversal(root.left)
console.log(root.value)
this.inOrderTraversal(root.right)
}
}
Pre-Order Traversal
preOrderTraversal(root: Node | null) {
/**
* @DepthFirstSearch - PreOrder Traversal
*/
if (root) {
console.log(root.value)
this.preOrderTraversal(root.left)
this.preOrderTraversal(root.right)
}
}
Post-Order Traversal
postOrderTraversal(root: Node | null) {
if (root) {
this.postOrderTraversal(root.left)
this.postOrderTraversal(root.right)
console.log(root.value)
}
}
Reverse Order Traversal
reverseOrderTraversal(root: Node | null) {
if (root) {
this.postOrderTraversal(root.right)
console.log(root.value)
this.postOrderTraversal(root.left)
}
}
Level Order Traversal
levelOrderTraversal() {
// Breadth First Search
const queue = new Queue()
queue.enqueue(this.root)
while (!queue.isEmpty()) {
const node = queue.dequeue()
console.log(node.value)
if (node.left) {
queue.enqueue(node.left)
}
if (node.right) {
queue.enqueue(node.right)
}
}
}
Searching for High's and Low's
Here are another couple useful methods for searching for the highest and lowest values: We will call them minValue() and maxValue():
minValue(root: Node | null): number | null {
if (root === null) {
return null
} else if (!root.left) {
return root.value
} else {
return this.minValue(root.left)
}
}
maxValue(root: Node | null): number | null {
if (root === null) {
return null
} else if (!root.right) {
return root.value
} else {
return this.maxValue(root.right)
}
}
Practical Applications
Real-World Use Cases
- Database Indexing: BSTs are the backbone of efficient database indexing.
- Autocomplete Features: Ever wondered how your search bar predicts text? Yep, BSTs at play!
- Gaming: From AI decision-making to game state management, BSTs are game-changers.
Tips for Optimization
- Balancing Act: Keep your BST balanced for optimal performance.
- TypeScript's Generics: Leverage TypeScript's generics for flexible and reusable BSTs.
- Memory Management: Be mindful of memory usage, especially with large datasets.
Conclusion
Binary Search Trees are a powerhouse in the world of data structures, and TypeScript brings them to life with elegance and efficiency. Whether you're building a complex application or simply love algorithms, mastering BSTs in TypeScript is a skill that'll pay dividends. So, keep practicing, keep coding, and let the power of BSTs elevate your programming prowess to new heights!