├── .gitignore ├── README.md ├── .npmignore ├── index.js ├── index.d.ts ├── .eslintrc ├── src ├── minHeap.d.ts ├── maxHeap.d.ts ├── heap.d.ts ├── maxHeap.js ├── minHeap.js └── heap.js ├── .github └── workflows │ └── npm-grunt.yml ├── Gruntfile.js ├── LICENSE ├── package.json ├── test ├── readme.ts ├── maxHeap.test.js ├── minHeap.test.js └── heap.test.js └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_Store 4 | .idea/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @datastructures-js/heap 2 | 3 | ## Docs 4 | https://datastructures-js.info/docs/heap 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | .travis.yml 3 | .gitignore 4 | .eslintrc 5 | Gruntfile.js 6 | coverage/ 7 | node_modules/ 8 | test/ 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Heap } = require('./src/heap'); 2 | const { MinHeap } = require('./src/minHeap'); 3 | const { MaxHeap } = require('./src/maxHeap'); 4 | 5 | exports.Heap = Heap; 6 | exports.MinHeap = MinHeap; 7 | exports.MaxHeap = MaxHeap; 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Heap, ICompare } from './src/heap'; 2 | import { MinHeap } from './src/minHeap'; 3 | import { MaxHeap, IGetCompareValue } from './src/maxHeap'; 4 | 5 | export { Heap } 6 | export { ICompare } 7 | export { IGetCompareValue } 8 | export { MinHeap } 9 | export { MaxHeap } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "max-len": ["error", { "code": 100, "ignoreComments": true }], 4 | "comma-dangle": ["error", { 5 | "functions": "ignore" 6 | }], 7 | "no-underscore-dangle": 0, 8 | "class-methods-use-this": 0, 9 | "no-restricted-syntax": 0 10 | }, 11 | "env": { 12 | "mocha": true, 13 | "node": true 14 | }, 15 | "extends": ["airbnb-base"] 16 | } 17 | -------------------------------------------------------------------------------- /src/minHeap.d.ts: -------------------------------------------------------------------------------- 1 | import { Heap } from "./heap"; 2 | import { IGetCompareValue } from "./maxHeap"; 3 | 4 | export class MinHeap extends Heap { 5 | constructor(getCompareValue?: IGetCompareValue, values?: T[]); 6 | insert(value: T): MinHeap; 7 | push(value: T): MinHeap; 8 | fix(): MinHeap; 9 | clone(): MinHeap; 10 | static heapify( 11 | values: T[], 12 | getCompareValue?: IGetCompareValue 13 | ): MinHeap; 14 | static isHeapified( 15 | values: T[], 16 | getCompareValue?: IGetCompareValue 17 | ): boolean; 18 | } 19 | -------------------------------------------------------------------------------- /src/maxHeap.d.ts: -------------------------------------------------------------------------------- 1 | import { Heap } from "./heap"; 2 | 3 | export interface IGetCompareValue { 4 | (value: T): number | string; 5 | } 6 | 7 | export class MaxHeap extends Heap { 8 | constructor(getCompareValue?: IGetCompareValue, values?: T[]); 9 | insert(value: T): MaxHeap; 10 | push(value: T): MaxHeap; 11 | fix(): MaxHeap; 12 | clone(): MaxHeap; 13 | static heapify( 14 | values: T[], 15 | getCompareValue?: IGetCompareValue 16 | ): MaxHeap; 17 | static isHeapified( 18 | values: T[], 19 | getCompareValue?: IGetCompareValue 20 | ): boolean; 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/npm-grunt.yml: -------------------------------------------------------------------------------- 1 | name: NodeJS with Grunt 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Build 26 | run: | 27 | npm install 28 | grunt build 29 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) => { 2 | grunt.initConfig({ 3 | eslint: { 4 | src: ['src/*.js', 'test/*.test.js'] 5 | }, 6 | mochaTest: { 7 | files: ['test/*.test.js'] 8 | }, 9 | mocha_istanbul: { 10 | coverage: { 11 | src: 'test', 12 | options: { 13 | mask: '*.test.js' 14 | } 15 | } 16 | } 17 | }); 18 | 19 | grunt.loadNpmTasks('grunt-eslint'); 20 | grunt.loadNpmTasks('grunt-mocha-test'); 21 | grunt.loadNpmTasks('grunt-mocha-istanbul'); 22 | 23 | grunt.registerTask('lint', ['eslint']); 24 | grunt.registerTask('test', ['mochaTest']); 25 | grunt.registerTask('coverage', ['mocha_istanbul']); 26 | grunt.registerTask('build', ['lint', 'coverage']); 27 | }; 28 | -------------------------------------------------------------------------------- /src/heap.d.ts: -------------------------------------------------------------------------------- 1 | export interface ICompare { 2 | (a: T, b: T): number; 3 | } 4 | 5 | export class Heap implements Iterable { 6 | constructor(comparator: ICompare, values?: T[], leaf?: T); 7 | toArray(): T[]; 8 | [Symbol.iterator](): Iterator; 9 | insert(value: T): Heap; 10 | push(value: T): Heap; 11 | extractRoot(): T | null; 12 | pop(): T | null; 13 | sort(): T[]; 14 | fix(): Heap; 15 | isValid(): boolean; 16 | clone(): Heap; 17 | root(): T | null; 18 | top(): T | null; 19 | leaf(): T | null; 20 | size(): number; 21 | isEmpty(): boolean; 22 | clear(): void; 23 | static heapify(values: T[], comparator: ICompare): Heap; 24 | static isHeapified(values: T[], comparator: ICompare): boolean; 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Eyas Ranjous 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datastructures-js/heap", 3 | "version": "4.3.7", 4 | "description": "Min/Max Heap & Heap Sort implementation in javascript", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "grunt test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/datastructures-js/heap.git" 13 | }, 14 | "keywords": [ 15 | "heap", 16 | "min heap", 17 | "min heap data structure", 18 | "max heap", 19 | "max heap data structure", 20 | "heap js", 21 | "heap data structure", 22 | "heap es6", 23 | "heap sort", 24 | "heapify" 25 | ], 26 | "author": "Eyas Ranjous ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/datastructures-js/heap/issues" 30 | }, 31 | "homepage": "https://github.com/datastructures-js/heap#readme", 32 | "devDependencies": { 33 | "chai": "^4.2.0", 34 | "eslint": "^6.7.2", 35 | "eslint-config-airbnb-base": "^14.0.0", 36 | "eslint-plugin-import": "^2.19.1", 37 | "grunt": "^1.0.4", 38 | "grunt-eslint": "^22.0.0", 39 | "grunt-mocha-istanbul": "^5.0.2", 40 | "grunt-mocha-test": "^0.13.3", 41 | "istanbul": "^0.4.5", 42 | "mocha": "^6.2.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/maxHeap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright 2020 Eyas Ranjous 4 | */ 5 | 6 | const { Heap } = require('./heap'); 7 | 8 | const getMaxCompare = (getCompareValue) => (a, b) => { 9 | const aVal = typeof getCompareValue === 'function' ? getCompareValue(a) : a; 10 | const bVal = typeof getCompareValue === 'function' ? getCompareValue(b) : b; 11 | return aVal < bVal ? 1 : -1; 12 | }; 13 | 14 | /** 15 | * @class MaxHeap 16 | * @extends Heap 17 | */ 18 | class MaxHeap extends Heap { 19 | /** 20 | * @param {function} [getCompareValue] 21 | * @param {array} [values] 22 | */ 23 | constructor(getCompareValue, values) { 24 | super(getMaxCompare(getCompareValue), values); 25 | this._getCompareValue = getCompareValue; 26 | } 27 | 28 | /** 29 | * Inserts a new value into the heap 30 | * @public 31 | * @param {number|string|object} value 32 | * @returns {MaxHeap} 33 | */ 34 | insert(value) { 35 | super.insert(value); 36 | return this; 37 | } 38 | 39 | /** 40 | * Inserts a new value into the heap 41 | * @public 42 | * @param {number|string|object} value 43 | * @returns {MaxHeap} 44 | */ 45 | push(value) { 46 | return this.insert(value); 47 | } 48 | 49 | /** 50 | * Fixes node positions in the heap 51 | * @public 52 | * @returns {MaxHeap} 53 | */ 54 | fix() { 55 | super.fix(); 56 | return this; 57 | } 58 | 59 | /** 60 | * Returns a shallow copy of the MaxHeap 61 | * @public 62 | * @returns {MaxHeap} 63 | */ 64 | clone() { 65 | return new MaxHeap(this._getCompareValue, this._nodes.slice()); 66 | } 67 | 68 | /** 69 | * Builds a MaxHeap from an array 70 | * @public 71 | * @static 72 | * @param {array} values 73 | * @param {function} [getCompareValue] 74 | * @returns {MaxHeap} 75 | */ 76 | static heapify(values, getCompareValue) { 77 | if (!Array.isArray(values)) { 78 | throw new Error('MaxHeap.heapify expects an array'); 79 | } 80 | return new MaxHeap(getCompareValue, values); 81 | } 82 | 83 | /** 84 | * Checks if a list of values is a valid max heap 85 | * @public 86 | * @static 87 | * @param {array} values 88 | * @param {function} [getCompareValue] 89 | * @returns {boolean} 90 | */ 91 | static isHeapified(values, getCompareValue) { 92 | return new MaxHeap(getCompareValue, values).isValid(); 93 | } 94 | } 95 | 96 | exports.MaxHeap = MaxHeap; 97 | -------------------------------------------------------------------------------- /src/minHeap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright 2020 Eyas Ranjous 4 | */ 5 | 6 | const { Heap } = require('./heap'); 7 | 8 | const getMinCompare = (getCompareValue) => (a, b) => { 9 | const aVal = typeof getCompareValue === 'function' ? getCompareValue(a) : a; 10 | const bVal = typeof getCompareValue === 'function' ? getCompareValue(b) : b; 11 | return aVal <= bVal ? -1 : 1; 12 | }; 13 | 14 | /** 15 | * @class MinHeap 16 | * @extends Heap 17 | */ 18 | class MinHeap extends Heap { 19 | /** 20 | * @param {function} [getCompareValue] 21 | * @param {array} [values] 22 | */ 23 | constructor(getCompareValue, values) { 24 | super(getMinCompare(getCompareValue), values); 25 | this._getCompareValue = getCompareValue; 26 | } 27 | 28 | /** 29 | * Inserts a new value into the heap 30 | * @public 31 | * @param {number|string|object} value 32 | * @returns {MinHeap} 33 | */ 34 | insert(value) { 35 | super.insert(value); 36 | return this; 37 | } 38 | 39 | /** 40 | * Inserts a new value into the heap 41 | * @public 42 | * @param {number|string|object} value 43 | * @returns {MinHeap} 44 | */ 45 | push(value) { 46 | return this.insert(value); 47 | } 48 | 49 | /** 50 | * Fixes node positions in the heap 51 | * @public 52 | * @returns {MinHeap} 53 | */ 54 | fix() { 55 | super.fix(); 56 | return this; 57 | } 58 | 59 | /** 60 | * Returns a shallow copy of the MinHeap 61 | * @public 62 | * @returns {MinHeap} 63 | */ 64 | clone() { 65 | return new MinHeap(this._getCompareValue, this._nodes.slice()); 66 | } 67 | 68 | /** 69 | * Builds a MinHeap from an array 70 | * @public 71 | * @static 72 | * @param {array} values 73 | * @param {function} [getCompareValue] 74 | * @returns {MinHeap} 75 | */ 76 | static heapify(values, getCompareValue) { 77 | if (!Array.isArray(values)) { 78 | throw new Error('MinHeap.heapify expects an array'); 79 | } 80 | return new MinHeap(getCompareValue, values); 81 | } 82 | 83 | /** 84 | * Checks if a list of values is a valid min heap 85 | * @public 86 | * @static 87 | * @param {array} values 88 | * @param {function} [getCompareValue] 89 | * @returns {boolean} 90 | */ 91 | static isHeapified(values, getCompareValue) { 92 | return new MinHeap(getCompareValue, values).isValid(); 93 | } 94 | } 95 | 96 | exports.MinHeap = MinHeap; 97 | -------------------------------------------------------------------------------- /test/readme.ts: -------------------------------------------------------------------------------- 1 | import { Heap, MinHeap, MaxHeap, ICompare, IGetCompareValue } from '../index'; 2 | 3 | interface ICar { 4 | year: number; 5 | price: number; 6 | } 7 | 8 | const compareCars: ICompare = (a: ICar, b: ICar) => { 9 | if (a.year > b.year) { 10 | return -1; 11 | } 12 | if (a.year < b.year) { 13 | // prioratize newest cars 14 | return 1; 15 | } 16 | // with least price 17 | return a.price < b.price ? -1 : 1; 18 | }; 19 | 20 | const carsHeap = new Heap(compareCars); 21 | 22 | const cars = [ 23 | { year: 2013, price: 35000 }, 24 | { year: 2010, price: 2000 }, 25 | { year: 2013, price: 30000 }, 26 | { year: 2017, price: 50000 }, 27 | { year: 2013, price: 25000 }, 28 | { year: 2015, price: 40000 }, 29 | { year: 2022, price: 70000 } 30 | ]; 31 | cars.forEach((car) => carsHeap.insert(car)); 32 | 33 | while (!carsHeap.isEmpty()) { 34 | console.log(carsHeap.extractRoot()); 35 | } 36 | 37 | const numbersHeap = new MinHeap(); 38 | 39 | interface IBid { 40 | id: number; 41 | value: number; 42 | } 43 | const getBidCompareValue: IGetCompareValue = (bid: IBid) => bid.value; 44 | const bidsHeap = new MaxHeap(getBidCompareValue); 45 | 46 | const numbers = [3, -2, 5, 0, -1, -5, 4]; 47 | numbers.forEach((num) => numbersHeap.insert(num)); 48 | 49 | const bids = [ 50 | { id: 1, value: 1000 }, 51 | { id: 2, value: 20000 }, 52 | { id: 3, value: 1000 }, 53 | { id: 4, value: 1500 }, 54 | { id: 5, value: 12000 }, 55 | { id: 6, value: 4000 }, 56 | { id: 7, value: 8000 } 57 | ]; 58 | bids.forEach((bid) => bidsHeap.insert(bid)); 59 | 60 | while (!numbersHeap.isEmpty()) { 61 | console.log(numbersHeap.extractRoot()); 62 | } 63 | 64 | while (!bidsHeap.isEmpty()) { 65 | console.log(bidsHeap.extractRoot()); 66 | } 67 | 68 | cars.forEach((car) => carsHeap.insert(car)); 69 | numbers.forEach((num) => numbersHeap.insert(num)); 70 | bids.forEach((bid) => bidsHeap.insert(bid)); 71 | 72 | console.log(carsHeap.root()); 73 | console.log(numbersHeap.root()); 74 | console.log(bidsHeap.root()); 75 | 76 | console.log(carsHeap.leaf()); 77 | console.log(numbersHeap.leaf()); 78 | console.log(bidsHeap.leaf()); 79 | 80 | console.log(carsHeap.size()); // 7 81 | console.log(numbersHeap.size()); // 7 82 | console.log(bidsHeap.size()); // 7 83 | 84 | console.log(carsHeap.sort()); 85 | console.log(numbersHeap.sort()); 86 | console.log(bidsHeap.sort()); 87 | 88 | console.log(carsHeap.isValid()); 89 | console.log(numbersHeap.isValid()); 90 | console.log(bidsHeap.isValid()); 91 | 92 | const heapifiedCars = Heap.heapify(cars, compareCars); 93 | console.log(heapifiedCars.isValid()); // true 94 | console.log(cars); 95 | 96 | const heapifiedNumbers = MinHeap.heapify(numbers); 97 | console.log(heapifiedNumbers.isValid()); // true 98 | console.log(numbers); 99 | 100 | const heapifiedBids = MaxHeap.heapify(bids, (bid) => bid.value); 101 | console.log(heapifiedBids.isValid()); // true 102 | console.log(bids); 103 | 104 | console.log(Heap.isHeapified(cars, compareCars)); // true 105 | console.log(MinHeap.isHeapified(numbers)); // true 106 | console.log(MaxHeap.isHeapified(bids, (bid) => bid.value)); // true 107 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ## [4.3.7] - 2025-10-11 9 | ### Fixed 10 | - Fix ts types. 11 | 12 | ## [4.3.6] - 2025-10-11 13 | ### Fixed 14 | - Heap initialization with list of values 15 | - MinHeap initialization with list of values 16 | - MaxHeap initialization with list of values 17 | 18 | ## [4.3.5] - 2025-08-25 19 | ### Fixed 20 | - README 21 | 22 | ## [4.3.4] - 2024-07-14 23 | ### Fixed 24 | - Redundant code 25 | 26 | ## [4.3.3] - 2024-01-07 27 | ### Fixed 28 | - default compare function for MinHeap 29 | 30 | ## [4.3.2] - 2023-06-19 31 | ### Fixed 32 | - ts types. 33 | 34 | ## [4.3.1] - 2023-01-08 35 | ### Fixed 36 | - lint config. 37 | 38 | ## [4.3.0] - 2023-01-08 39 | ### Added 40 | - `toArray` to convert the heap into an array without sorting. 41 | 42 | ## [4.2.2] - 2022-12-24 43 | ### Fixed 44 | - add iterable for ts definitions. 45 | 46 | ## [4.2.1] - 2022-12-23 47 | ### Fixed 48 | - typo in readme. 49 | 50 | ## [4.2.0] - 2022-12-23 51 | ### Added 52 | - `Symbol.iterator` to iterate on heaps pop. 53 | 54 | ### Fixed 55 | - `.fix()` to also fix heap leaf value in addition to nodes positions. 56 | 57 | ## [4.1.2] - 2022-09-04 58 | ### Fixed 59 | - Optimize `.fix()` to run in O(n) runtime instead of O(n*log(n)). 60 | 61 | ## [4.1.1] - 2022-08-15 62 | ### Fixed 63 | - add types to package.json 64 | 65 | ## [4.1.0] - 2022-05-30 66 | ### Added 67 | - push, pop & top as alias methods for insert, extractRoot & root 68 | 69 | ## [4.0.2] - 2022-03-13 70 | ### Fixed 71 | - ts types (again). 72 | 73 | ## [4.0.1] - 2022-03-09 74 | ### Fixed 75 | - ts types. 76 | 77 | ## [4.0.0] - 2022-02-21 78 | ### Changed 79 | - better code, better world. 80 | 81 | ## [3.2.0] - 2021-08-05 82 | ### Added 83 | - CustomHeap to allow constructing a heap with a custom comparator callback. 84 | 85 | ## [3.1.1] - 2021-06-20 86 | 87 | ### Fixed 88 | - index.d.ts 89 | 90 | ## [3.1.0] - 2021-06-15 91 | 92 | ### Added 93 | - typescript. 94 | 95 | ## [3.0.1] - 2021-03-28 96 | 97 | ### Fixed 98 | - Readme 99 | 100 | ## [3.0.0] - 2020-01-17 101 | 102 | ### Changed 103 | - simplified heap nodes. preserves numbers and strings, and use object literal for key:value. 104 | - `.heapify` static function now heapify the input list as well as returning a heap insatnce. 105 | 106 | ### Added 107 | - `.fix()` to fix positions of nodes in the heap. 108 | - `.isValid` to validate heap nodes are in right positions. 109 | - `.isHeapified` static function to valida if a given list is heapified. 110 | 111 | ### Fixed 112 | - jsdoc 113 | - README 114 | 115 | ## [2.0.0] - 2020-04-06 116 | ### Changed 117 | - remove none-standard method `.serialize()`. 118 | 119 | ### Fixed 120 | - return inserted node in Min/Max Heap. 121 | - README 122 | - jsdoc 123 | 124 | ## [1.2.0] - 2020-03-07 125 | ### Added 126 | - `.leaf()` to get the max node in a MinHeap or the min node in a MaxHeap. 127 | 128 | ## [1.1.2] - 2020-03-06 129 | ### Fixed 130 | - params naming. 131 | 132 | ## [1.1.1] - 2019-12-24 133 | ### Fixed 134 | - add a table of content to readme 135 | 136 | ## [1.1.0] - 2019-12-16 137 | ### Added 138 | `.serialize()` to convert a heap to a list of serialized nodes. 139 | 140 | ### Fixed 141 | - improve README. 142 | 143 | ## [1.0.1] - 2019-12-16 144 | ### Fixed 145 | - Readme & Description. 146 | 147 | ## [1.0.0] - 2019-12-15 148 | ### Added 149 | - initial release 150 | -------------------------------------------------------------------------------- /test/maxHeap.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { MaxHeap } = require('../src/maxHeap'); 3 | 4 | describe('MaxHeap', () => { 5 | describe('primitive values heap', () => { 6 | const values = ['m', 'x', 'f', 'b', 'z', 'k', 'c']; 7 | const heap = new MaxHeap(); 8 | 9 | it('insert values', () => { 10 | values.forEach((value) => heap.insert(value)); 11 | }); 12 | 13 | it('sort in ascending order', () => { 14 | expect(heap.sort()).to.eql(values.slice().sort((a, b) => ( 15 | a > b ? 1 : -1 16 | ))); 17 | }); 18 | 19 | it('fix position after sort', () => { 20 | expect(heap.isValid()).to.equal(false); 21 | expect(heap.fix().isValid()).to.equal(true); 22 | }); 23 | 24 | it('gets root value', () => { 25 | expect(heap.root()).to.eql('z'); 26 | }); 27 | 28 | it('gets leaf value', () => { 29 | expect(heap.leaf()).to.eql('b'); 30 | }); 31 | 32 | it('gets heap size', () => { 33 | expect(heap.size()).to.equal(7); 34 | }); 35 | 36 | it('checks if heap is empty', () => { 37 | expect(heap.isEmpty()).to.equal(false); 38 | }); 39 | 40 | it('clone the heap', () => { 41 | expect(heap.clone().sort()).to.eql(values.slice().sort((a, b) => ( 42 | a > b ? 1 : -1 43 | ))); 44 | expect(heap.isValid()).to.equal(true); 45 | }); 46 | 47 | it('extract root value', () => { 48 | expect(heap.extractRoot()).to.deep.equal('z'); 49 | expect(heap.extractRoot()).to.deep.equal('x'); 50 | expect(heap.extractRoot()).to.deep.equal('m'); 51 | expect(heap.extractRoot()).to.deep.equal('k'); 52 | expect(heap.extractRoot()).to.deep.equal('f'); 53 | expect(heap.extractRoot()).to.deep.equal('c'); 54 | expect(heap.extractRoot()).to.deep.equal('b'); 55 | expect(heap.isEmpty()).to.equal(true); 56 | }); 57 | }); 58 | 59 | describe('object values heap', () => { 60 | const values = [ 61 | { id: 'm' }, 62 | { id: 'x' }, 63 | { id: 'f' }, 64 | { id: 'b' }, 65 | { id: 'z' }, 66 | { id: 'k' }, 67 | { id: 'c' } 68 | ]; 69 | const heap = new MaxHeap((value) => value.id); 70 | 71 | it('push values', () => { 72 | values.forEach((value) => heap.push(value)); 73 | }); 74 | 75 | it('sort in ascending order', () => { 76 | expect(heap.sort()).to.eql(values.slice().sort((a, b) => ( 77 | a.id > b.id ? 1 : -1 78 | ))); 79 | }); 80 | 81 | it('fix position after sort', () => { 82 | expect(heap.isValid()).to.equal(false); 83 | expect(heap.fix().isValid()).to.equal(true); 84 | }); 85 | 86 | it('gets root value', () => { 87 | expect(heap.top()).to.eql({ id: 'z' }); 88 | }); 89 | 90 | it('gets leaf value', () => { 91 | expect(heap.leaf()).to.eql({ id: 'b' }); 92 | }); 93 | 94 | it('gets heap size', () => { 95 | expect(heap.size()).to.equal(7); 96 | }); 97 | 98 | it('checks if heap is empty', () => { 99 | expect(heap.isEmpty()).to.equal(false); 100 | }); 101 | 102 | it('clone the heap', () => { 103 | expect(heap.clone().sort()).to.eql(values.slice().sort((a, b) => ( 104 | a.id > b.id ? 1 : -1 105 | ))); 106 | expect(heap.isValid()).to.equal(true); 107 | }); 108 | 109 | it('pop root value', () => { 110 | expect(heap.pop()).to.deep.equal({ id: 'z' }); 111 | expect(heap.pop()).to.deep.equal({ id: 'x' }); 112 | expect(heap.pop()).to.deep.equal({ id: 'm' }); 113 | expect(heap.pop()).to.deep.equal({ id: 'k' }); 114 | expect(heap.pop()).to.deep.equal({ id: 'f' }); 115 | expect(heap.pop()).to.deep.equal({ id: 'c' }); 116 | expect(heap.pop()).to.deep.equal({ id: 'b' }); 117 | expect(heap.isEmpty()).to.equal(true); 118 | }); 119 | }); 120 | 121 | describe('isValid', () => { 122 | it('consider heap with duplicates as valid', () => { 123 | const maxHeap = new MaxHeap((x) => x.value); 124 | 125 | maxHeap.insert({ value: 2268 }); 126 | maxHeap.insert({ value: 2268 }); 127 | expect(maxHeap.isValid()).to.equal(true); 128 | 129 | const maxHeap2 = new MaxHeap(); 130 | maxHeap2.insert(22); 131 | maxHeap2.insert(22); 132 | expect(maxHeap2.isValid()).to.equal(true); 133 | }); 134 | }); 135 | 136 | describe('iterator', () => { 137 | it('allows iterating on heap elements', () => { 138 | const testArr = [90, 80, 50, 40, 30, 20]; 139 | const h1 = MaxHeap.heapify(testArr.slice()); 140 | expect([...h1]).to.eql(testArr); 141 | const h2 = MaxHeap.heapify(testArr.slice()); 142 | const res = []; 143 | for (const n of h2) { 144 | res.push(n); 145 | } 146 | expect(res).to.eql(testArr); 147 | }); 148 | }); 149 | describe('toArray', () => { 150 | it('Converts the heap to a cloned array.', () => { 151 | const testArr = [90, 80, 50, 40, 30, 20].sort((a, b) => -a + b); 152 | const h1 = MaxHeap.heapify(testArr.slice()); 153 | expect(h1.toArray().sort((a, b) => -a + b)).to.eql(testArr); 154 | }); 155 | }); 156 | 157 | describe('constructor with initial values', () => { 158 | it('should properly heapify initial values and maintain heap property after insertions', () => { 159 | const heap = new MaxHeap(null, [3, 1, 4]); 160 | expect(heap.isValid()).to.equal(true); 161 | 162 | heap.insert(2); 163 | expect(heap.toArray()).to.eql([4, 2, 3, 1]); 164 | expect(heap.isValid()).to.equal(true); 165 | }); 166 | 167 | it('should handle insertion of largest element correctly', () => { 168 | const heap1 = new MaxHeap(null, [3, 1, 4]); 169 | heap1.insert(5); 170 | expect(heap1.toArray()).to.eql([5, 4, 3, 1]); 171 | expect(heap1.isValid()).to.equal(true); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/minHeap.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { MinHeap } = require('../src/minHeap'); 3 | 4 | describe('MinHeap', () => { 5 | describe('primitive values heap', () => { 6 | const values = [50, 80, 30, 90, 60, 40, 20]; 7 | const heap = new MinHeap(); 8 | 9 | it('insert values', () => { 10 | values.forEach((value) => heap.insert(value)); 11 | }); 12 | 13 | it('sort in descending order', () => { 14 | expect(heap.sort()).to.eql(values.slice().sort((a, b) => b - a)); 15 | }); 16 | 17 | it('fix position after sort', () => { 18 | expect(heap.isValid()).to.equal(false); 19 | expect(heap.fix().isValid()).to.equal(true); 20 | }); 21 | 22 | it('gets root value', () => { 23 | expect(heap.root()).to.eql(20); 24 | }); 25 | 26 | it('gets leaf value', () => { 27 | expect(heap.leaf()).to.eql(90); 28 | }); 29 | 30 | it('gets heap size', () => { 31 | expect(heap.size()).to.equal(7); 32 | }); 33 | 34 | it('checks if heap is empty', () => { 35 | expect(heap.isEmpty()).to.equal(false); 36 | }); 37 | 38 | it('clone the heap', () => { 39 | expect(heap.clone().sort()).to.eql(values.slice().sort((a, b) => b - a)); 40 | expect(heap.isValid()).to.equal(true); 41 | }); 42 | 43 | it('extract root value', () => { 44 | expect(heap.extractRoot()).to.deep.equal(20); 45 | expect(heap.extractRoot()).to.deep.equal(30); 46 | expect(heap.extractRoot()).to.deep.equal(40); 47 | expect(heap.extractRoot()).to.deep.equal(50); 48 | expect(heap.extractRoot()).to.deep.equal(60); 49 | expect(heap.extractRoot()).to.deep.equal(80); 50 | expect(heap.extractRoot()).to.deep.equal(90); 51 | expect(heap.isEmpty()).to.equal(true); 52 | }); 53 | }); 54 | 55 | describe('object values heap', () => { 56 | const values = [ 57 | { id: 50 }, 58 | { id: 80 }, 59 | { id: 30 }, 60 | { id: 90 }, 61 | { id: 60 }, 62 | { id: 40 }, 63 | { id: 20 } 64 | ]; 65 | const heap = new MinHeap((value) => value.id); 66 | 67 | it('push values', () => { 68 | values.forEach((value) => heap.push(value)); 69 | }); 70 | 71 | it('sort in descending order', () => { 72 | expect(heap.sort()).to.eql(values.slice().sort((a, b) => b.id - a.id)); 73 | }); 74 | 75 | it('fix position after sort', () => { 76 | expect(heap.isValid()).to.equal(false); 77 | expect(heap.fix().isValid()).to.equal(true); 78 | }); 79 | 80 | it('gets root value', () => { 81 | expect(heap.top()).to.eql({ id: 20 }); 82 | }); 83 | 84 | it('gets leaf value', () => { 85 | expect(heap.leaf()).to.eql({ id: 90 }); 86 | }); 87 | 88 | it('gets heap size', () => { 89 | expect(heap.size()).to.equal(7); 90 | }); 91 | 92 | it('checks if heap is empty', () => { 93 | expect(heap.isEmpty()).to.equal(false); 94 | }); 95 | 96 | it('clone the heap', () => { 97 | expect(heap.clone().sort()) 98 | .to.eql(values.slice().sort((a, b) => b.id - a.id)); 99 | expect(heap.isValid()).to.equal(true); 100 | }); 101 | 102 | it('pop root value', () => { 103 | expect(heap.pop()).to.deep.equal({ id: 20 }); 104 | expect(heap.pop()).to.deep.equal({ id: 30 }); 105 | expect(heap.pop()).to.deep.equal({ id: 40 }); 106 | expect(heap.pop()).to.deep.equal({ id: 50 }); 107 | expect(heap.pop()).to.deep.equal({ id: 60 }); 108 | expect(heap.pop()).to.deep.equal({ id: 80 }); 109 | expect(heap.pop()).to.deep.equal({ id: 90 }); 110 | expect(heap.isEmpty()).to.equal(true); 111 | }); 112 | }); 113 | 114 | describe('isValid', () => { 115 | it('consider heap with duplicates as valid', () => { 116 | const minHeap = new MinHeap((x) => x.value); 117 | 118 | minHeap.insert({ value: 2268 }); 119 | minHeap.insert({ value: 2268 }); 120 | expect(minHeap.isValid()).to.equal(true); 121 | 122 | const minHeap2 = new MinHeap(); 123 | minHeap2.insert(22); 124 | minHeap2.insert(22); 125 | expect(minHeap2.isValid()).to.equal(true); 126 | }); 127 | }); 128 | 129 | describe('iterator', () => { 130 | it('allows iterating on heap elements', () => { 131 | const testArr = [20, 30, 40, 50, 80, 90]; 132 | const h1 = MinHeap.heapify(testArr.slice()); 133 | expect([...h1]).to.eql(testArr); 134 | const h2 = MinHeap.heapify(testArr.slice()); 135 | const res = []; 136 | for (const n of h2) { 137 | res.push(n); 138 | } 139 | expect(res).to.eql(testArr); 140 | }); 141 | 142 | it('iterates correctly with duplicates', () => { 143 | const heap2 = new MinHeap(); 144 | [3, 1, 4, 1, 5].forEach((n) => heap2.insert(n)); 145 | const result = []; 146 | for (const num of heap2) { 147 | result.push(num); 148 | } 149 | expect(result).to.eql([1, 1, 3, 4, 5]); 150 | }); 151 | }); 152 | 153 | describe('toArray', () => { 154 | it('Converts the heap to a cloned array', () => { 155 | const testArr = [20, 30, 40, 50, 80, 90].sort((a, b) => a - b); 156 | const h1 = MinHeap.heapify(testArr.slice()); 157 | expect(h1.toArray().sort((a, b) => a - b)).to.eql(testArr); 158 | }); 159 | }); 160 | 161 | describe('constructor with initial values', () => { 162 | it('should properly heapify initial values and maintain heap property after insertions', () => { 163 | const heap = new MinHeap(null, [3, 1, 4]); 164 | expect(heap.isValid()).to.equal(true); 165 | 166 | heap.insert(2); 167 | expect(heap.toArray()).to.eql([1, 2, 4, 3]); 168 | expect(heap.isValid()).to.equal(true); 169 | }); 170 | 171 | it('should handle insertion of smallest element correctly', () => { 172 | const heap1 = new MinHeap(null, [3, 1, 4]); 173 | heap1.insert(0); 174 | expect(heap1.toArray()).to.eql([0, 1, 4, 3]); 175 | expect(heap1.isValid()).to.equal(true); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/heap.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { Heap } = require('../src/heap'); 3 | 4 | describe('Heap', () => { 5 | const numComparator = (a, b) => a.id - b.id; 6 | const numValues = [ 7 | { id: 50 }, 8 | { id: 80 }, 9 | { id: 30 }, 10 | { id: 90 }, 11 | { id: 60 }, 12 | { id: 40 }, 13 | { id: 20 } 14 | ]; 15 | 16 | const charComparator = (a, b) => ( 17 | a.id < b.id ? 1 : -1 18 | ); 19 | const charValues = [ 20 | { id: 'm' }, 21 | { id: 'x' }, 22 | { id: 'f' }, 23 | { id: 'b' }, 24 | { id: 'z' }, 25 | { id: 'k' }, 26 | { id: 'c' } 27 | ]; 28 | 29 | describe('min heap', () => { 30 | const heap = new Heap(numComparator); 31 | 32 | it('insert values', () => { 33 | numValues.forEach((value) => heap.insert(value)); 34 | }); 35 | 36 | it('sort in descending order', () => { 37 | expect(heap.sort()).to.eql(numValues.slice().sort((a, b) => b.id - a.id)); 38 | }); 39 | 40 | it('fix position after sort', () => { 41 | expect(heap.isValid()).to.equal(false); 42 | expect(heap.fix().isValid()).to.equal(true); 43 | expect(heap.leaf()).to.eql({ id: 90 }); 44 | }); 45 | 46 | it('gets root value', () => { 47 | expect(heap.top()).to.eql({ id: 20 }); 48 | }); 49 | 50 | it('gets leaf value', () => { 51 | expect(heap.leaf()).to.eql({ id: 90 }); 52 | }); 53 | 54 | it('gets heap size', () => { 55 | expect(heap.size()).to.equal(7); 56 | }); 57 | 58 | it('checks if heap is empty', () => { 59 | expect(heap.isEmpty()).to.equal(false); 60 | }); 61 | 62 | it('clone the heap', () => { 63 | expect(heap.clone().sort()) 64 | .to.eql(numValues.slice().sort((a, b) => b.id - a.id)); 65 | expect(heap.isValid()).to.equal(true); 66 | }); 67 | 68 | it('extract root value', () => { 69 | expect(heap.extractRoot()).to.deep.equal({ id: 20 }); 70 | expect(heap.extractRoot()).to.deep.equal({ id: 30 }); 71 | expect(heap.extractRoot()).to.deep.equal({ id: 40 }); 72 | expect(heap.extractRoot()).to.deep.equal({ id: 50 }); 73 | expect(heap.extractRoot()).to.deep.equal({ id: 60 }); 74 | expect(heap.extractRoot()).to.deep.equal({ id: 80 }); 75 | expect(heap.extractRoot()).to.deep.equal({ id: 90 }); 76 | expect(heap.isEmpty()).to.equal(true); 77 | }); 78 | }); 79 | 80 | describe('max heap', () => { 81 | const heap = new Heap(charComparator); 82 | 83 | it('push values', () => { 84 | charValues.forEach((value) => heap.push(value)); 85 | }); 86 | 87 | it('sort in ascending order', () => { 88 | expect(heap.sort()) 89 | .to.eql(charValues.slice().sort((a, b) => ( 90 | a.id > b.id ? 1 : -1 91 | ))); 92 | }); 93 | 94 | it('fix position after sort', () => { 95 | expect(heap.isValid()).to.equal(false); 96 | expect(heap.fix().isValid()).to.equal(true); 97 | expect(heap.leaf()).to.eql({ id: 'b' }); 98 | }); 99 | 100 | it('gets root value', () => { 101 | expect(heap.root()).to.eql({ id: 'z' }); 102 | }); 103 | 104 | it('gets leaf value', () => { 105 | expect(heap.leaf()).to.eql({ id: 'b' }); 106 | }); 107 | 108 | it('gets heap size', () => { 109 | expect(heap.size()).to.equal(7); 110 | }); 111 | 112 | it('checks if heap is empty', () => { 113 | expect(heap.isEmpty()).to.equal(false); 114 | }); 115 | 116 | it('clone the heap', () => { 117 | expect(heap.clone().sort()) 118 | .to.eql(charValues.slice().sort((a, b) => ( 119 | a.id > b.id ? 1 : -1 120 | ))); 121 | expect(heap.isValid()).to.equal(true); 122 | }); 123 | 124 | it('pop root value', () => { 125 | expect(heap.pop()).to.deep.equal({ id: 'z' }); 126 | expect(heap.pop()).to.deep.equal({ id: 'x' }); 127 | expect(heap.pop()).to.deep.equal({ id: 'm' }); 128 | expect(heap.pop()).to.deep.equal({ id: 'k' }); 129 | expect(heap.pop()).to.deep.equal({ id: 'f' }); 130 | expect(heap.pop()).to.deep.equal({ id: 'c' }); 131 | expect(heap.pop()).to.deep.equal({ id: 'b' }); 132 | expect(heap.isEmpty()).to.equal(true); 133 | }); 134 | }); 135 | 136 | describe('heapify', () => { 137 | it('buids min heap from array', () => { 138 | const heap = Heap.heapify(numValues, numComparator); 139 | expect(heap.isValid()).to.equal(true); 140 | expect(heap.leaf()).to.eql({ id: 90 }); 141 | }); 142 | 143 | it('builds max heap from array', () => { 144 | const heap = Heap.heapify(charValues, charComparator); 145 | expect(heap.isValid()).to.equal(true); 146 | expect(heap.leaf()).to.eql({ id: 'b' }); 147 | }); 148 | }); 149 | 150 | describe('isHeapified', () => { 151 | it('checks if a list is min-heapified', () => { 152 | const heapifiedValues = [ 153 | { id: 20 }, 154 | { id: 60 }, 155 | { id: 30 }, 156 | { id: 90 }, 157 | { id: 80 }, 158 | { id: 50 }, 159 | { id: 40 } 160 | ]; 161 | expect(Heap.isHeapified(heapifiedValues, numComparator)).to.equal(true); 162 | }); 163 | 164 | it('checks if a list is max-heapified', () => { 165 | const heapifiedValues = [ 166 | { id: 'z' }, 167 | { id: 'x' }, 168 | { id: 'k' }, 169 | { id: 'b' }, 170 | { id: 'm' }, 171 | { id: 'f' }, 172 | { id: 'c' } 173 | ]; 174 | expect(Heap.isHeapified(heapifiedValues, charComparator)).to.equal(true); 175 | }); 176 | }); 177 | 178 | describe('iterator', () => { 179 | it('allows iterating on heap elements', () => { 180 | const testArr = [20, 30, 40, 50, 80, 90]; 181 | const h1 = Heap.heapify(testArr.slice(), (a, b) => a - b); 182 | expect([...h1]).to.eql(testArr); 183 | const h2 = Heap.heapify(testArr.slice(), (a, b) => a - b); 184 | const res = []; 185 | for (const n of h2) { 186 | res.push(n); 187 | } 188 | expect(res).to.eql(testArr); 189 | }); 190 | }); 191 | describe('toArray', () => { 192 | it('Converts the heap to a cloned array.', () => { 193 | const testArr = [20, 30, 40, 50, 80, 90].sort((a, b) => a - b); 194 | const h1 = Heap.heapify(testArr.slice(), (a, b) => a - b); 195 | expect(h1.toArray().sort((a, b) => a - b)).to.eql(testArr); 196 | }); 197 | }); 198 | 199 | describe('constructor with initial values', () => { 200 | it('should properly heapify initial values and maintain heap property after insertions', () => { 201 | const heap = new Heap((a, b) => a - b, [3, 1, 4]); 202 | expect(heap.isValid()).to.equal(true); 203 | 204 | heap.insert(2); 205 | expect(heap.toArray()).to.eql([1, 2, 4, 3]); 206 | expect(heap.isValid()).to.equal(true); 207 | }); 208 | 209 | it('should handle insertion of smallest element correctly', () => { 210 | const heap1 = new Heap((a, b) => a - b, [3, 1, 4]); 211 | heap1.insert(0); 212 | expect(heap1.toArray()).to.eql([0, 1, 4, 3]); 213 | expect(heap1.isValid()).to.equal(true); 214 | }); 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /src/heap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @copyright 2020 Eyas Ranjous 4 | * 5 | * @class 6 | */ 7 | class Heap { 8 | /** 9 | * @param {function} compare 10 | * @param {array} [values] 11 | * @param {number|string|object} [_leaf] 12 | */ 13 | constructor(compare, values, _leaf) { 14 | if (typeof compare !== 'function') { 15 | throw new Error('Heap constructor expects a compare function'); 16 | } 17 | this._compare = compare; 18 | this._nodes = Array.isArray(values) ? values : []; 19 | this._leaf = _leaf || null; 20 | 21 | if (this._nodes.length > 0) { 22 | this.fix(); 23 | } 24 | } 25 | 26 | /** 27 | * Converts the heap to a cloned array without sorting. 28 | * @public 29 | * @returns {Array} 30 | */ 31 | toArray() { 32 | return Array.from(this._nodes); 33 | } 34 | 35 | /** 36 | * Checks if a parent has a left child 37 | * @private 38 | */ 39 | _hasLeftChild(parentIndex) { 40 | const leftChildIndex = (parentIndex * 2) + 1; 41 | return leftChildIndex < this.size(); 42 | } 43 | 44 | /** 45 | * Checks if a parent has a right child 46 | * @private 47 | */ 48 | _hasRightChild(parentIndex) { 49 | const rightChildIndex = (parentIndex * 2) + 2; 50 | return rightChildIndex < this.size(); 51 | } 52 | 53 | /** 54 | * Compares two nodes 55 | * @private 56 | */ 57 | _compareAt(i, j) { 58 | return this._compare(this._nodes[i], this._nodes[j]); 59 | } 60 | 61 | /** 62 | * Swaps two nodes in the heap 63 | * @private 64 | */ 65 | _swap(i, j) { 66 | const temp = this._nodes[i]; 67 | this._nodes[i] = this._nodes[j]; 68 | this._nodes[j] = temp; 69 | } 70 | 71 | /** 72 | * Checks if parent and child should be swapped 73 | * @private 74 | */ 75 | _shouldSwap(parentIndex, childIndex) { 76 | if (parentIndex < 0 || parentIndex >= this.size()) { 77 | return false; 78 | } 79 | 80 | if (childIndex < 0 || childIndex >= this.size()) { 81 | return false; 82 | } 83 | 84 | return this._compareAt(parentIndex, childIndex) > 0; 85 | } 86 | 87 | /** 88 | * Compares children of a parent 89 | * @private 90 | */ 91 | _compareChildrenOf(parentIndex) { 92 | if (!this._hasLeftChild(parentIndex) && !this._hasRightChild(parentIndex)) { 93 | return -1; 94 | } 95 | 96 | const leftChildIndex = (parentIndex * 2) + 1; 97 | const rightChildIndex = (parentIndex * 2) + 2; 98 | 99 | if (!this._hasLeftChild(parentIndex)) { 100 | return rightChildIndex; 101 | } 102 | 103 | if (!this._hasRightChild(parentIndex)) { 104 | return leftChildIndex; 105 | } 106 | 107 | const compare = this._compareAt(leftChildIndex, rightChildIndex); 108 | return compare > 0 ? rightChildIndex : leftChildIndex; 109 | } 110 | 111 | /** 112 | * Compares two children before a position 113 | * @private 114 | */ 115 | _compareChildrenBefore(index, leftChildIndex, rightChildIndex) { 116 | const compare = this._compareAt(rightChildIndex, leftChildIndex); 117 | 118 | if (compare <= 0 && rightChildIndex < index) { 119 | return rightChildIndex; 120 | } 121 | 122 | return leftChildIndex; 123 | } 124 | 125 | /** 126 | * Recursively bubbles up a node if it's in a wrong position 127 | * @private 128 | */ 129 | _heapifyUp(startIndex) { 130 | let childIndex = startIndex; 131 | let parentIndex = Math.floor((childIndex - 1) / 2); 132 | 133 | while (this._shouldSwap(parentIndex, childIndex)) { 134 | this._swap(parentIndex, childIndex); 135 | childIndex = parentIndex; 136 | parentIndex = Math.floor((childIndex - 1) / 2); 137 | } 138 | } 139 | 140 | /** 141 | * Recursively bubbles down a node if it's in a wrong position 142 | * @private 143 | */ 144 | _heapifyDown(startIndex) { 145 | let parentIndex = startIndex; 146 | let childIndex = this._compareChildrenOf(parentIndex); 147 | 148 | while (this._shouldSwap(parentIndex, childIndex)) { 149 | this._swap(parentIndex, childIndex); 150 | parentIndex = childIndex; 151 | childIndex = this._compareChildrenOf(parentIndex); 152 | } 153 | } 154 | 155 | /** 156 | * Recursively bubbles down a node before a given index 157 | * @private 158 | */ 159 | _heapifyDownUntil(index) { 160 | let parentIndex = 0; 161 | let leftChildIndex = 1; 162 | let rightChildIndex = 2; 163 | let childIndex; 164 | 165 | while (leftChildIndex < index) { 166 | childIndex = this._compareChildrenBefore( 167 | index, 168 | leftChildIndex, 169 | rightChildIndex 170 | ); 171 | 172 | if (this._shouldSwap(parentIndex, childIndex)) { 173 | this._swap(parentIndex, childIndex); 174 | } 175 | 176 | parentIndex = childIndex; 177 | leftChildIndex = (parentIndex * 2) + 1; 178 | rightChildIndex = (parentIndex * 2) + 2; 179 | } 180 | } 181 | 182 | /** 183 | * Inserts a new value into the heap 184 | * @public 185 | * @param {number|string|object} value 186 | * @returns {Heap} 187 | */ 188 | insert(value) { 189 | this._nodes.push(value); 190 | this._heapifyUp(this.size() - 1); 191 | if (this._leaf === null || this._compare(value, this._leaf) > 0) { 192 | this._leaf = value; 193 | } 194 | return this; 195 | } 196 | 197 | /** 198 | * Inserts a new value into the heap 199 | * @public 200 | * @param {number|string|object} value 201 | * @returns {Heap} 202 | */ 203 | push(value) { 204 | return this.insert(value); 205 | } 206 | 207 | /** 208 | * Removes and returns the root node in the heap 209 | * @public 210 | * @returns {number|string|object} 211 | */ 212 | extractRoot() { 213 | if (this.isEmpty()) { 214 | return null; 215 | } 216 | 217 | const root = this.root(); 218 | this._nodes[0] = this._nodes[this.size() - 1]; 219 | this._nodes.pop(); 220 | this._heapifyDown(0); 221 | 222 | if (root === this._leaf) { 223 | this._leaf = null; 224 | } 225 | 226 | return root; 227 | } 228 | 229 | /** 230 | * Removes and returns the root node in the heap 231 | * @public 232 | * @returns {number|string|object} 233 | */ 234 | pop() { 235 | return this.extractRoot(); 236 | } 237 | 238 | /** 239 | * Applies heap sort and return the values sorted by priority 240 | * @public 241 | * @returns {array} 242 | */ 243 | sort() { 244 | for (let i = this.size() - 1; i > 0; i -= 1) { 245 | this._swap(0, i); 246 | this._heapifyDownUntil(i); 247 | } 248 | return this._nodes; 249 | } 250 | 251 | /** 252 | * Fixes node positions in the heap 253 | * @public 254 | * @returns {Heap} 255 | */ 256 | fix() { 257 | // fix node positions 258 | for (let i = Math.floor(this.size() / 2) - 1; i >= 0; i -= 1) { 259 | this._heapifyDown(i); 260 | } 261 | 262 | // fix leaf value 263 | for (let i = Math.floor(this.size() / 2); i < this.size(); i += 1) { 264 | const value = this._nodes[i]; 265 | if (this._leaf === null || this._compare(value, this._leaf) > 0) { 266 | this._leaf = value; 267 | } 268 | } 269 | 270 | return this; 271 | } 272 | 273 | /** 274 | * Verifies that all heap nodes are in the right position 275 | * @public 276 | * @returns {boolean} 277 | */ 278 | isValid() { 279 | const isValidRecursive = (parentIndex) => { 280 | let isValidLeft = true; 281 | let isValidRight = true; 282 | 283 | if (this._hasLeftChild(parentIndex)) { 284 | const leftChildIndex = (parentIndex * 2) + 1; 285 | if (this._compareAt(parentIndex, leftChildIndex) > 0) { 286 | return false; 287 | } 288 | isValidLeft = isValidRecursive(leftChildIndex); 289 | } 290 | 291 | if (this._hasRightChild(parentIndex)) { 292 | const rightChildIndex = (parentIndex * 2) + 2; 293 | if (this._compareAt(parentIndex, rightChildIndex) > 0) { 294 | return false; 295 | } 296 | isValidRight = isValidRecursive(rightChildIndex); 297 | } 298 | 299 | return isValidLeft && isValidRight; 300 | }; 301 | 302 | return isValidRecursive(0); 303 | } 304 | 305 | /** 306 | * Returns a shallow copy of the heap 307 | * @public 308 | * @returns {Heap} 309 | */ 310 | clone() { 311 | return new Heap(this._compare, this._nodes.slice(), this._leaf); 312 | } 313 | 314 | /** 315 | * Returns the root node in the heap 316 | * @public 317 | * @returns {number|string|object} 318 | */ 319 | root() { 320 | if (this.isEmpty()) { 321 | return null; 322 | } 323 | 324 | return this._nodes[0]; 325 | } 326 | 327 | /** 328 | * Returns the root node in the heap 329 | * @public 330 | * @returns {number|string|object} 331 | */ 332 | top() { 333 | return this.root(); 334 | } 335 | 336 | /** 337 | * Returns a leaf node in the heap 338 | * @public 339 | * @returns {number|string|object} 340 | */ 341 | leaf() { 342 | return this._leaf; 343 | } 344 | 345 | /** 346 | * Returns the number of nodes in the heap 347 | * @public 348 | * @returns {number} 349 | */ 350 | size() { 351 | return this._nodes.length; 352 | } 353 | 354 | /** 355 | * Checks if the heap is empty 356 | * @public 357 | * @returns {boolean} 358 | */ 359 | isEmpty() { 360 | return this.size() === 0; 361 | } 362 | 363 | /** 364 | * Clears the heap 365 | * @public 366 | */ 367 | clear() { 368 | this._nodes = []; 369 | this._leaf = null; 370 | } 371 | 372 | /** 373 | * Implements an iterable on the heap 374 | * @public 375 | */ 376 | [Symbol.iterator]() { 377 | let size = this.size(); 378 | return { 379 | next: () => { 380 | size -= 1; 381 | return { 382 | value: this.pop(), 383 | done: size === -1 384 | }; 385 | } 386 | }; 387 | } 388 | 389 | /** 390 | * Builds a heap from a array of values 391 | * @public 392 | * @static 393 | * @param {array} values 394 | * @param {function} compare 395 | * @returns {Heap} 396 | */ 397 | static heapify(values, compare) { 398 | if (!Array.isArray(values)) { 399 | throw new Error('Heap.heapify expects an array of values'); 400 | } 401 | 402 | if (typeof compare !== 'function') { 403 | throw new Error('Heap.heapify expects a compare function'); 404 | } 405 | 406 | return new Heap(compare, values).fix(); 407 | } 408 | 409 | /** 410 | * Checks if a list of values is a valid heap 411 | * @public 412 | * @static 413 | * @param {array} values 414 | * @param {function} compare 415 | * @returns {boolean} 416 | */ 417 | static isHeapified(values, compare) { 418 | return new Heap(compare, values).isValid(); 419 | } 420 | } 421 | 422 | exports.Heap = Heap; 423 | --------------------------------------------------------------------------------