Skip to content

Commit 2c67b48

Browse files
authoredMar 9, 2024
Ad hoc versions of MinHeap, MaxHeap, and DisjointSet (trekhleb#1117)
* Add DisjointSetMinimalistic * Add MinHeapMinimalistic and MaxHeapMinimalistic * Rename minimalistic to adhoc * Update README
1 parent ac78353 commit 2c67b48

File tree

8 files changed

+552
-0
lines changed

8 files changed

+552
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* The minimalistic (ad hoc) version of a DisjointSet (or a UnionFind) data structure
3+
* that doesn't have external dependencies and that is easy to copy-paste and
4+
* use during the coding interview if allowed by the interviewer (since many
5+
* data structures in JS are missing).
6+
*
7+
* Time Complexity:
8+
*
9+
* - Constructor: O(N)
10+
* - Find: O(α(N))
11+
* - Union: O(α(N))
12+
* - Connected: O(α(N))
13+
*
14+
* Where N is the number of vertices in the graph.
15+
* α refers to the Inverse Ackermann function.
16+
* In practice, we assume it's a constant.
17+
* In other words, O(α(N)) is regarded as O(1) on average.
18+
*/
19+
class DisjointSetAdhoc {
20+
/**
21+
* Initializes the set of specified size.
22+
* @param {number} size
23+
*/
24+
constructor(size) {
25+
// The index of a cell is an id of the node in a set.
26+
// The value of a cell is an id (index) of the root node.
27+
// By default, the node is a parent of itself.
28+
this.roots = new Array(size).fill(0).map((_, i) => i);
29+
30+
// Using the heights array to record the height of each node.
31+
// By default each node has a height of 1 because it has no children.
32+
this.heights = new Array(size).fill(1);
33+
}
34+
35+
/**
36+
* Finds the root of node `a`
37+
* @param {number} a
38+
* @returns {number}
39+
*/
40+
find(a) {
41+
if (a === this.roots[a]) return a;
42+
this.roots[a] = this.find(this.roots[a]);
43+
return this.roots[a];
44+
}
45+
46+
/**
47+
* Joins the `a` and `b` nodes into same set.
48+
* @param {number} a
49+
* @param {number} b
50+
* @returns {number}
51+
*/
52+
union(a, b) {
53+
const aRoot = this.find(a);
54+
const bRoot = this.find(b);
55+
56+
if (aRoot === bRoot) return;
57+
58+
if (this.heights[aRoot] > this.heights[bRoot]) {
59+
this.roots[bRoot] = aRoot;
60+
} else if (this.heights[aRoot] < this.heights[bRoot]) {
61+
this.roots[aRoot] = bRoot;
62+
} else {
63+
this.roots[bRoot] = aRoot;
64+
this.heights[aRoot] += 1;
65+
}
66+
}
67+
68+
/**
69+
* Checks if `a` and `b` belong to the same set.
70+
* @param {number} a
71+
* @param {number} b
72+
*/
73+
connected(a, b) {
74+
return this.find(a) === this.find(b);
75+
}
76+
}
77+
78+
export default DisjointSetAdhoc;

‎src/data-structures/disjoint-set/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ _MakeSet_ creates 8 singletons.
1919

2020
After some operations of _Union_, some sets are grouped together.
2121

22+
## Implementation
23+
24+
- [DisjointSet.js](./DisjointSet.js)
25+
- [DisjointSetAdhoc.js](./DisjointSetAdhoc.js) - The minimalistic (ad hoc) version of a DisjointSet (or a UnionFind) data structure that doesn't have external dependencies and that is easy to copy-paste and use during the coding interview if allowed by the interviewer (since many data structures in JS are missing).
26+
2227
## References
2328

2429
- [Wikipedia](https://en.wikipedia.org/wiki/Disjoint-set_data_structure)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import DisjointSetAdhoc from '../DisjointSetAdhoc';
2+
3+
describe('DisjointSetAdhoc', () => {
4+
it('should create unions and find connected elements', () => {
5+
const set = new DisjointSetAdhoc(10);
6+
7+
// 1-2-5-6-7 3-8-9 4
8+
set.union(1, 2);
9+
set.union(2, 5);
10+
set.union(5, 6);
11+
set.union(6, 7);
12+
13+
set.union(3, 8);
14+
set.union(8, 9);
15+
16+
expect(set.connected(1, 5)).toBe(true);
17+
expect(set.connected(5, 7)).toBe(true);
18+
expect(set.connected(3, 8)).toBe(true);
19+
20+
expect(set.connected(4, 9)).toBe(false);
21+
expect(set.connected(4, 7)).toBe(false);
22+
23+
// 1-2-5-6-7 3-8-9-4
24+
set.union(9, 4);
25+
26+
expect(set.connected(4, 9)).toBe(true);
27+
expect(set.connected(4, 3)).toBe(true);
28+
expect(set.connected(8, 4)).toBe(true);
29+
30+
expect(set.connected(8, 7)).toBe(false);
31+
expect(set.connected(2, 3)).toBe(false);
32+
});
33+
34+
it('should keep the height of the tree small', () => {
35+
const set = new DisjointSetAdhoc(10);
36+
37+
// 1-2-6-7-9 1 3 4 5
38+
set.union(7, 6);
39+
set.union(1, 2);
40+
set.union(2, 6);
41+
set.union(1, 7);
42+
set.union(9, 1);
43+
44+
expect(set.connected(1, 7)).toBe(true);
45+
expect(set.connected(6, 9)).toBe(true);
46+
expect(set.connected(4, 9)).toBe(false);
47+
48+
expect(Math.max(...set.heights)).toBe(3);
49+
});
50+
});
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* The minimalistic (ad hoc) version of a MaxHeap data structure that doesn't have
3+
* external dependencies and that is easy to copy-paste and use during the
4+
* coding interview if allowed by the interviewer (since many data
5+
* structures in JS are missing).
6+
*/
7+
class MaxHeapAdhoc {
8+
constructor(heap = []) {
9+
this.heap = [];
10+
heap.forEach(this.add);
11+
}
12+
13+
add(num) {
14+
this.heap.push(num);
15+
this.heapifyUp();
16+
}
17+
18+
peek() {
19+
return this.heap[0];
20+
}
21+
22+
poll() {
23+
if (this.heap.length === 0) return undefined;
24+
const top = this.heap[0];
25+
this.heap[0] = this.heap[this.heap.length - 1];
26+
this.heap.pop();
27+
this.heapifyDown();
28+
return top;
29+
}
30+
31+
isEmpty() {
32+
return this.heap.length === 0;
33+
}
34+
35+
toString() {
36+
return this.heap.join(',');
37+
}
38+
39+
heapifyUp() {
40+
let nodeIndex = this.heap.length - 1;
41+
while (nodeIndex > 0) {
42+
const parentIndex = this.getParentIndex(nodeIndex);
43+
if (this.heap[parentIndex] >= this.heap[nodeIndex]) break;
44+
this.swap(parentIndex, nodeIndex);
45+
nodeIndex = parentIndex;
46+
}
47+
}
48+
49+
heapifyDown() {
50+
let nodeIndex = 0;
51+
52+
while (
53+
(
54+
this.hasLeftChild(nodeIndex) && this.heap[nodeIndex] < this.leftChild(nodeIndex)
55+
)
56+
|| (
57+
this.hasRightChild(nodeIndex) && this.heap[nodeIndex] < this.rightChild(nodeIndex)
58+
)
59+
) {
60+
const leftIndex = this.getLeftChildIndex(nodeIndex);
61+
const rightIndex = this.getRightChildIndex(nodeIndex);
62+
const left = this.leftChild(nodeIndex);
63+
const right = this.rightChild(nodeIndex);
64+
65+
if (this.hasLeftChild(nodeIndex) && this.hasRightChild(nodeIndex)) {
66+
if (left >= right) {
67+
this.swap(leftIndex, nodeIndex);
68+
nodeIndex = leftIndex;
69+
} else {
70+
this.swap(rightIndex, nodeIndex);
71+
nodeIndex = rightIndex;
72+
}
73+
} else if (this.hasLeftChild(nodeIndex)) {
74+
this.swap(leftIndex, nodeIndex);
75+
nodeIndex = leftIndex;
76+
}
77+
}
78+
}
79+
80+
getLeftChildIndex(parentIndex) {
81+
return (2 * parentIndex) + 1;
82+
}
83+
84+
getRightChildIndex(parentIndex) {
85+
return (2 * parentIndex) + 2;
86+
}
87+
88+
getParentIndex(childIndex) {
89+
return Math.floor((childIndex - 1) / 2);
90+
}
91+
92+
hasLeftChild(parentIndex) {
93+
return this.getLeftChildIndex(parentIndex) < this.heap.length;
94+
}
95+
96+
hasRightChild(parentIndex) {
97+
return this.getRightChildIndex(parentIndex) < this.heap.length;
98+
}
99+
100+
leftChild(parentIndex) {
101+
return this.heap[this.getLeftChildIndex(parentIndex)];
102+
}
103+
104+
rightChild(parentIndex) {
105+
return this.heap[this.getRightChildIndex(parentIndex)];
106+
}
107+
108+
swap(indexOne, indexTwo) {
109+
const tmp = this.heap[indexTwo];
110+
this.heap[indexTwo] = this.heap[indexOne];
111+
this.heap[indexOne] = tmp;
112+
}
113+
}
114+
115+
export default MaxHeapAdhoc;
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* The minimalistic (ad hoc) version of a MinHeap data structure that doesn't have
3+
* external dependencies and that is easy to copy-paste and use during the
4+
* coding interview if allowed by the interviewer (since many data
5+
* structures in JS are missing).
6+
*/
7+
class MinHeapAdhoc {
8+
constructor(heap = []) {
9+
this.heap = [];
10+
heap.forEach(this.add);
11+
}
12+
13+
add(num) {
14+
this.heap.push(num);
15+
this.heapifyUp();
16+
}
17+
18+
peek() {
19+
return this.heap[0];
20+
}
21+
22+
poll() {
23+
if (this.heap.length === 0) return undefined;
24+
const top = this.heap[0];
25+
this.heap[0] = this.heap[this.heap.length - 1];
26+
this.heap.pop();
27+
this.heapifyDown();
28+
return top;
29+
}
30+
31+
isEmpty() {
32+
return this.heap.length === 0;
33+
}
34+
35+
toString() {
36+
return this.heap.join(',');
37+
}
38+
39+
heapifyUp() {
40+
let nodeIndex = this.heap.length - 1;
41+
while (nodeIndex > 0) {
42+
const parentIndex = this.getParentIndex(nodeIndex);
43+
if (this.heap[parentIndex] <= this.heap[nodeIndex]) break;
44+
this.swap(parentIndex, nodeIndex);
45+
nodeIndex = parentIndex;
46+
}
47+
}
48+
49+
heapifyDown() {
50+
let nodeIndex = 0;
51+
52+
while (
53+
(
54+
this.hasLeftChild(nodeIndex)
55+
&& this.heap[nodeIndex] > this.leftChild(nodeIndex)
56+
)
57+
|| (
58+
this.hasRightChild(nodeIndex)
59+
&& this.heap[nodeIndex] > this.rightChild(nodeIndex)
60+
)
61+
) {
62+
const leftIndex = this.getLeftChildIndex(nodeIndex);
63+
const rightIndex = this.getRightChildIndex(nodeIndex);
64+
const left = this.leftChild(nodeIndex);
65+
const right = this.rightChild(nodeIndex);
66+
67+
if (this.hasLeftChild(nodeIndex) && this.hasRightChild(nodeIndex)) {
68+
if (left <= right) {
69+
this.swap(leftIndex, nodeIndex);
70+
nodeIndex = leftIndex;
71+
} else {
72+
this.swap(rightIndex, nodeIndex);
73+
nodeIndex = rightIndex;
74+
}
75+
} else if (this.hasLeftChild(nodeIndex)) {
76+
this.swap(leftIndex, nodeIndex);
77+
nodeIndex = leftIndex;
78+
}
79+
}
80+
}
81+
82+
getLeftChildIndex(parentIndex) {
83+
return 2 * parentIndex + 1;
84+
}
85+
86+
getRightChildIndex(parentIndex) {
87+
return 2 * parentIndex + 2;
88+
}
89+
90+
getParentIndex(childIndex) {
91+
return Math.floor((childIndex - 1) / 2);
92+
}
93+
94+
hasLeftChild(parentIndex) {
95+
return this.getLeftChildIndex(parentIndex) < this.heap.length;
96+
}
97+
98+
hasRightChild(parentIndex) {
99+
return this.getRightChildIndex(parentIndex) < this.heap.length;
100+
}
101+
102+
leftChild(parentIndex) {
103+
return this.heap[this.getLeftChildIndex(parentIndex)];
104+
}
105+
106+
rightChild(parentIndex) {
107+
return this.heap[this.getRightChildIndex(parentIndex)];
108+
}
109+
110+
swap(indexOne, indexTwo) {
111+
const tmp = this.heap[indexTwo];
112+
this.heap[indexTwo] = this.heap[indexOne];
113+
this.heap[indexOne] = tmp;
114+
}
115+
}
116+
117+
export default MinHeapAdhoc;

‎src/data-structures/heap/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ Where:
5858

5959
> In this repository, the [MaxHeap.js](./MaxHeap.js) and [MinHeap.js](./MinHeap.js) are examples of the **Binary** heap.
6060
61+
## Implementation
62+
63+
- [MaxHeap.js](./MaxHeap.js) and [MinHeap.js](./MinHeap.js)
64+
- [MaxHeapAdhoc.js](./MaxHeapAdhoc.js) and [MinHeapAdhoc.js](./MinHeapAdhoc.js) - The minimalistic (ad hoc) version of a MinHeap/MaxHeap data structure that doesn't have external dependencies and that is easy to copy-paste and use during the coding interview if allowed by the interviewer (since many data structures in JS are missing).
65+
6166
## References
6267

6368
- [Wikipedia](https://en.wikipedia.org/wiki/Heap_(data_structure))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import MaxHeap from '../MaxHeapAdhoc';
2+
3+
describe('MaxHeapAdhoc', () => {
4+
it('should create an empty max heap', () => {
5+
const maxHeap = new MaxHeap();
6+
7+
expect(maxHeap).toBeDefined();
8+
expect(maxHeap.peek()).toBe(undefined);
9+
expect(maxHeap.isEmpty()).toBe(true);
10+
});
11+
12+
it('should add items to the heap and heapify it up', () => {
13+
const maxHeap = new MaxHeap();
14+
15+
maxHeap.add(5);
16+
expect(maxHeap.isEmpty()).toBe(false);
17+
expect(maxHeap.peek()).toBe(5);
18+
expect(maxHeap.toString()).toBe('5');
19+
20+
maxHeap.add(3);
21+
expect(maxHeap.peek()).toBe(5);
22+
expect(maxHeap.toString()).toBe('5,3');
23+
24+
maxHeap.add(10);
25+
expect(maxHeap.peek()).toBe(10);
26+
expect(maxHeap.toString()).toBe('10,3,5');
27+
28+
maxHeap.add(1);
29+
expect(maxHeap.peek()).toBe(10);
30+
expect(maxHeap.toString()).toBe('10,3,5,1');
31+
32+
maxHeap.add(1);
33+
expect(maxHeap.peek()).toBe(10);
34+
expect(maxHeap.toString()).toBe('10,3,5,1,1');
35+
36+
expect(maxHeap.poll()).toBe(10);
37+
expect(maxHeap.toString()).toBe('5,3,1,1');
38+
39+
expect(maxHeap.poll()).toBe(5);
40+
expect(maxHeap.toString()).toBe('3,1,1');
41+
42+
expect(maxHeap.poll()).toBe(3);
43+
expect(maxHeap.toString()).toBe('1,1');
44+
});
45+
46+
it('should poll items from the heap and heapify it down', () => {
47+
const maxHeap = new MaxHeap();
48+
49+
maxHeap.add(5);
50+
maxHeap.add(3);
51+
maxHeap.add(10);
52+
maxHeap.add(11);
53+
maxHeap.add(1);
54+
55+
expect(maxHeap.toString()).toBe('11,10,5,3,1');
56+
57+
expect(maxHeap.poll()).toBe(11);
58+
expect(maxHeap.toString()).toBe('10,3,5,1');
59+
60+
expect(maxHeap.poll()).toBe(10);
61+
expect(maxHeap.toString()).toBe('5,3,1');
62+
63+
expect(maxHeap.poll()).toBe(5);
64+
expect(maxHeap.toString()).toBe('3,1');
65+
66+
expect(maxHeap.poll()).toBe(3);
67+
expect(maxHeap.toString()).toBe('1');
68+
69+
expect(maxHeap.poll()).toBe(1);
70+
expect(maxHeap.toString()).toBe('');
71+
72+
expect(maxHeap.poll()).toBe(undefined);
73+
expect(maxHeap.toString()).toBe('');
74+
});
75+
76+
it('should heapify down through the right branch as well', () => {
77+
const maxHeap = new MaxHeap();
78+
79+
maxHeap.add(3);
80+
maxHeap.add(12);
81+
maxHeap.add(10);
82+
83+
expect(maxHeap.toString()).toBe('12,3,10');
84+
85+
maxHeap.add(11);
86+
expect(maxHeap.toString()).toBe('12,11,10,3');
87+
88+
expect(maxHeap.poll()).toBe(12);
89+
expect(maxHeap.toString()).toBe('11,3,10');
90+
});
91+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import MinHeapAdhoc from '../MinHeapAdhoc';
2+
3+
describe('MinHeapAdhoc', () => {
4+
it('should create an empty min heap', () => {
5+
const minHeap = new MinHeapAdhoc();
6+
7+
expect(minHeap).toBeDefined();
8+
expect(minHeap.peek()).toBe(undefined);
9+
expect(minHeap.isEmpty()).toBe(true);
10+
});
11+
12+
it('should add items to the heap and heapify it up', () => {
13+
const minHeap = new MinHeapAdhoc();
14+
15+
minHeap.add(5);
16+
expect(minHeap.isEmpty()).toBe(false);
17+
expect(minHeap.peek()).toBe(5);
18+
expect(minHeap.toString()).toBe('5');
19+
20+
minHeap.add(3);
21+
expect(minHeap.peek()).toBe(3);
22+
expect(minHeap.toString()).toBe('3,5');
23+
24+
minHeap.add(10);
25+
expect(minHeap.peek()).toBe(3);
26+
expect(minHeap.toString()).toBe('3,5,10');
27+
28+
minHeap.add(1);
29+
expect(minHeap.peek()).toBe(1);
30+
expect(minHeap.toString()).toBe('1,3,10,5');
31+
32+
minHeap.add(1);
33+
expect(minHeap.peek()).toBe(1);
34+
expect(minHeap.toString()).toBe('1,1,10,5,3');
35+
36+
expect(minHeap.poll()).toBe(1);
37+
expect(minHeap.toString()).toBe('1,3,10,5');
38+
39+
expect(minHeap.poll()).toBe(1);
40+
expect(minHeap.toString()).toBe('3,5,10');
41+
42+
expect(minHeap.poll()).toBe(3);
43+
expect(minHeap.toString()).toBe('5,10');
44+
});
45+
46+
it('should poll items from the heap and heapify it down', () => {
47+
const minHeap = new MinHeapAdhoc();
48+
49+
minHeap.add(5);
50+
minHeap.add(3);
51+
minHeap.add(10);
52+
minHeap.add(11);
53+
minHeap.add(1);
54+
55+
expect(minHeap.toString()).toBe('1,3,10,11,5');
56+
57+
expect(minHeap.poll()).toBe(1);
58+
expect(minHeap.toString()).toBe('3,5,10,11');
59+
60+
expect(minHeap.poll()).toBe(3);
61+
expect(minHeap.toString()).toBe('5,11,10');
62+
63+
expect(minHeap.poll()).toBe(5);
64+
expect(minHeap.toString()).toBe('10,11');
65+
66+
expect(minHeap.poll()).toBe(10);
67+
expect(minHeap.toString()).toBe('11');
68+
69+
expect(minHeap.poll()).toBe(11);
70+
expect(minHeap.toString()).toBe('');
71+
72+
expect(minHeap.poll()).toBe(undefined);
73+
expect(minHeap.toString()).toBe('');
74+
});
75+
76+
it('should heapify down through the right branch as well', () => {
77+
const minHeap = new MinHeapAdhoc();
78+
79+
minHeap.add(3);
80+
minHeap.add(12);
81+
minHeap.add(10);
82+
83+
expect(minHeap.toString()).toBe('3,12,10');
84+
85+
minHeap.add(11);
86+
expect(minHeap.toString()).toBe('3,11,10,12');
87+
88+
expect(minHeap.poll()).toBe(3);
89+
expect(minHeap.toString()).toBe('10,11,12');
90+
});
91+
});

0 commit comments

Comments
 (0)
Please sign in to comment.