Skip to content

Commit ead784c

Browse files
committed
feat(bst): implement balanced binary search tree with traversal, balance, and rebalance support
1 parent 86920da commit ead784c

File tree

5 files changed

+380
-0
lines changed

5 files changed

+380
-0
lines changed

binary-search-tree/Node.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default class Node {
2+
constructor(data) {
3+
this.data = data;
4+
this.left = null;
5+
this.right = null;
6+
}
7+
}

binary-search-tree/README.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# 🌳 Binary Search Tree (BST) — The Odin Project
2+
3+
A Binary Search Tree (BST) is a hierarchical data structure where each node has:
4+
- At most two children
5+
- Left child < Parent
6+
- Right child > Parent
7+
8+
This project implements a **balanced BST** from scratch and covers all common operations.
9+
10+
---
11+
12+
## 📚 Node & Tree Overview
13+
14+
### ✅ Node Structure
15+
Each node holds:
16+
- `data`: the value
17+
- `left`: reference to the left child
18+
- `right`: reference to the right child
19+
20+
```js
21+
class Node {
22+
constructor(data) {
23+
this.data = data;
24+
this.left = null;
25+
this.right = null;
26+
}
27+
}
28+
```
29+
---
30+
31+
## ✅ Tree Class
32+
33+
The `Tree` class:
34+
- Accepts an array
35+
- Builds a balanced BST
36+
- Provides methods to insert, delete, traverse, check balance, etc.
37+
38+
---
39+
40+
## ⚙️ Core Methods
41+
42+
### 🔨 buildTree(array)
43+
- Removes duplicates
44+
- Sorts the array
45+
- Recursively selects the middle element as the root
46+
- Balances the tree by design
47+
48+
```js
49+
[1, 2, 3, 4, 5, 6, 7]
50+
⬇️
51+
(4)
52+
/ \
53+
(2) (6)
54+
/ \ / \
55+
(1) (3) (5) (7)
56+
```
57+
---
58+
59+
## ➕ insert(value)
60+
Adds a value while preserving BST rules.
61+
62+
---
63+
64+
## ➖ deleteItem(value)
65+
Deletes a node while handling:
66+
- No children
67+
- One child
68+
- Two children (replace with in-order successor)
69+
70+
---
71+
72+
## 🔍 find(value)
73+
Searches for a value in **O(log n)** time (if balanced).
74+
75+
---
76+
77+
## 🔄 Tree Traversals
78+
Each function accepts a callback (e.g., `node => console.log(node.data)`):
79+
80+
### 🌐 levelOrderForEach(callback)
81+
- Breadth-first traversal
82+
- Uses a queue
83+
84+
### 🧭 inOrderForEach(callback)
85+
- Left → Root → Right
86+
- Yields sorted values in BST
87+
88+
### 🧭 preOrderForEach(callback)
89+
- Root → Left → Right
90+
91+
### 🧭 postOrderForEach(callback)
92+
- Left → Right → Root
93+
94+
---
95+
96+
## 📏 Tree Measurements
97+
98+
### 📐 height(value)
99+
- Number of edges in longest path from node to a leaf.
100+
101+
### 📏 depth(value)
102+
- Number of edges from root to the node.
103+
104+
---
105+
106+
## ✅ Balancing
107+
108+
### 🔄 isBalanced()
109+
A tree is balanced if:
110+
- Heights of left and right subtrees of every node differ by ≤ 1
111+
- Both subtrees are balanced
112+
113+
### 🧘 rebalance()
114+
- Traverses in-order to collect all values
115+
- Rebuilds tree using `buildTree`
116+
117+
---
118+
119+
## 🧪 Driver Script
120+
```js
121+
// Create a tree
122+
const tree = new Tree([1, 7, 4, 23, 8, 9, 4, 3, 5, 7, 9, 67, 6345, 324]);
123+
124+
tree.insert(101);
125+
tree.insert(150);
126+
tree.insert(203);
127+
128+
console.log(tree.isBalanced()); // false
129+
130+
tree.rebalance();
131+
132+
console.log(tree.isBalanced()); // true
133+
```
134+
---
135+
136+
## 🖼️ prettyPrint()
137+
A function to visualize the tree:
138+
139+
```js
140+
const prettyPrint = (node, prefix = '', isLeft = true) => { ... }
141+
prettyPrint(tree.root);
142+
```
143+
Example output:
144+
```bash
145+
┌── 67
146+
┌── 23
147+
│ └── 9
148+
└── 7
149+
└── 4
150+
```
151+
---
152+
153+
## 🧠 Key Takeaways
154+
155+
- BST enables fast **search, insert, delete** — O(log n) if balanced.
156+
- **Balance is crucial** for performance.
157+
- Traversals reveal structural and logical properties of the tree.
158+
- Practice **recursion deeply** — it’s everywhere in trees.
159+
160+
---
161+
162+
## ✅ Tasks Completed (TOP Milestones)
163+
164+
- [x] Node class
165+
- [x] Tree class with `buildTree`
166+
- [x] Insertion & deletion logic
167+
- [x] Find method
168+
- [x] All four traversals with callback support
169+
- [x] `height()` and `depth()`
170+
- [x] `isBalanced()` and `rebalance()`
171+
- [x] Driver script and visualization
172+
173+
---
174+

binary-search-tree/Tree.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import Node from './Node.js';
2+
3+
export default class Tree {
4+
constructor(array) {
5+
this.root = this.buildTree(array);
6+
}
7+
8+
buildTree(array) {
9+
const sorted = [...new Set(array)].sort((a, b) => a - b);
10+
return this.buildBalancedTree(sorted);
11+
}
12+
13+
buildBalancedTree(sorted) {
14+
if (!sorted.length) return null;
15+
16+
const mid = Math.floor(sorted.length / 2);
17+
const root = new Node(sorted[mid]);
18+
19+
root.left = this.buildBalancedTree(sorted.slice(0, mid));
20+
root.right = this.buildBalancedTree(sorted.slice(mid + 1));
21+
22+
return root;
23+
}
24+
25+
insert(value, root = this.root) {
26+
if (root === null) return new Node(value);
27+
28+
if (value < root.data) {
29+
root.left = this.insert(value, root.left);
30+
} else if (value > root.data) {
31+
root.right = this.insert(value, root.right);
32+
}
33+
34+
return root;
35+
}
36+
37+
deleteItem(value, root = this.root) {
38+
if (root === null) return null;
39+
40+
if (value < root.data) {
41+
root.left = this.deleteItem(value, root.left);
42+
} else if (value > root.data) {
43+
root.right = this.deleteItem(value, root.right);
44+
} else {
45+
if (!root.left) return root.right;
46+
if (!root.right) return root.left;
47+
48+
const minLargerNode = this.findMin(root.right);
49+
root.data = minLargerNode.data;
50+
root.right = this.deleteItem(minLargerNode.data, root.right);
51+
}
52+
return root;
53+
}
54+
55+
findMin(node) {
56+
while (node.left !== null) node = node.left;
57+
return node;
58+
}
59+
60+
find(value, root = this.root) {
61+
if (!root || root.data === value) return root;
62+
return value < root.data
63+
? this.find(value, root.left)
64+
: this.find(value, root.right);
65+
}
66+
67+
levelOrderForEach(callback) {
68+
if (!callback) throw new Error("Callback is required");
69+
const queue = [this.root];
70+
while (queue.length) {
71+
const node = queue.shift();
72+
callback(node);
73+
if (node.left) queue.push(node.left);
74+
if (node.right) queue.push(node.right);
75+
}
76+
}
77+
78+
inOrderForEach(callback, node = this.root) {
79+
if (!callback) throw new Error("Callback is required");
80+
if (node === null) return;
81+
this.inOrderForEach(callback, node.left);
82+
callback(node);
83+
this.inOrderForEach(callback, node.right);
84+
}
85+
86+
preOrderForEach(callback, node = this.root) {
87+
if (!callback) throw new Error("Callback is required");
88+
if (node === null) return;
89+
callback(node);
90+
this.preOrderForEach(callback, node.left);
91+
this.preOrderForEach(callback, node.right);
92+
}
93+
94+
postOrderForEach(callback, node = this.root) {
95+
if (!callback) throw new Error("Callback is required");
96+
if (node === null) return;
97+
this.postOrderForEach(callback, node.left);
98+
this.postOrderForEach(callback, node.right);
99+
callback(node);
100+
}
101+
102+
height(value) {
103+
const node = this.find(value);
104+
if (!node) return null;
105+
106+
const getHeight = (node) => {
107+
if (!node) return -1;
108+
return 1 + Math.max(getHeight(node.left), getHeight(node.right));
109+
};
110+
111+
return getHeight(node);
112+
}
113+
114+
depth(value, node = this.root, depthCount = 0) {
115+
if (!node) return null;
116+
if (node.data === value) return depthCount;
117+
118+
if (value < node.data) return this.depth(value, node.left, depthCount + 1);
119+
return this.depth(value, node.right, depthCount + 1);
120+
}
121+
122+
isBalanced(root = this.root) {
123+
if (!root) return true;
124+
125+
const height = (node) => {
126+
if (!node) return -1;
127+
return 1 + Math.max(height(node.left), height(node.right));
128+
};
129+
130+
const leftHeight = height(root.left);
131+
const rightHeight = height(root.right);
132+
133+
const balanced =
134+
Math.abs(leftHeight - rightHeight) <= 1 &&
135+
this.isBalanced(root.left) &&
136+
this.isBalanced(root.right);
137+
138+
return balanced;
139+
}
140+
141+
rebalance() {
142+
const values = [];
143+
this.inOrderForEach((node) => values.push(node.data));
144+
this.root = this.buildBalancedTree(values);
145+
}
146+
}

binary-search-tree/driver.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Tree from './Tree.js';
2+
import prettyPrint from './prettyPrint.js';
3+
4+
function getRandomArray(length = 10, max = 100) {
5+
return Array.from({ length }, () => Math.floor(Math.random() * max));
6+
}
7+
8+
const randomData = getRandomArray();
9+
const tree = new Tree(randomData);
10+
11+
console.log('Initial Tree:');
12+
prettyPrint(tree.root);
13+
14+
console.log('Is Balanced:', tree.isBalanced());
15+
16+
console.log('Level Order:');
17+
tree.levelOrderForEach((node) => console.log(node.data));
18+
19+
console.log('Pre Order:');
20+
tree.preOrderForEach((node) => console.log(node.data));
21+
22+
console.log('Post Order:');
23+
tree.postOrderForEach((node) => console.log(node.data));
24+
25+
console.log('In Order:');
26+
tree.inOrderForEach((node) => console.log(node.data));
27+
28+
// Unbalancing
29+
tree.insert(150);
30+
tree.insert(200);
31+
tree.insert(300);
32+
33+
console.log('\nUnbalanced Tree:');
34+
prettyPrint(tree.root);
35+
console.log('Is Balanced:', tree.isBalanced());
36+
37+
// Rebalancing
38+
tree.rebalance();
39+
console.log('\nRebalanced Tree:');
40+
prettyPrint(tree.root);
41+
console.log('Is Balanced:', tree.isBalanced());

binary-search-tree/prettyPrint.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const prettyPrint = (node, prefix = '', isLeft = true) => {
2+
if (node === null) return;
3+
if (node.right !== null) {
4+
prettyPrint(node.right, `${prefix}${isLeft ? '│ ' : ' '}`, false);
5+
}
6+
console.log(`${prefix}${isLeft ? '└── ' : '┌── '}${node.data}`);
7+
if (node.left !== null) {
8+
prettyPrint(node.left, `${prefix}${isLeft ? ' ' : '│ '}`, true);
9+
}
10+
};
11+
12+
export default prettyPrint;

0 commit comments

Comments
 (0)