diff --git a/src/data-structures/disjoint-set/DisjointSetAdhoc.js b/src/data-structures/disjoint-set/DisjointSetAdhoc.js new file mode 100644 index 00000000..96534180 --- /dev/null +++ b/src/data-structures/disjoint-set/DisjointSetAdhoc.js @@ -0,0 +1,78 @@ +/** + * 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). + * + * Time Complexity: + * + * - Constructor: O(N) + * - Find: O(α(N)) + * - Union: O(α(N)) + * - Connected: O(α(N)) + * + * Where N is the number of vertices in the graph. + * α refers to the Inverse Ackermann function. + * In practice, we assume it's a constant. + * In other words, O(α(N)) is regarded as O(1) on average. + */ +class DisjointSetAdhoc { + /** + * Initializes the set of specified size. + * @param {number} size + */ + constructor(size) { + // The index of a cell is an id of the node in a set. + // The value of a cell is an id (index) of the root node. + // By default, the node is a parent of itself. + this.roots = new Array(size).fill(0).map((_, i) => i); + + // Using the heights array to record the height of each node. + // By default each node has a height of 1 because it has no children. + this.heights = new Array(size).fill(1); + } + + /** + * Finds the root of node `a` + * @param {number} a + * @returns {number} + */ + find(a) { + if (a === this.roots[a]) return a; + this.roots[a] = this.find(this.roots[a]); + return this.roots[a]; + } + + /** + * Joins the `a` and `b` nodes into same set. + * @param {number} a + * @param {number} b + * @returns {number} + */ + union(a, b) { + const aRoot = this.find(a); + const bRoot = this.find(b); + + if (aRoot === bRoot) return; + + if (this.heights[aRoot] > this.heights[bRoot]) { + this.roots[bRoot] = aRoot; + } else if (this.heights[aRoot] < this.heights[bRoot]) { + this.roots[aRoot] = bRoot; + } else { + this.roots[bRoot] = aRoot; + this.heights[aRoot] += 1; + } + } + + /** + * Checks if `a` and `b` belong to the same set. + * @param {number} a + * @param {number} b + */ + connected(a, b) { + return this.find(a) === this.find(b); + } +} + +export default DisjointSetAdhoc; diff --git a/src/data-structures/disjoint-set/README.md b/src/data-structures/disjoint-set/README.md index 44bcfa27..772f6819 100644 --- a/src/data-structures/disjoint-set/README.md +++ b/src/data-structures/disjoint-set/README.md @@ -19,6 +19,11 @@ _MakeSet_ creates 8 singletons. After some operations of _Union_, some sets are grouped together. +## Implementation + +- [DisjointSet.js](./DisjointSet.js) +- [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). + ## References - [Wikipedia](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) diff --git a/src/data-structures/disjoint-set/__test__/DisjointSetAdhoc.test.js b/src/data-structures/disjoint-set/__test__/DisjointSetAdhoc.test.js new file mode 100644 index 00000000..f7be0fcd --- /dev/null +++ b/src/data-structures/disjoint-set/__test__/DisjointSetAdhoc.test.js @@ -0,0 +1,50 @@ +import DisjointSetAdhoc from '../DisjointSetAdhoc'; + +describe('DisjointSetAdhoc', () => { + it('should create unions and find connected elements', () => { + const set = new DisjointSetAdhoc(10); + + // 1-2-5-6-7 3-8-9 4 + set.union(1, 2); + set.union(2, 5); + set.union(5, 6); + set.union(6, 7); + + set.union(3, 8); + set.union(8, 9); + + expect(set.connected(1, 5)).toBe(true); + expect(set.connected(5, 7)).toBe(true); + expect(set.connected(3, 8)).toBe(true); + + expect(set.connected(4, 9)).toBe(false); + expect(set.connected(4, 7)).toBe(false); + + // 1-2-5-6-7 3-8-9-4 + set.union(9, 4); + + expect(set.connected(4, 9)).toBe(true); + expect(set.connected(4, 3)).toBe(true); + expect(set.connected(8, 4)).toBe(true); + + expect(set.connected(8, 7)).toBe(false); + expect(set.connected(2, 3)).toBe(false); + }); + + it('should keep the height of the tree small', () => { + const set = new DisjointSetAdhoc(10); + + // 1-2-6-7-9 1 3 4 5 + set.union(7, 6); + set.union(1, 2); + set.union(2, 6); + set.union(1, 7); + set.union(9, 1); + + expect(set.connected(1, 7)).toBe(true); + expect(set.connected(6, 9)).toBe(true); + expect(set.connected(4, 9)).toBe(false); + + expect(Math.max(...set.heights)).toBe(3); + }); +}); diff --git a/src/data-structures/heap/MaxHeapAdhoc.js b/src/data-structures/heap/MaxHeapAdhoc.js new file mode 100644 index 00000000..b9d69c59 --- /dev/null +++ b/src/data-structures/heap/MaxHeapAdhoc.js @@ -0,0 +1,115 @@ +/** + * The minimalistic (ad hoc) version of a 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). + */ +class MaxHeapAdhoc { + constructor(heap = []) { + this.heap = []; + heap.forEach(this.add); + } + + add(num) { + this.heap.push(num); + this.heapifyUp(); + } + + peek() { + return this.heap[0]; + } + + poll() { + if (this.heap.length === 0) return undefined; + const top = this.heap[0]; + this.heap[0] = this.heap[this.heap.length - 1]; + this.heap.pop(); + this.heapifyDown(); + return top; + } + + isEmpty() { + return this.heap.length === 0; + } + + toString() { + return this.heap.join(','); + } + + heapifyUp() { + let nodeIndex = this.heap.length - 1; + while (nodeIndex > 0) { + const parentIndex = this.getParentIndex(nodeIndex); + if (this.heap[parentIndex] >= this.heap[nodeIndex]) break; + this.swap(parentIndex, nodeIndex); + nodeIndex = parentIndex; + } + } + + heapifyDown() { + let nodeIndex = 0; + + while ( + ( + this.hasLeftChild(nodeIndex) && this.heap[nodeIndex] < this.leftChild(nodeIndex) + ) + || ( + this.hasRightChild(nodeIndex) && this.heap[nodeIndex] < this.rightChild(nodeIndex) + ) + ) { + const leftIndex = this.getLeftChildIndex(nodeIndex); + const rightIndex = this.getRightChildIndex(nodeIndex); + const left = this.leftChild(nodeIndex); + const right = this.rightChild(nodeIndex); + + if (this.hasLeftChild(nodeIndex) && this.hasRightChild(nodeIndex)) { + if (left >= right) { + this.swap(leftIndex, nodeIndex); + nodeIndex = leftIndex; + } else { + this.swap(rightIndex, nodeIndex); + nodeIndex = rightIndex; + } + } else if (this.hasLeftChild(nodeIndex)) { + this.swap(leftIndex, nodeIndex); + nodeIndex = leftIndex; + } + } + } + + getLeftChildIndex(parentIndex) { + return (2 * parentIndex) + 1; + } + + getRightChildIndex(parentIndex) { + return (2 * parentIndex) + 2; + } + + getParentIndex(childIndex) { + return Math.floor((childIndex - 1) / 2); + } + + hasLeftChild(parentIndex) { + return this.getLeftChildIndex(parentIndex) < this.heap.length; + } + + hasRightChild(parentIndex) { + return this.getRightChildIndex(parentIndex) < this.heap.length; + } + + leftChild(parentIndex) { + return this.heap[this.getLeftChildIndex(parentIndex)]; + } + + rightChild(parentIndex) { + return this.heap[this.getRightChildIndex(parentIndex)]; + } + + swap(indexOne, indexTwo) { + const tmp = this.heap[indexTwo]; + this.heap[indexTwo] = this.heap[indexOne]; + this.heap[indexOne] = tmp; + } +} + +export default MaxHeapAdhoc; diff --git a/src/data-structures/heap/MinHeapAdhoc.js b/src/data-structures/heap/MinHeapAdhoc.js new file mode 100644 index 00000000..c70692f3 --- /dev/null +++ b/src/data-structures/heap/MinHeapAdhoc.js @@ -0,0 +1,117 @@ +/** + * The minimalistic (ad hoc) version of a MinHeap 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). + */ +class MinHeapAdhoc { + constructor(heap = []) { + this.heap = []; + heap.forEach(this.add); + } + + add(num) { + this.heap.push(num); + this.heapifyUp(); + } + + peek() { + return this.heap[0]; + } + + poll() { + if (this.heap.length === 0) return undefined; + const top = this.heap[0]; + this.heap[0] = this.heap[this.heap.length - 1]; + this.heap.pop(); + this.heapifyDown(); + return top; + } + + isEmpty() { + return this.heap.length === 0; + } + + toString() { + return this.heap.join(','); + } + + heapifyUp() { + let nodeIndex = this.heap.length - 1; + while (nodeIndex > 0) { + const parentIndex = this.getParentIndex(nodeIndex); + if (this.heap[parentIndex] <= this.heap[nodeIndex]) break; + this.swap(parentIndex, nodeIndex); + nodeIndex = parentIndex; + } + } + + heapifyDown() { + let nodeIndex = 0; + + while ( + ( + this.hasLeftChild(nodeIndex) + && this.heap[nodeIndex] > this.leftChild(nodeIndex) + ) + || ( + this.hasRightChild(nodeIndex) + && this.heap[nodeIndex] > this.rightChild(nodeIndex) + ) + ) { + const leftIndex = this.getLeftChildIndex(nodeIndex); + const rightIndex = this.getRightChildIndex(nodeIndex); + const left = this.leftChild(nodeIndex); + const right = this.rightChild(nodeIndex); + + if (this.hasLeftChild(nodeIndex) && this.hasRightChild(nodeIndex)) { + if (left <= right) { + this.swap(leftIndex, nodeIndex); + nodeIndex = leftIndex; + } else { + this.swap(rightIndex, nodeIndex); + nodeIndex = rightIndex; + } + } else if (this.hasLeftChild(nodeIndex)) { + this.swap(leftIndex, nodeIndex); + nodeIndex = leftIndex; + } + } + } + + getLeftChildIndex(parentIndex) { + return 2 * parentIndex + 1; + } + + getRightChildIndex(parentIndex) { + return 2 * parentIndex + 2; + } + + getParentIndex(childIndex) { + return Math.floor((childIndex - 1) / 2); + } + + hasLeftChild(parentIndex) { + return this.getLeftChildIndex(parentIndex) < this.heap.length; + } + + hasRightChild(parentIndex) { + return this.getRightChildIndex(parentIndex) < this.heap.length; + } + + leftChild(parentIndex) { + return this.heap[this.getLeftChildIndex(parentIndex)]; + } + + rightChild(parentIndex) { + return this.heap[this.getRightChildIndex(parentIndex)]; + } + + swap(indexOne, indexTwo) { + const tmp = this.heap[indexTwo]; + this.heap[indexTwo] = this.heap[indexOne]; + this.heap[indexOne] = tmp; + } +} + +export default MinHeapAdhoc; diff --git a/src/data-structures/heap/README.md b/src/data-structures/heap/README.md index 824d6460..392c5c96 100644 --- a/src/data-structures/heap/README.md +++ b/src/data-structures/heap/README.md @@ -58,6 +58,11 @@ Where: > In this repository, the [MaxHeap.js](./MaxHeap.js) and [MinHeap.js](./MinHeap.js) are examples of the **Binary** heap. +## Implementation + +- [MaxHeap.js](./MaxHeap.js) and [MinHeap.js](./MinHeap.js) +- [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). + ## References - [Wikipedia](https://en.wikipedia.org/wiki/Heap_(data_structure)) diff --git a/src/data-structures/heap/__test__/MaxHeapAdhoc.test.js b/src/data-structures/heap/__test__/MaxHeapAdhoc.test.js new file mode 100644 index 00000000..0217a98d --- /dev/null +++ b/src/data-structures/heap/__test__/MaxHeapAdhoc.test.js @@ -0,0 +1,91 @@ +import MaxHeap from '../MaxHeapAdhoc'; + +describe('MaxHeapAdhoc', () => { + it('should create an empty max heap', () => { + const maxHeap = new MaxHeap(); + + expect(maxHeap).toBeDefined(); + expect(maxHeap.peek()).toBe(undefined); + expect(maxHeap.isEmpty()).toBe(true); + }); + + it('should add items to the heap and heapify it up', () => { + const maxHeap = new MaxHeap(); + + maxHeap.add(5); + expect(maxHeap.isEmpty()).toBe(false); + expect(maxHeap.peek()).toBe(5); + expect(maxHeap.toString()).toBe('5'); + + maxHeap.add(3); + expect(maxHeap.peek()).toBe(5); + expect(maxHeap.toString()).toBe('5,3'); + + maxHeap.add(10); + expect(maxHeap.peek()).toBe(10); + expect(maxHeap.toString()).toBe('10,3,5'); + + maxHeap.add(1); + expect(maxHeap.peek()).toBe(10); + expect(maxHeap.toString()).toBe('10,3,5,1'); + + maxHeap.add(1); + expect(maxHeap.peek()).toBe(10); + expect(maxHeap.toString()).toBe('10,3,5,1,1'); + + expect(maxHeap.poll()).toBe(10); + expect(maxHeap.toString()).toBe('5,3,1,1'); + + expect(maxHeap.poll()).toBe(5); + expect(maxHeap.toString()).toBe('3,1,1'); + + expect(maxHeap.poll()).toBe(3); + expect(maxHeap.toString()).toBe('1,1'); + }); + + it('should poll items from the heap and heapify it down', () => { + const maxHeap = new MaxHeap(); + + maxHeap.add(5); + maxHeap.add(3); + maxHeap.add(10); + maxHeap.add(11); + maxHeap.add(1); + + expect(maxHeap.toString()).toBe('11,10,5,3,1'); + + expect(maxHeap.poll()).toBe(11); + expect(maxHeap.toString()).toBe('10,3,5,1'); + + expect(maxHeap.poll()).toBe(10); + expect(maxHeap.toString()).toBe('5,3,1'); + + expect(maxHeap.poll()).toBe(5); + expect(maxHeap.toString()).toBe('3,1'); + + expect(maxHeap.poll()).toBe(3); + expect(maxHeap.toString()).toBe('1'); + + expect(maxHeap.poll()).toBe(1); + expect(maxHeap.toString()).toBe(''); + + expect(maxHeap.poll()).toBe(undefined); + expect(maxHeap.toString()).toBe(''); + }); + + it('should heapify down through the right branch as well', () => { + const maxHeap = new MaxHeap(); + + maxHeap.add(3); + maxHeap.add(12); + maxHeap.add(10); + + expect(maxHeap.toString()).toBe('12,3,10'); + + maxHeap.add(11); + expect(maxHeap.toString()).toBe('12,11,10,3'); + + expect(maxHeap.poll()).toBe(12); + expect(maxHeap.toString()).toBe('11,3,10'); + }); +}); diff --git a/src/data-structures/heap/__test__/MinHeapAdhoc.test.js b/src/data-structures/heap/__test__/MinHeapAdhoc.test.js new file mode 100644 index 00000000..766b307f --- /dev/null +++ b/src/data-structures/heap/__test__/MinHeapAdhoc.test.js @@ -0,0 +1,91 @@ +import MinHeapAdhoc from '../MinHeapAdhoc'; + +describe('MinHeapAdhoc', () => { + it('should create an empty min heap', () => { + const minHeap = new MinHeapAdhoc(); + + expect(minHeap).toBeDefined(); + expect(minHeap.peek()).toBe(undefined); + expect(minHeap.isEmpty()).toBe(true); + }); + + it('should add items to the heap and heapify it up', () => { + const minHeap = new MinHeapAdhoc(); + + minHeap.add(5); + expect(minHeap.isEmpty()).toBe(false); + expect(minHeap.peek()).toBe(5); + expect(minHeap.toString()).toBe('5'); + + minHeap.add(3); + expect(minHeap.peek()).toBe(3); + expect(minHeap.toString()).toBe('3,5'); + + minHeap.add(10); + expect(minHeap.peek()).toBe(3); + expect(minHeap.toString()).toBe('3,5,10'); + + minHeap.add(1); + expect(minHeap.peek()).toBe(1); + expect(minHeap.toString()).toBe('1,3,10,5'); + + minHeap.add(1); + expect(minHeap.peek()).toBe(1); + expect(minHeap.toString()).toBe('1,1,10,5,3'); + + expect(minHeap.poll()).toBe(1); + expect(minHeap.toString()).toBe('1,3,10,5'); + + expect(minHeap.poll()).toBe(1); + expect(minHeap.toString()).toBe('3,5,10'); + + expect(minHeap.poll()).toBe(3); + expect(minHeap.toString()).toBe('5,10'); + }); + + it('should poll items from the heap and heapify it down', () => { + const minHeap = new MinHeapAdhoc(); + + minHeap.add(5); + minHeap.add(3); + minHeap.add(10); + minHeap.add(11); + minHeap.add(1); + + expect(minHeap.toString()).toBe('1,3,10,11,5'); + + expect(minHeap.poll()).toBe(1); + expect(minHeap.toString()).toBe('3,5,10,11'); + + expect(minHeap.poll()).toBe(3); + expect(minHeap.toString()).toBe('5,11,10'); + + expect(minHeap.poll()).toBe(5); + expect(minHeap.toString()).toBe('10,11'); + + expect(minHeap.poll()).toBe(10); + expect(minHeap.toString()).toBe('11'); + + expect(minHeap.poll()).toBe(11); + expect(minHeap.toString()).toBe(''); + + expect(minHeap.poll()).toBe(undefined); + expect(minHeap.toString()).toBe(''); + }); + + it('should heapify down through the right branch as well', () => { + const minHeap = new MinHeapAdhoc(); + + minHeap.add(3); + minHeap.add(12); + minHeap.add(10); + + expect(minHeap.toString()).toBe('3,12,10'); + + minHeap.add(11); + expect(minHeap.toString()).toBe('3,11,10,12'); + + expect(minHeap.poll()).toBe(3); + expect(minHeap.toString()).toBe('10,11,12'); + }); +});