From 10e633f075cf3aa6833335025d12989eb10f85e3 Mon Sep 17 00:00:00 2001 From: Oleksii Kersov Date: Thu, 16 Aug 2018 18:18:19 +0300 Subject: [PATCH] Add MaxHeap (#167) * Add MaxHeap * Add parent class for MinHeap and MaxHeap --- src/data-structures/heap/Heap.js | 175 +++++++++++++++++ src/data-structures/heap/MaxHeap.js | 104 +++++++++++ src/data-structures/heap/MinHeap.js | 176 +----------------- .../heap/__test__/MaxHeap.test.js | 172 +++++++++++++++++ 4 files changed, 460 insertions(+), 167 deletions(-) create mode 100644 src/data-structures/heap/Heap.js create mode 100644 src/data-structures/heap/MaxHeap.js create mode 100644 src/data-structures/heap/__test__/MaxHeap.test.js diff --git a/src/data-structures/heap/Heap.js b/src/data-structures/heap/Heap.js new file mode 100644 index 00000000..cce76a17 --- /dev/null +++ b/src/data-structures/heap/Heap.js @@ -0,0 +1,175 @@ +import Comparator from '../../utils/comparator/Comparator'; + +/** + * Parent class for heaps + * @class + */ +class Heap { + /** + * @constructs Heap + * @param {Function} [comparatorFunction] + */ + constructor(comparatorFunction) { + // Array representation of the heap. + this.heapContainer = []; + this.compare = new Comparator(comparatorFunction); + } + + /** + * @param {number} parentIndex + * @return {number} + */ + getLeftChildIndex(parentIndex) { + return (2 * parentIndex) + 1; + } + + /** + * @param {number} parentIndex + * @return {number} + */ + getRightChildIndex(parentIndex) { + return (2 * parentIndex) + 2; + } + + /** + * @param {number} childIndex + * @return {number} + */ + getParentIndex(childIndex) { + return Math.floor((childIndex - 1) / 2); + } + + /** + * @param {number} childIndex + * @return {boolean} + */ + hasParent(childIndex) { + return this.getParentIndex(childIndex) >= 0; + } + + /** + * @param {number} parentIndex + * @return {boolean} + */ + hasLeftChild(parentIndex) { + return this.getLeftChildIndex(parentIndex) < this.heapContainer.length; + } + + /** + * @param {number} parentIndex + * @return {boolean} + */ + hasRightChild(parentIndex) { + return this.getRightChildIndex(parentIndex) < this.heapContainer.length; + } + + /** + * @param {number} parentIndex + * @return {*} + */ + leftChild(parentIndex) { + return this.heapContainer[this.getLeftChildIndex(parentIndex)]; + } + + /** + * @param {number} parentIndex + * @return {*} + */ + rightChild(parentIndex) { + return this.heapContainer[this.getRightChildIndex(parentIndex)]; + } + + /** + * @param {number} childIndex + * @return {*} + */ + parent(childIndex) { + return this.heapContainer[this.getParentIndex(childIndex)]; + } + + /** + * @param {number} indexOne + * @param {number} indexTwo + */ + swap(indexOne, indexTwo) { + const tmp = this.heapContainer[indexTwo]; + this.heapContainer[indexTwo] = this.heapContainer[indexOne]; + this.heapContainer[indexOne] = tmp; + } + + /** + * @return {*} + */ + peek() { + if (this.heapContainer.length === 0) { + return null; + } + + return this.heapContainer[0]; + } + + /** + * @return {*} + */ + poll() { + if (this.heapContainer.length === 0) { + return null; + } + + if (this.heapContainer.length === 1) { + return this.heapContainer.pop(); + } + + const item = this.heapContainer[0]; + + // Move the last element from the end to the head. + this.heapContainer[0] = this.heapContainer.pop(); + this.heapifyDown(); + + return item; + } + + /** + * @param {*} item + * @return {MinHeap} + */ + add(item) { + this.heapContainer.push(item); + this.heapifyUp(); + return this; + } + + /** + * @param {*} item + * @param {Comparator} [customComparator] + * @return {Number[]} + */ + find(item, customComparator) { + const foundItemIndices = []; + const comparator = customComparator || this.compare; + + for (let itemIndex = 0; itemIndex < this.heapContainer.length; itemIndex += 1) { + if (comparator.equal(item, this.heapContainer[itemIndex])) { + foundItemIndices.push(itemIndex); + } + } + + return foundItemIndices; + } + + /** + * @return {boolean} + */ + isEmpty() { + return !this.heapContainer.length; + } + + /** + * @return {string} + */ + toString() { + return this.heapContainer.toString(); + } +} + +export default Heap; diff --git a/src/data-structures/heap/MaxHeap.js b/src/data-structures/heap/MaxHeap.js new file mode 100644 index 00000000..abff4f36 --- /dev/null +++ b/src/data-structures/heap/MaxHeap.js @@ -0,0 +1,104 @@ +import Heap from './Heap'; + +/** + * Creates a new MaxHeap + * @class + * @augments Heap + */ +class MaxHeap extends Heap { + /** + * @param {*} item + * @param {Comparator} [customFindingComparator] + * @return {MaxHeap} + */ + remove(item, customFindingComparator) { + // Find number of items to remove. + const customComparator = customFindingComparator || this.compare; + const numberOfItemsToRemove = this.find(item, customComparator).length; + + for (let iteration = 0; iteration < numberOfItemsToRemove; iteration += 1) { + // We need to find item index to remove each time after removal since + // indices are being change after each heapify process. + const indexToRemove = this.find(item, customComparator).pop(); + + // If we need to remove last child in the heap then just remove it. + // There is no need to heapify the heap afterwards. + if (indexToRemove === (this.heapContainer.length - 1)) { + this.heapContainer.pop(); + } else { + // Move last element in heap to the vacant (removed) position. + this.heapContainer[indexToRemove] = this.heapContainer.pop(); + + // Get parent. + const parentItem = this.hasParent(indexToRemove) ? this.parent(indexToRemove) : null; + const leftChild = this.hasLeftChild(indexToRemove) ? this.leftChild(indexToRemove) : null; + + // If there is no parent or parent is greater then node to delete then heapify down. + // Otherwise heapify up. + if ( + leftChild !== null + && ( + parentItem === null + || this.compare.greaterThan(parentItem, this.heapContainer[indexToRemove]) + ) + ) { + this.heapifyDown(indexToRemove); + } else { + this.heapifyUp(indexToRemove); + } + } + } + + return this; + } + + /** + * @param {number} [customStartIndex] + */ + heapifyUp(customStartIndex) { + // Take last element (last in array or the bottom left in a tree) in + // a heap container and lift him up until we find the parent element + // that is greater then the current new one. + let currentIndex = customStartIndex || this.heapContainer.length - 1; + + while ( + this.hasParent(currentIndex) + && this.compare.greaterThan(this.heapContainer[currentIndex], this.parent(currentIndex)) + ) { + this.swap(currentIndex, this.getParentIndex(currentIndex)); + currentIndex = this.getParentIndex(currentIndex); + } + } + + /** + * @param {number} [customStartIndex] + */ + heapifyDown(customStartIndex) { + // Compare the root element to its children and swap root with the smallest + // of children. Do the same for next children after swap. + let currentIndex = customStartIndex || 0; + let nextIndex = null; + + while (this.hasLeftChild(currentIndex)) { + if ( + this.hasRightChild(currentIndex) + && this.compare.greaterThan(this.rightChild(currentIndex), this.leftChild(currentIndex)) + ) { + nextIndex = this.getRightChildIndex(currentIndex); + } else { + nextIndex = this.getLeftChildIndex(currentIndex); + } + + if ( + this.compare.greaterThan(this.heapContainer[currentIndex], this.heapContainer[nextIndex]) + ) { + break; + } + + this.swap(currentIndex, nextIndex); + currentIndex = nextIndex; + } + } +} + +export default MaxHeap; diff --git a/src/data-structures/heap/MinHeap.js b/src/data-structures/heap/MinHeap.js index 9c321d6e..9eed46fb 100644 --- a/src/data-structures/heap/MinHeap.js +++ b/src/data-structures/heap/MinHeap.js @@ -1,139 +1,11 @@ -import Comparator from '../../utils/comparator/Comparator'; - -export default class MinHeap { - /** - * @param {Function} [comparatorFunction] - */ - constructor(comparatorFunction) { - // Array representation of the heap. - this.heapContainer = []; - this.compare = new Comparator(comparatorFunction); - } - - /** - * @param {number} parentIndex - * @return {number} - */ - getLeftChildIndex(parentIndex) { - return (2 * parentIndex) + 1; - } - - /** - * @param {number} parentIndex - * @return {number} - */ - getRightChildIndex(parentIndex) { - return (2 * parentIndex) + 2; - } - - /** - * @param {number} childIndex - * @return {number} - */ - getParentIndex(childIndex) { - return Math.floor((childIndex - 1) / 2); - } - - /** - * @param {number} childIndex - * @return {boolean} - */ - hasParent(childIndex) { - return this.getParentIndex(childIndex) >= 0; - } - - /** - * @param {number} parentIndex - * @return {boolean} - */ - hasLeftChild(parentIndex) { - return this.getLeftChildIndex(parentIndex) < this.heapContainer.length; - } - - /** - * @param {number} parentIndex - * @return {boolean} - */ - hasRightChild(parentIndex) { - return this.getRightChildIndex(parentIndex) < this.heapContainer.length; - } - - /** - * @param {number} parentIndex - * @return {*} - */ - leftChild(parentIndex) { - return this.heapContainer[this.getLeftChildIndex(parentIndex)]; - } - - /** - * @param {number} parentIndex - * @return {*} - */ - rightChild(parentIndex) { - return this.heapContainer[this.getRightChildIndex(parentIndex)]; - } - - /** - * @param {number} childIndex - * @return {*} - */ - parent(childIndex) { - return this.heapContainer[this.getParentIndex(childIndex)]; - } - - /** - * @param {number} indexOne - * @param {number} indexTwo - */ - swap(indexOne, indexTwo) { - const tmp = this.heapContainer[indexTwo]; - this.heapContainer[indexTwo] = this.heapContainer[indexOne]; - this.heapContainer[indexOne] = tmp; - } - - /** - * @return {*} - */ - peek() { - if (this.heapContainer.length === 0) { - return null; - } - - return this.heapContainer[0]; - } - - /** - * @return {*} - */ - poll() { - if (this.heapContainer.length === 0) { - return null; - } - - if (this.heapContainer.length === 1) { - return this.heapContainer.pop(); - } - - const item = this.heapContainer[0]; - - // Move the last element from the end to the head. - this.heapContainer[0] = this.heapContainer.pop(); - this.heapifyDown(); - - return item; - } - - /** - * @param {*} item - * @return {MinHeap} - */ - add(item) { - this.heapContainer.push(item); - this.heapifyUp(); - return this; - } +import Heap from './Heap'; +/** + * Creates a new MinHeap + * @class + * @augments Heap + */ +class MinHeap extends Heap { /** * @param {*} item * @param {Comparator} [customFindingComparator] @@ -180,24 +52,6 @@ export default class MinHeap { return this; } - /** - * @param {*} item - * @param {Comparator} [customComparator] - * @return {Number[]} - */ - find(item, customComparator) { - const foundItemIndices = []; - const comparator = customComparator || this.compare; - - for (let itemIndex = 0; itemIndex < this.heapContainer.length; itemIndex += 1) { - if (comparator.equal(item, this.heapContainer[itemIndex])) { - foundItemIndices.push(itemIndex); - } - } - - return foundItemIndices; - } - /** * @param {number} [customStartIndex] */ @@ -243,18 +97,6 @@ export default class MinHeap { currentIndex = nextIndex; } } - - /** - * @return {boolean} - */ - isEmpty() { - return !this.heapContainer.length; - } - - /** - * @return {string} - */ - toString() { - return this.heapContainer.toString(); - } } + +export default MinHeap; diff --git a/src/data-structures/heap/__test__/MaxHeap.test.js b/src/data-structures/heap/__test__/MaxHeap.test.js new file mode 100644 index 00000000..c005e7d4 --- /dev/null +++ b/src/data-structures/heap/__test__/MaxHeap.test.js @@ -0,0 +1,172 @@ +import MaxHeap from '../MaxHeap'; +import Comparator from '../../../utils/comparator/Comparator'; + +describe('MaxHeap', () => { + it('should create an empty max heap', () => { + const maxHeap = new MaxHeap(); + + expect(maxHeap).toBeDefined(); + expect(maxHeap.peek()).toBeNull(); + 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()).toBeNull(); + 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'); + }); + + it('should be possible to find item indices in heap', () => { + const maxHeap = new MaxHeap(); + + maxHeap.add(3); + maxHeap.add(12); + maxHeap.add(10); + maxHeap.add(11); + maxHeap.add(11); + + expect(maxHeap.toString()).toBe('12,11,10,3,11'); + + expect(maxHeap.find(5)).toEqual([]); + expect(maxHeap.find(12)).toEqual([0]); + expect(maxHeap.find(11)).toEqual([1, 4]); + }); + + it('should be possible to remove items from heap with heapify down', () => { + const maxHeap = new MaxHeap(); + + maxHeap.add(3); + maxHeap.add(12); + maxHeap.add(10); + maxHeap.add(11); + maxHeap.add(11); + + expect(maxHeap.toString()).toBe('12,11,10,3,11'); + + expect(maxHeap.remove(12).toString()).toEqual('11,11,10,3'); + expect(maxHeap.remove(12).peek()).toEqual(11); + expect(maxHeap.remove(11).toString()).toEqual('10,3'); + expect(maxHeap.remove(10).peek()).toEqual(3); + }); + + it('should be possible to remove items from heap with heapify up', () => { + const maxHeap = new MaxHeap(); + + maxHeap.add(3); + maxHeap.add(10); + maxHeap.add(5); + maxHeap.add(6); + maxHeap.add(7); + maxHeap.add(4); + maxHeap.add(6); + maxHeap.add(8); + maxHeap.add(2); + maxHeap.add(1); + + expect(maxHeap.toString()).toBe('10,8,6,7,6,4,5,3,2,1'); + expect(maxHeap.remove(4).toString()).toEqual('10,8,6,7,6,1,5,3,2'); + expect(maxHeap.remove(3).toString()).toEqual('10,8,6,7,6,1,5,2'); + expect(maxHeap.remove(5).toString()).toEqual('10,8,6,7,6,1,2'); + expect(maxHeap.remove(10).toString()).toEqual('8,7,6,2,6,1'); + expect(maxHeap.remove(6).toString()).toEqual('8,7,1,2'); + expect(maxHeap.remove(2).toString()).toEqual('8,7,1'); + expect(maxHeap.remove(1).toString()).toEqual('8,7'); + expect(maxHeap.remove(7).toString()).toEqual('8'); + expect(maxHeap.remove(8).toString()).toEqual(''); + }); + + it('should be possible to remove items from heap with custom finding comparator', () => { + const maxHeap = new MaxHeap(); + maxHeap.add('a'); + maxHeap.add('bb'); + maxHeap.add('ccc'); + maxHeap.add('dddd'); + + expect(maxHeap.toString()).toBe('dddd,ccc,bb,a'); + + const comparator = new Comparator((a, b) => { + if (a.length === b.length) { + return 0; + } + + return a.length < b.length ? -1 : 1; + }); + + maxHeap.remove('hey', comparator); + expect(maxHeap.toString()).toBe('dddd,a,bb'); + }); +});