Last active
September 14, 2022 18:53
-
-
Save rinogo/24940fe6d650ed8bd54d17528ffd3dae to your computer and use it in GitHub Desktop.
Reliable and copy-pasteable JavaScript Heap/MinHeap/MaxHeap implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Source: https://gist.github.com/rinogo/24940fe6d650ed8bd54d17528ffd3dae | |
//Originally from: https://github.com/datastructures-js/heap | |
/** | |
* @license MIT | |
* @copyright 2020 Eyas Ranjous <[email protected]> | |
* | |
* @class | |
*/ | |
class Heap { | |
/** | |
* @param {function} compare | |
* @param {array} [_values] | |
* @param {number|string|object} [_leaf] | |
*/ | |
constructor(compare, _values, _leaf) { | |
if (typeof compare !== 'function') { | |
throw new Error('Heap constructor expects a compare function'); | |
} | |
this._compare = compare; | |
this._nodes = Array.isArray(_values) ? _values : []; | |
this._leaf = _leaf || null; | |
} | |
/** | |
* Checks if a parent has a left child | |
* @private | |
*/ | |
_hasLeftChild(parentIndex) { | |
const leftChildIndex = (parentIndex * 2) + 1; | |
return leftChildIndex < this.size(); | |
} | |
/** | |
* Checks if a parent has a right child | |
* @private | |
*/ | |
_hasRightChild(parentIndex) { | |
const rightChildIndex = (parentIndex * 2) + 2; | |
return rightChildIndex < this.size(); | |
} | |
/** | |
* Compares two nodes | |
* @private | |
*/ | |
_compareAt(i, j) { | |
return this._compare(this._nodes[i], this._nodes[j]); | |
} | |
/** | |
* Swaps two nodes in the heap | |
* @private | |
*/ | |
_swap(i, j) { | |
const temp = this._nodes[i]; | |
this._nodes[i] = this._nodes[j]; | |
this._nodes[j] = temp; | |
} | |
/** | |
* Checks if parent and child should be swapped | |
* @private | |
*/ | |
_shouldSwap(parentIndex, childIndex) { | |
if (parentIndex < 0 || parentIndex >= this.size()) { | |
return false; | |
} | |
if (childIndex < 0 || childIndex >= this.size()) { | |
return false; | |
} | |
return this._compareAt(parentIndex, childIndex) > 0; | |
} | |
/** | |
* Compares children of a parent | |
* @private | |
*/ | |
_compareChildrenOf(parentIndex) { | |
if (!this._hasLeftChild(parentIndex) && !this._hasRightChild(parentIndex)) { | |
return -1; | |
} | |
const leftChildIndex = (parentIndex * 2) + 1; | |
const rightChildIndex = (parentIndex * 2) + 2; | |
if (!this._hasLeftChild(parentIndex)) { | |
return rightChildIndex; | |
} | |
if (!this._hasRightChild(parentIndex)) { | |
return leftChildIndex; | |
} | |
const compare = this._compareAt(leftChildIndex, rightChildIndex); | |
return compare > 0 ? rightChildIndex : leftChildIndex; | |
} | |
/** | |
* Compares two children before a position | |
* @private | |
*/ | |
_compareChildrenBefore(index, leftChildIndex, rightChildIndex) { | |
const compare = this._compareAt(rightChildIndex, leftChildIndex); | |
if (compare <= 0 && rightChildIndex < index) { | |
return rightChildIndex; | |
} | |
return leftChildIndex; | |
} | |
/** | |
* Recursively bubbles up a node if it's in a wrong position | |
* @private | |
*/ | |
_heapifyUp(startIndex) { | |
let childIndex = startIndex; | |
let parentIndex = Math.floor((childIndex - 1) / 2); | |
while (this._shouldSwap(parentIndex, childIndex)) { | |
this._swap(parentIndex, childIndex); | |
childIndex = parentIndex; | |
parentIndex = Math.floor((childIndex - 1) / 2); | |
} | |
} | |
/** | |
* Recursively bubbles down a node if it's in a wrong position | |
* @private | |
*/ | |
_heapifyDown(startIndex) { | |
let parentIndex = startIndex; | |
let childIndex = this._compareChildrenOf(parentIndex); | |
while (this._shouldSwap(parentIndex, childIndex)) { | |
this._swap(parentIndex, childIndex); | |
parentIndex = childIndex; | |
childIndex = this._compareChildrenOf(parentIndex); | |
} | |
} | |
/** | |
* Recursively bubbles down a node before a given index | |
* @private | |
*/ | |
_heapifyDownUntil(index) { | |
let parentIndex = 0; | |
let leftChildIndex = 1; | |
let rightChildIndex = 2; | |
let childIndex; | |
while (leftChildIndex < index) { | |
childIndex = this._compareChildrenBefore( | |
index, | |
leftChildIndex, | |
rightChildIndex | |
); | |
if (this._shouldSwap(parentIndex, childIndex)) { | |
this._swap(parentIndex, childIndex); | |
} | |
parentIndex = childIndex; | |
leftChildIndex = (parentIndex * 2) + 1; | |
rightChildIndex = (parentIndex * 2) + 2; | |
} | |
} | |
/** | |
* Inserts a new value into the heap | |
* @public | |
* @param {number|string|object} value | |
* @returns {Heap} | |
*/ | |
insert(value) { | |
this._nodes.push(value); | |
this._heapifyUp(this.size() - 1); | |
if (this._leaf === null || this._compare(value, this._leaf) > 0) { | |
this._leaf = value; | |
} | |
return this; | |
} | |
/** | |
* Inserts a new value into the heap | |
* @public | |
* @param {number|string|object} value | |
* @returns {Heap} | |
*/ | |
push(value) { | |
return this.insert(value); | |
} | |
/** | |
* Removes and returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
extractRoot() { | |
if (this.isEmpty()) { | |
return null; | |
} | |
const root = this.root(); | |
this._nodes[0] = this._nodes[this.size() - 1]; | |
this._nodes.pop(); | |
this._heapifyDown(0); | |
if (root === this._leaf) { | |
this._leaf = this.root(); | |
} | |
return root; | |
} | |
/** | |
* Removes and returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
pop() { | |
return this.extractRoot(); | |
} | |
/** | |
* Applies heap sort and return the values sorted by priority | |
* @public | |
* @returns {array} | |
*/ | |
sort() { | |
for (let i = this.size() - 1; i > 0; i -= 1) { | |
this._swap(0, i); | |
this._heapifyDownUntil(i); | |
} | |
return this._nodes; | |
} | |
/** | |
* Fixes node positions in the heap | |
* @public | |
* @returns {Heap} | |
*/ | |
fix() { | |
for (let i = Math.floor(this.size() / 2) - 1; i >= 0; i -= 1) { | |
this._heapifyDown(i); | |
} | |
return this; | |
} | |
/** | |
* Verifies that all heap nodes are in the right position | |
* @public | |
* @returns {boolean} | |
*/ | |
isValid() { | |
const isValidRecursive = (parentIndex) => { | |
let isValidLeft = true; | |
let isValidRight = true; | |
if (this._hasLeftChild(parentIndex)) { | |
const leftChildIndex = (parentIndex * 2) + 1; | |
if (this._compareAt(parentIndex, leftChildIndex) > 0) { | |
return false; | |
} | |
isValidLeft = isValidRecursive(leftChildIndex); | |
} | |
if (this._hasRightChild(parentIndex)) { | |
const rightChildIndex = (parentIndex * 2) + 2; | |
if (this._compareAt(parentIndex, rightChildIndex) > 0) { | |
return false; | |
} | |
isValidRight = isValidRecursive(rightChildIndex); | |
} | |
return isValidLeft && isValidRight; | |
}; | |
return isValidRecursive(0); | |
} | |
/** | |
* Returns a shallow copy of the heap | |
* @public | |
* @returns {Heap} | |
*/ | |
clone() { | |
return new Heap(this._compare, this._nodes.slice(), this._leaf); | |
} | |
/** | |
* Returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
root() { | |
if (this.isEmpty()) { | |
return null; | |
} | |
return this._nodes[0]; | |
} | |
/** | |
* Returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
top() { | |
return this.root(); | |
} | |
/** | |
* Returns a leaf node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
leaf() { | |
return this._leaf; | |
} | |
/** | |
* Returns the number of nodes in the heap | |
* @public | |
* @returns {number} | |
*/ | |
size() { | |
return this._nodes.length; | |
} | |
/** | |
* Checks if the heap is empty | |
* @public | |
* @returns {boolean} | |
*/ | |
isEmpty() { | |
return this.size() === 0; | |
} | |
/** | |
* Clears the heap | |
* @public | |
*/ | |
clear() { | |
this._nodes = []; | |
this._leaf = null; | |
} | |
/** | |
* Builds a heap from a array of values | |
* @public | |
* @static | |
* @param {array} values | |
* @param {function} compare | |
* @returns {Heap} | |
*/ | |
static heapify(values, compare) { | |
if (!Array.isArray(values)) { | |
throw new Error('Heap.heapify expects an array of values'); | |
} | |
if (typeof compare !== 'function') { | |
throw new Error('Heap.heapify expects a compare function'); | |
} | |
return new Heap(compare, values).fix(); | |
} | |
/** | |
* Checks if a list of values is a valid heap | |
* @public | |
* @static | |
* @param {array} values | |
* @param {function} compare | |
* @returns {boolean} | |
*/ | |
static isHeapified(values, compare) { | |
return new Heap(compare, values).isValid(); | |
} | |
} | |
exports.Heap = Heap; | |
/** | |
* @license MIT | |
* @copyright 2020 Eyas Ranjous <[email protected]> | |
*/ | |
const getMaxCompare = (getCompareValue) => (a, b) => { | |
const aVal = typeof getCompareValue === 'function' ? getCompareValue(a) : a; | |
const bVal = typeof getCompareValue === 'function' ? getCompareValue(b) : b; | |
return aVal < bVal ? 1 : -1; | |
}; | |
/** | |
* @class MaxHeap | |
* @extends Heap | |
*/ | |
class MaxHeap { | |
/** | |
* @param {function} [getCompareValue] | |
* @param {Heap} [_heap] | |
*/ | |
constructor(getCompareValue, _heap) { | |
this._getCompareValue = getCompareValue; | |
this._heap = _heap || new Heap(getMaxCompare(getCompareValue)); | |
} | |
/** | |
* Inserts a new value into the heap | |
* @public | |
* @param {number|string|object} value | |
* @returns {MaxHeap} | |
*/ | |
insert(value) { | |
return this._heap.insert(value); | |
} | |
/** | |
* Inserts a new value into the heap | |
* @public | |
* @param {number|string|object} value | |
* @returns {Heap} | |
*/ | |
push(value) { | |
return this.insert(value); | |
} | |
/** | |
* Removes and returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
extractRoot() { | |
return this._heap.extractRoot(); | |
} | |
/** | |
* Removes and returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
pop() { | |
return this.extractRoot(); | |
} | |
/** | |
* Applies heap sort and return the values sorted by priority | |
* @public | |
* @returns {array} | |
*/ | |
sort() { | |
return this._heap.sort(); | |
} | |
/** | |
* Fixes node positions in the heap | |
* @public | |
* @returns {MaxHeap} | |
*/ | |
fix() { | |
return this._heap.fix(); | |
} | |
/** | |
* Verifies that all heap nodes are in the right position | |
* @public | |
* @returns {boolean} | |
*/ | |
isValid() { | |
return this._heap.isValid(); | |
} | |
/** | |
* Returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
root() { | |
return this._heap.root(); | |
} | |
/** | |
* Returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
top() { | |
return this.root(); | |
} | |
/** | |
* Returns a leaf node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
leaf() { | |
return this._heap.leaf(); | |
} | |
/** | |
* Returns the number of nodes in the heap | |
* @public | |
* @returns {number} | |
*/ | |
size() { | |
return this._heap.size(); | |
} | |
/** | |
* Checks if the heap is empty | |
* @public | |
* @returns {boolean} | |
*/ | |
isEmpty() { | |
return this._heap.isEmpty(); | |
} | |
/** | |
* Clears the heap | |
* @public | |
*/ | |
clear() { | |
this._heap.clear(); | |
} | |
/** | |
* Returns a shallow copy of the MaxHeap | |
* @public | |
* @returns {MaxHeap} | |
*/ | |
clone() { | |
return new MaxHeap(this._getCompareValue, this._heap.clone()); | |
} | |
/** | |
* Builds a MaxHeap from an array | |
* @public | |
* @static | |
* @param {array} values | |
* @param {function} [getCompareValue] | |
* @returns {MaxHeap} | |
*/ | |
static heapify(values, getCompareValue) { | |
if (!Array.isArray(values)) { | |
throw new Error('MaxHeap.heapify expects an array'); | |
} | |
const heap = new Heap(getMaxCompare(getCompareValue), values); | |
return new MaxHeap(getCompareValue, heap).fix(); | |
} | |
/** | |
* Checks if a list of values is a valid max heap | |
* @public | |
* @static | |
* @param {array} values | |
* @param {function} [getCompareValue] | |
* @returns {boolean} | |
*/ | |
static isHeapified(values, getCompareValue) { | |
const heap = new Heap(getMaxCompare(getCompareValue), values); | |
return new MaxHeap(getCompareValue, heap).isValid(); | |
} | |
} | |
exports.MaxHeap = MaxHeap; | |
/** | |
* @license MIT | |
* @copyright 2020 Eyas Ranjous <[email protected]> | |
*/ | |
const getMinCompare = (getCompareValue) => (a, b) => { | |
const aVal = typeof getCompareValue === 'function' ? getCompareValue(a) : a; | |
const bVal = typeof getCompareValue === 'function' ? getCompareValue(b) : b; | |
return aVal < bVal ? -1 : 1; | |
}; | |
/** | |
* @class MinHeap | |
* @extends Heap | |
*/ | |
class MinHeap { | |
/** | |
* @param {function} [getCompareValue] | |
* @param {Heap} [_heap] | |
*/ | |
constructor(getCompareValue, _heap) { | |
this._getCompareValue = getCompareValue; | |
this._heap = _heap || new Heap(getMinCompare(getCompareValue)); | |
} | |
/** | |
* Inserts a new value into the heap | |
* @public | |
* @param {number|string|object} value | |
* @returns {MinHeap} | |
*/ | |
insert(value) { | |
return this._heap.insert(value); | |
} | |
/** | |
* Inserts a new value into the heap | |
* @public | |
* @param {number|string|object} value | |
* @returns {Heap} | |
*/ | |
push(value) { | |
return this.insert(value); | |
} | |
/** | |
* Removes and returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
extractRoot() { | |
return this._heap.extractRoot(); | |
} | |
/** | |
* Removes and returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
pop() { | |
return this.extractRoot(); | |
} | |
/** | |
* Applies heap sort and return the values sorted by priority | |
* @public | |
* @returns {array} | |
*/ | |
sort() { | |
return this._heap.sort(); | |
} | |
/** | |
* Fixes node positions in the heap | |
* @public | |
* @returns {MinHeap} | |
*/ | |
fix() { | |
return this._heap.fix(); | |
} | |
/** | |
* Verifies that all heap nodes are in the right position | |
* @public | |
* @returns {boolean} | |
*/ | |
isValid() { | |
return this._heap.isValid(); | |
} | |
/** | |
* Returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
root() { | |
return this._heap.root(); | |
} | |
/** | |
* Returns the root node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
top() { | |
return this.root(); | |
} | |
/** | |
* Returns a leaf node in the heap | |
* @public | |
* @returns {number|string|object} | |
*/ | |
leaf() { | |
return this._heap.leaf(); | |
} | |
/** | |
* Returns the number of nodes in the heap | |
* @public | |
* @returns {number} | |
*/ | |
size() { | |
return this._heap.size(); | |
} | |
/** | |
* Checks if the heap is empty | |
* @public | |
* @returns {boolean} | |
*/ | |
isEmpty() { | |
return this._heap.isEmpty(); | |
} | |
/** | |
* Clears the heap | |
* @public | |
*/ | |
clear() { | |
this._heap.clear(); | |
} | |
/** | |
* Returns a shallow copy of the MinHeap | |
* @public | |
* @returns {MinHeap} | |
*/ | |
clone() { | |
return new MinHeap(this._getCompareValue, this._heap.clone()); | |
} | |
/** | |
* Builds a MinHeap from an array | |
* @public | |
* @static | |
* @param {array} values | |
* @param {function} [getCompareValue] | |
* @returns {MinHeap} | |
*/ | |
static heapify(values, getCompareValue) { | |
if (!Array.isArray(values)) { | |
throw new Error('MinHeap.heapify expects an array'); | |
} | |
const heap = new Heap(getMinCompare(getCompareValue), values); | |
return new MinHeap(getCompareValue, heap).fix(); | |
} | |
/** | |
* Checks if a list of values is a valid min heap | |
* @public | |
* @static | |
* @param {array} values | |
* @param {function} [getCompareValue] | |
* @returns {boolean} | |
*/ | |
static isHeapified(values, getCompareValue) { | |
const heap = new Heap(getMinCompare(getCompareValue), values); | |
return new MinHeap(getCompareValue, heap).isValid(); | |
} | |
} | |
exports.MinHeap = MinHeap; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment