├── .travis.yml ├── LICENSE ├── package.json ├── benchmark.js ├── README.md ├── qheap.js └── test-qheap.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - 0.10 5 | - 4 6 | - 6 7 | - 8 8 | - 10 9 | - 12 10 | - 13 11 | after_success: 12 | - if [ `node -p 'process.version.slice(0, 3)'` != "v8." ]; then exit; fi 13 | - npm install -g nyc codecov coveralls 14 | - nyc --reporter lcov npm test && codecov 15 | - nyc report -r text-lcov | coveralls 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013-2021 Andras Radics 2 | andras at andrasq dot com 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qheap", 3 | "version": "1.5.0-pre", 4 | "description": "binary heap priority queue", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/andrasq/node-qheap" 9 | }, 10 | "main": "qheap.js", 11 | "author": { 12 | "name": "Andras", 13 | "url": "http://github.com/andrasq" 14 | }, 15 | "engines": { 16 | "node": "*" 17 | }, 18 | "readme": "see Readme.md", 19 | "files": ["qheap.js", "LICENSE", "README.md"], 20 | 21 | "scripts": { 22 | "test": "qnit -t 10000 test-*", 23 | "coverage": "env NODE_COVERAGE=1 nyc --reporter lcov --reporter text npm test", 24 | "clean": "rm -rf .nyc_output coverage" 25 | }, 26 | "dependencies": { 27 | }, 28 | "devDependencies": { 29 | "qnit": "0.16.0", 30 | "qtimeit": "0.19.0" 31 | }, 32 | "keywords": [ 33 | "Andras", 34 | "binary", 35 | "heap", 36 | "priority queue" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017-2021 Andras Radics 3 | * Licensed under the Apache License, Version 2.0 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var timeit = require('qtimeit'); 9 | 10 | var fastpriorityqueue = require('fastpriorityqueue'); 11 | var qheap = require('./'); 12 | var heap = require('heap'); 13 | var jsprioqueue = require('js-priority-queue'); 14 | 15 | // rand() from `fastpriorityqueue` 16 | function rand(i) { 17 | i = i + 10000; 18 | i = i ^ (i << 16); 19 | i = (i >> 5) ^ i ; 20 | return i & 0xFF; 21 | } 22 | 23 | function testLoop() { 24 | var i, b = qheap(); 25 | for (i = 0; i < 128; i++) { b.insert(rand(i)) } 26 | for (i = 128; i < 1280; i++) { b.insert(rand(i)) ; b.remove() } 27 | } 28 | 29 | function comparAfter(a, b) { return a > b } 30 | function comparAfter2(a, b) { return (a > b) } 31 | function comparSubtract(a, b) { return a - b } 32 | function comparOrder(a, b) { return a < b ? -1 : 1 } 33 | function comparBefore(a, b) { return a < b } 34 | function comparBefore2(a, b) { return (a < b) } 35 | 36 | 37 | timeit.bench.visualize = true; 38 | 39 | var x; 40 | //console.log("new heap"); 41 | timeit.bench.preRunMessage = "new heap"; 42 | timeit.bench.timeGoal = 0.25; 43 | /** 44 | timeit.bench({ 45 | heap: function(){ 46 | x = new heap() }, 47 | jspriorityqueue: function() { 48 | x = new jsprioqueue(); }, 49 | fastpriorityqueue_compar: function(){ 50 | x = new fastpriorityqueue(comparAfter) }, 51 | qheap: function(){ 52 | x = qheap() }, 53 | }); 54 | **/ 55 | 56 | // just creating a qheap with different compar functions cuts performance 6x 57 | // fastpriorityqueue same behavior 58 | // x = qheap({ comparBefore: comparBefore }); 59 | 60 | timeit.bench.preRunMessage = 61 | rand.toString() + "\n" + 62 | testLoop.toString() + "\n" + 63 | "\n" + 64 | "new heap + 128 inserts + 1152 insert/removes"; 65 | timeit.bench.timeGoal = .2; 66 | //timeit.bench.baselineAvg = 20000 * 1280; 67 | //timeit.bench.forkTests = true; 68 | //timeit.bench.verbose = 5; 69 | for (var i=0; i<20; i++) { 70 | timeit.bench({ 71 | // the test from `fastpriorityqueue` 72 | jsprioqueue: function(){ 73 | var i, b = new jsprioqueue(); 74 | for (i = 0; i < 128; i++) { b.queue(rand(i)) } 75 | for (i = 128; i < 1280; i++) { b.queue(rand(i)) ; b.dequeue() } 76 | }, 77 | heap: function(){ 78 | var i, b = new heap(); 79 | for (i = 0; i < 128; i++) { b.push(rand(i)) } 80 | for (i = 128; i < 1280; i++) { b.push(rand(i)) ; b.pop() } 81 | }, 82 | /** 83 | // much slower with custom comparator 84 | heap_compar: function(){ 85 | var i, b = new heap(function(a,b){ return a - b }); 86 | for (i = 0; i < 128; i++) { b.push(rand(i)) } 87 | for (i = 128; i < 1280; i++) { b.push(rand(i)) ; b.pop() } 88 | }, 89 | **/ 90 | qheap: function(){ 91 | var i, b = qheap(); 92 | for (i = 0; i < 128; i++) { b.insert(rand(i)) } 93 | for (i = 128; i < 1280; i++) { b.insert(rand(i)) ; b.remove() } 94 | }, 95 | fastpriorityqueue: function(){ 96 | var i, b = new fastpriorityqueue(comparAfter); 97 | //var i, b = new fastpriorityqueue(); 98 | for (i = 0; i < 128; i++) { b.add(rand(i)) } 99 | for (i = 128; i < 1280; i++) { b.add(rand(i)) ; b.poll() } 100 | }, 101 | qheap2: function(){ 102 | var i, b = qheap(); 103 | for (i = 0; i < 128; i++) { b.insert(rand(i)) } 104 | for (i = 128; i < 1280; i++) { b.insert(rand(i)) ; b.remove() } 105 | }, 106 | 107 | // note: toggling between two comparator functions de-optimizes the code to run 5x slower 108 | // constructing new fastpriorityqueue with different comparators also kills performance, 6x slower 109 | // Maybe could build custom insert / remove methods on the fly to work with the comparator, 110 | // to be optimized separately and not affect the base class methods? 111 | /** 112 | fastpriorityqueue_compar2: function(){ 113 | var i, b = new fastpriorityqueue(comparAfter2); 114 | for (i = 0; i < 128; i++) { b.add(rand(i)) } 115 | for (i = 128; i < 1280; i++) { b.add(rand(i)) ; b.poll() } 116 | }, 117 | qheap_compar: function(){ 118 | var i, b = qheap({ comparBefore: comparBefore }); 119 | for (i = 0; i < 128; i++) { b.insert(rand(i)) } 120 | for (i = 128; i < 1280; i++) { b.insert(rand(i)) ; b.remove() } 121 | }, 122 | qheap_compar2: function(){ 123 | var i, b = qheap({ comparBefore: comparBefore2 }); 124 | for (i = 0; i < 128; i++) { b.insert(rand(i)) } 125 | for (i = 128; i < 1280; i++) { b.insert(rand(i)) ; b.remove() } 126 | }, 127 | /**/ 128 | 'new qheap 10k': function() { 129 | var h; 130 | for (var i=0; i<10000; i++) h = qheap(); 131 | }, 132 | 'qheap insert 10k': function() { 133 | var h = qheap(); 134 | for (var i=0; i<10000; i++) h.insert(rand(i)); 135 | }, 136 | 'qheap insert 10k / remove 10k': function() { 137 | var h = qheap(); 138 | for (var i=0; i<10000; i++) h.insert(rand(i)); 139 | for (var i=0; i<10000; i++) h.shift(); 140 | }, 141 | }); 142 | timeit.bench.preRunMessage = "----"; 143 | timeit.bench.showPlatformInfo = false; 144 | timeit.bench.showRunDetails = false; 145 | //timeit.bench.verbose = 1; 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | qheap 2 | ===== 3 | 4 | [![Build Status](https://api.travis-ci.org/andrasq/node-qheap.svg?branch=master)](https://travis-ci.org/andrasq/node-qheap) 5 | [![Coverage Status](https://coveralls.io/repos/github/andrasq/node-qheap/badge.svg?branch=master)](https://coveralls.io/github/andrasq/node-qheap?branch=master) 6 | 7 | Qheap is a very fast classic heap / priority queue. 8 | 9 | A heap is partially ordered balanced binary tree with the property that the 10 | value at the root comes before any value in either the left or right subtrees. 11 | It supports two operations, insert and remove, both O(log n) (and peek, O(1)). 12 | The tree can be efficiently mapped into an array: the root at offset `[1]` and 13 | each node at `[n]` having children Left at `[2*n]` and Right at `[2*n + 1]`. 14 | 15 | var Heap = require('qheap'); 16 | var h = new Heap(); 17 | h.insert('c'); 18 | h.insert('a'); 19 | h.insert('b'); 20 | h.remove(); // => 'a' 21 | 22 | 23 | Api 24 | --- 25 | 26 | ### new Heap( options ) 27 | 28 | create a new empty heap. 29 | 30 | Options: 31 | 32 | - `comparBefore`: a fast comparison function that returns true when the first 33 | argument should be sorted before the second. This comparator runs much faster than 34 | the three-way `compar` below. The default is `function(a,b) { return a < b }`. 35 | - `compar` : comparison function to determine the item ordering. The function 36 | should return a value less than zero if the first argument should be ordered 37 | before the second (compatible with the function passed to `sort()`). The 38 | default ordering if no compar is specified is by `<`: `function(a,b){ return 39 | a < b ? -1 : 1 }` 40 | 41 | - `freeSpace` : when the heap shrinks to 1/4 its high-water mark, reallocate the 42 | storage space to free the unused memory, and reset the high-water mark. 43 | Default is false, avoiding the overhead of the array slice. Note: freeing 44 | space from the array halves the insert rate; use advisedly. 45 | 46 | - `size` : the initial capacity of the heap. The heap is extended as needed, 47 | but for debugging dumps it can be useful to specify a smaller starting size than 48 | the default 100. 49 | 50 | If options is a function, it is taken to be the comparison function `compar`. 51 | 52 | ### insert( item ), push( item ), enqueue( item ) 53 | 54 | insert the item into the heap and rebalance the tree. Item can be anything, 55 | only the `compar` function needs to know the actual type. 56 | Push is an alias for insert. 57 | 58 | ### remove( ), shift( ), dequeue( ) 59 | 60 | remove and return the item at the root of the heap (the next item in the 61 | sequence), and rebalance the tree. When empty, returns `undefined`. 62 | Shift is an alias for remove. 63 | 64 | ### peek( ) 65 | 66 | return the item at the root of the heap, but do not remove it. When empty, 67 | returns `undefined`. 68 | 69 | ### length 70 | 71 | the heap `length` property is the count of items currently in the heap. This 72 | is a read-only property, it must not be changed. 73 | 74 | ### gc( [options] ) 75 | 76 | Resize the storage array to free all unused array slots. The options, if 77 | specified, control when to gc for more efficient operation. 78 | 79 | Options: 80 | 81 | - `minLength` - do not resize arrays smaller than this cutoff. 82 | Default 0, resize even the smallest arrays. 83 | - `minFull` - do not resize arrays that are more full than this fraction. 84 | Default 1.00, resize unless 100% full. 85 | 86 | ### toArray( ) 87 | 88 | Return a snapshot of the raw, unsorted contents of the internal storage array. 89 | The returned array will be sized to the number of contained elements, 0 .. length - 1. 90 | 91 | The equivalent function to insert the contents of an array into the heap is just 92 | a loop over the elements: 93 | 94 | array.forEach((e) => heap.insert(e)); 95 | 96 | 97 | Performance 98 | ----------- 99 | 100 | Running the `fastpriorityqueue` benchmark: 101 | 102 | function rand(i) { 103 | i = i + 10000; 104 | i = i ^ (i << 16); 105 | i = (i >> 5) ^ i ; 106 | return i & 0xFF; 107 | } 108 | function testLoop() { 109 | var i, b = new qheap(); 110 | for (i = 0; i < 128; i++) { b.insert(rand(i)) } 111 | for (i = 128; i < 1280; i++) { b.insert(rand(i)) ; b.remove() } 112 | } 113 | 114 | new heap + 128 inserts + 1152 insert/removes 115 | qtimeit=0.19.0 node=6.10.2 v8=5.1.281.98 platform=linux kernel=3.16.0-4-amd64 up_threshold=11 116 | arch=ia32 mhz=4419 cpuCount=8 cpu="Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz" 117 | name speed rate 118 | jsprioqueue 6,037 ops/sec 1000 >>>>> 119 | heap 29,913 ops/sec 4955 >>>>>>>>>>>>>>>>>>>>>>>>> 120 | fastpriorityqueue 38,264 ops/sec 6338 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 121 | qheap 41,139 ops/sec 6814 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 122 | 123 | 124 | Changelog 125 | --------- 126 | 127 | - 1.5.0-pre - new experimental methods; simplify file hierarchy, slightly faster insert 128 | - 1.4.1 - minor speedups, backfill changelog 129 | - 1.4.0 - add test tools to devDependencies 130 | - 1.3.4 - 100% test coverage 131 | - 1.3.0 - `options.comparBefore` comparator, zero out removed array slots 132 | - 1.2.0 - allow a compar function instead of options, fix off-by-one 133 | 134 | 135 | Todo 136 | ---- 137 | 138 | - might be more efficient to periodically gc the heap on a timer instead of checking 139 | on every remove 140 | - hashify([array]), toArray(), fromArray() (emulate the common `hash` methods) 141 | 142 | 143 | Related Work 144 | ------------ 145 | 146 | - [heap](https://www.npmjs.com/package/heap) - the classic, slow with custom comparator 147 | - [qheap, this](https://www.npmjs.org/package/qheap) - very fast heap and priority queue 148 | - [fastpriorityqueue](https://www.npmjs.com/package/fastpriorityqueue) - very fast, includes comprehensive list nodejs heaps 149 | - [qlist](https://www.npmjs.com/package/qlist) - very fast circular buffer 150 | -------------------------------------------------------------------------------- /qheap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * nodejs heap, classic array implementation 3 | * 4 | * Items are stored in a balanced binary tree packed into an array where 5 | * node is at [i], left child is at [2*i], right at [2*i+1]. Root is at [1]. 6 | * 7 | * Copyright (C) 2014-2021 Andras Radics 8 | * Licensed under the Apache License, Version 2.0 9 | */ 10 | 11 | 'use strict'; 12 | 13 | module.exports = Heap; 14 | 15 | function isBeforeDefault( a, b ) { return a < b; } 16 | 17 | function Heap( opts ) { 18 | if (!(this instanceof Heap)) return new Heap(opts); 19 | 20 | if (typeof opts === 'function') opts = {compar: opts}; 21 | 22 | // copy out known options to not bind to caller object 23 | this.options = !opts ? {} : { 24 | compar: opts.compar, 25 | comparBefore: opts.comparBefore, 26 | freeSpace: opts.freeSpace, 27 | size: opts.size, 28 | }; 29 | opts = this.options; 30 | 31 | var self = this; 32 | this._isBefore = opts.compar ? function(a, b) { return opts.compar(a, b) < 0 } : opts.comparBefore || isBeforeDefault; 33 | this._sortBefore = opts.compar || function(a, b) { return self._isBefore(a, b) ? -1 : 1 }; 34 | this._freeSpace = opts.freeSpace ? this._trimArraySize : false; 35 | 36 | this._list = new Array(opts.size || 20); 37 | // 14% slower to mix ints and pointers in an array, even if deleted 38 | // this._list[0] = undefined; 39 | 40 | this.length = 0; 41 | } 42 | 43 | Heap.prototype._list = null; 44 | Heap.prototype._compar = null; 45 | Heap.prototype._isBefore = null; 46 | Heap.prototype._freeSpace = null; 47 | Heap.prototype._sortBefore = null; 48 | Heap.prototype.length = 0; 49 | 50 | /* 51 | * insert new item at end, and bubble up 52 | */ 53 | Heap.prototype.insert = function Heap_insert( item ) { 54 | var idx = ++this.length; 55 | return this._bubbleup(idx, item); 56 | }; 57 | Heap.prototype._bubbleup = function _bubbleup( idx, item ) { 58 | var list = this._list; 59 | list[idx] = item; 60 | if (idx <= 1) return; 61 | do { 62 | var pp = idx >>> 1; 63 | if (this._isBefore(item, list[pp])) list[idx] = list[pp]; else break; 64 | idx = pp; 65 | } while (idx > 1); 66 | list[idx] = item; 67 | }; 68 | Heap.prototype.append = Heap.prototype.insert; 69 | Heap.prototype.push = Heap.prototype.insert; 70 | Heap.prototype.unshift = Heap.prototype.insert; 71 | Heap.prototype.enqueue = Heap.prototype.insert; 72 | 73 | Heap.prototype.peek = function Heap_peek( ) { 74 | return this.length > 0 ? this._list[1] : undefined; 75 | }; 76 | 77 | Heap.prototype.size = function Heap_size( ) { 78 | return this.length; 79 | }; 80 | 81 | /* 82 | * return the root, and bubble down last item from top root position 83 | * when bubbling down, r: root idx, c: child sub-tree root idx, cv: child root value 84 | * Note that the child at (c == this.length) does not have to be tested in the loop, 85 | * since its value is the one being bubbled down, so can loop `while (c < len)`. 86 | */ 87 | Heap.prototype.remove = function Heap_remove( ) { 88 | var len = this.length; 89 | if (len < 1) return undefined; 90 | return this._bubbledown(1, len); 91 | 92 | /** 93 | // experiment: ripple down hole from root, bubble up last from hole 94 | var list = this._list; 95 | var ret = list[1]; 96 | var holeIdx = this._rippleup(1, len); 97 | this._bubbleup(holeIdx, list[this.length--]); 98 | if (this._freeSpace) this._freeSpace(list, len); 99 | return ret; 100 | /**/ 101 | }; 102 | Heap.prototype._bubbledown = function _bubbledown( r, len ) { 103 | var list = this._list, ret = list[r], itm = list[len]; 104 | var c, _isBefore = this._isBefore; 105 | 106 | while ((c = r << 1) < len) { 107 | var cv = list[c], cv1 = list[c+1]; 108 | if (_isBefore(cv1, cv)) { c++; cv = cv1; } 109 | if (!(_isBefore(cv, itm))) break; 110 | list[r] = cv; 111 | r = c; 112 | } 113 | list[r] = itm; 114 | list[len] = 0; 115 | this.length = --len; 116 | if (this._freeSpace) this._freeSpace(this._list, this.length); 117 | 118 | return ret; 119 | }; 120 | /** 121 | Heap.prototype._rippleup = function _rippleup( r, len ) { 122 | var list = this._list; 123 | 124 | var c, _isBefore = this._isBefore; 125 | while ((c = r << 1) < len) { 126 | var cv = list[c]; 127 | var cv1 = list[c+1]; 128 | if (_isBefore(cv1, cv)) { cv = cv1; c = c+1 } 129 | list[r] = cv; 130 | r = c; 131 | } 132 | if (c === len) { 133 | list[r] = list[c]; 134 | r = c; 135 | } 136 | 137 | return r; 138 | }; 139 | /**/ 140 | Heap.prototype.shift = Heap.prototype.remove; 141 | Heap.prototype.pop = Heap.prototype.remove; 142 | Heap.prototype.dequeue = Heap.prototype.remove; 143 | 144 | // builder, not initializer: appends items, not replaces 145 | // FIXME: more useful to re-initialize from array 146 | Heap.prototype.fromArray = function fromArray( array, base, bound ) { 147 | var base = base || 0; 148 | var bound = bound || array.length; 149 | for (var i=base; ik, with k/i probability. 211 | // Eg: pick 2 [1,2,3]: get [1,2], replace 3 with 2/3 probability into slot [0] or [1]. 212 | // Note that this._list is indexed 1..N, but samples are indexed 0..k-1 213 | // Note: if k is much smaller than .length, would be faster to 214 | // generate k unique random array indexes than N random values. 215 | Heap.prototype.subsample = function subsample( k, options ) { 216 | options = options || {}; 217 | var samples = new Array(); 218 | 219 | if (k > this.length) k = this.length; 220 | for (var i = 1; i <= k; i++) samples[i - 1] = this._list[i]; 221 | for (var i = k + 1; i <= this.length; i++) { 222 | var j = Math.floor(Math.random() * i); 223 | if (j < k) samples[j] = this._list[i]; 224 | } 225 | 226 | if (options.sort) samples.sort(this._sortBefore); 227 | return samples; 228 | } 229 | 230 | /* 231 | * Free unused storage slots in the _list. 232 | * The default is to unconditionally gc, use the options to omit when not useful. 233 | */ 234 | Heap.prototype.gc = function Heap_gc( options ) { 235 | if (!options) options = {}; 236 | 237 | var minListLength = options.minLength; // smallest list that will be gc-d 238 | if (minListLength === undefined) minListLength = 0; 239 | 240 | var minListFull = options.minFull; // list utilization below which to gc 241 | if (minListFull === undefined) minListFull = 1.00; 242 | 243 | if (this._list.length >= minListLength && (this.length < this._list.length * minListFull)) { 244 | // gc reallocates the array to free the unused storage slots at the end 245 | // use splice to actually free memory; 7% slower than setting .length 246 | // note: list.slice makes the array slower to insert to?? splice is better 247 | this._list.splice(this.length+1, this._list.length); 248 | } 249 | } 250 | 251 | Heap.prototype._trimArraySize = function Heap__trimArraySize( list, len ) { 252 | if (len > 10000 && list.length > 4 * len) { 253 | // use slice to actually free memory; 7% slower than setting .length 254 | // note: list.slice makes the array slower to insert to?? splice is better 255 | list.splice(len+1, list.length); 256 | } 257 | } 258 | 259 | Heap.prototype._check = function Heap__check( ) { 260 | var isBefore = this._isBefore; 261 | var _compar = this._sortBefore; 262 | 263 | var i, p, fail = 0; 264 | for (i=this.length; i>1; i--) { 265 | // error if parent should go after child, but not if don`t care 266 | p = i >>> 1; 267 | // swapping the values must change their ordering, otherwise the 268 | // comparison is a tie. (Ie, consider the ordering func (a <= b) 269 | // that for some values reports both that a < b and b < a.) 270 | if (_compar(this._list[p], this._list[i]) > 0 && 271 | _compar(this._list[i], this._list[p]) < 0) 272 | { 273 | fail = i; 274 | } 275 | } 276 | if (fail) console.log("failed at", (fail >>> 1), fail); 277 | return !fail; 278 | } 279 | 280 | // optimize access 281 | Heap.prototype = toStruct(Heap.prototype); 282 | function toStruct(o) { return toStruct.prototype = o } 283 | -------------------------------------------------------------------------------- /test-qheap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014-2020 Andras Radics 3 | * Licensed under the Apache License, Version 2.0 4 | */ 5 | 6 | var assert = require('assert'); 7 | 8 | var Heap = require('./'); 9 | 10 | function insertArray( l, a ) { 11 | for (var i=0; i= item, i + ": " + x + " should be >= " + item); 158 | assert(x >= item); 159 | item = x; 160 | } 161 | t.equal(this.cut.remove(), undefined); 162 | t.equal(this.cut.length, 0); 163 | t.done(); 164 | }, 165 | 166 | 'should insert fromArray': function(t) { 167 | var h = new Heap(); 168 | h.fromArray([3]); 169 | h.fromArray([2, 1]); 170 | t.equal(h.shift(), 1); 171 | t.equal(h.shift(), 2); 172 | t.equal(h.length, 1); 173 | t.done(); 174 | }, 175 | 176 | 'should convert toArray': function(t) { 177 | var l = new Heap(); 178 | t.deepEqual(l.toArray(), []); 179 | l.insert(1); 180 | t.deepEqual(l.toArray(), [1]); 181 | l.insert(2); 182 | t.deepEqual(l.toArray().sort(), [1, 2]); 183 | l.insert(3); 184 | l.insert(4); 185 | t.deepEqual(l.toArray().sort(), [1, 2, 3, 4]); 186 | t.deepEqual(l.toArray(0).sort(), []); 187 | t.deepEqual(l.toArray(1).sort(), [1]); 188 | t.deepEqual(l.toArray(2).sort(), [1, 2]); 189 | t.done(); 190 | }, 191 | 192 | 'should copy': function(t) { 193 | var l1 = new Heap(); 194 | l1.push(1); 195 | l1.push(2); 196 | l1.push(3); 197 | l1.otherProperty = 123; 198 | var l2 = l1.copy(); 199 | l2.pop(); 200 | l2.push(0); 201 | t.deepEqual(l1.toArray().sort(), [1,2,3]); 202 | t.deepEqual(l2.toArray().sort(), [0,2,3]); 203 | t.strictEqual(l2.otherProperty, undefined); 204 | t.done(); 205 | }, 206 | 207 | 'should peekHead': function(t) { 208 | var l1 = new Heap(); 209 | insertArray(l1, [2, 1, 3, 5, 4]); 210 | t.deepEqual(l1.peekHead(1), [1]); 211 | t.deepEqual(l1.peekHead(2), [1, 2]); 212 | t.deepEqual(l1.peekHead(3), [1, 2, 3]); 213 | t.deepEqual(l1.peekHead(4), [1, 2, 3, 4]); 214 | t.deepEqual(l1.peekHead(5), [1, 2, 3, 4, 5]); 215 | t.deepEqual(l1.peekHead(6), [1, 2, 3, 4, 5]); 216 | t.deepEqual(l1.toArray().sort(), [1, 2, 3, 4, 5]); 217 | t.done() 218 | }, 219 | 220 | 'should sort': function(t) { 221 | var l1 = new Heap(); 222 | l1.sort(); 223 | t.deepEqual(l1.toArray(), []); 224 | 225 | l1.push(3); 226 | l1.sort(); 227 | t.deepEqual(l1.toArray(), [3]); 228 | 229 | l1.push(2); 230 | l1.sort(); 231 | t.deepEqual(l1.toArray(), [2, 3]); 232 | 233 | l1.push(1); 234 | t.deepEqual(l1.toArray(), [1, 3, 2]); 235 | l1.sort(); 236 | t.deepEqual(l1.toArray(), [1, 2, 3]); 237 | t.equal(l1.shift(), 1); 238 | t.equal(l1.shift(), 2); 239 | t.equal(l1.shift(), 3); 240 | 241 | var l2 = new Heap(); 242 | for (var i=0; i<1000; i++) l2.insert(Math.random()); 243 | var timeit = require('qtimeit'); 244 | timeit(1000, function() { l2.sort() }); 245 | 246 | t.done(); 247 | }, 248 | 249 | 'subsample': { 250 | 'should subsample': function(t) { 251 | sampleit([], -1, 0); 252 | sampleit([], 0, 0); 253 | sampleit([], 1, 0); 254 | 255 | sampleit([1], 0, 0); 256 | sampleit([1], 1, 1); 257 | sampleit([1], 3, 1); 258 | sampleit([1, 2], 1, 1); 259 | sampleit([1, 2], 2, 2); 260 | 261 | sampleit([1, 2, 3, 4], -1, 0); 262 | sampleit([1, 2, 3, 4], 0, 0); 263 | sampleit([1, 2, 3, 4], 1, 1); 264 | sampleit([1, 2, 3, 4], 2, 2); 265 | sampleit([1, 2, 3, 4], 4, 4); 266 | sampleit([1, 2, 3, 4], 5, 4); 267 | sampleit([1, 2, 3, 4], 99, 4); 268 | 269 | h = new Heap(); 270 | insertArray(h, [3, 2, 1]); 271 | t.deepEqual(h.subsample(3), [1, 3, 2]); 272 | t.deepEqual(h.subsample(3, { sort: true }), [1, 2, 3]); 273 | 274 | t.done(); 275 | 276 | function sampleit(array, limit, length) { 277 | var h = new Heap(); 278 | insertArray(h, array); 279 | for (var i=0; i<100; i++) { 280 | var samp = h.subsample(limit).sort(); 281 | t.equal(samp.length, length); 282 | t.contains(array, samp); 283 | } 284 | } 285 | }, 286 | 287 | 'should subsample fairly': function(t) { 288 | var h = new Heap(); 289 | var counts = []; 290 | 291 | for (var i=1; i<10; i++) { h.insert(i); counts[i] = 0 } 292 | 293 | for (var i=0; i<100000; i++) { 294 | var samp = h.subsample(2); 295 | counts[samp[0]] += 1; 296 | counts[samp[1]] += 1; 297 | } 298 | var min = Math.min.apply(null, counts.slice(1)); 299 | var max = Math.max.apply(null, counts.slice(1)); 300 | // no more than 1% over 100k 301 | t.ok(max - min < 1000); 302 | t.done(); 303 | }, 304 | }, 305 | 306 | 'gc': { 307 | 308 | 'should always gc if no options': function(t) { 309 | var h = new Heap(); 310 | h.push(1); 311 | h.gc(); 312 | t.equal(h._list.length, 2); 313 | t.done(); 314 | }, 315 | 316 | 'should gc if meet options': function(t) { 317 | var h = new Heap({ size: 10 }) 318 | h.gc({ minLength: 0, minFull: 0.00001 }); 319 | t.equal(h._list.length, 1); // list[0] is always present but is unused 320 | t.done(); 321 | }, 322 | 323 | 'should not gc if list is too small': function(t) { 324 | var h = new Heap({ size: 10 }); 325 | h.gc({ minLength: 100 }); 326 | t.equal(h._list.length, 10); 327 | t.done(); 328 | }, 329 | 330 | 'should not gc if list utilization is high': function(t) { 331 | var h = new Heap(); 332 | for (var i=0; i<20001; i++) h.push(i); 333 | for (var i=0; i<5000; i++) h.shift(); 334 | h.gc({ minFull: 0.50 }); 335 | t.equal(h._list.length, 20002); 336 | t.done(); 337 | }, 338 | 339 | }, 340 | 341 | 'options': { 342 | 'should accept comparator': function(t) { 343 | var compar = t.spy(); 344 | var h = new Heap(compar); 345 | h.push(1); 346 | h.push(2); 347 | t.equal(compar.stub.callCount, 1); 348 | t.done(); 349 | }, 350 | 351 | 'should accept comparBefore': function(t) { 352 | var cmp = function(){}; 353 | var h = new Heap({ comparBefore: cmp }); 354 | t.equal(h._isBefore, cmp); 355 | t.done(); 356 | }, 357 | 358 | 'should accept size': function(t) { 359 | var h = new Heap({ size: 3 }); 360 | t.equal(h._list.length, 3); 361 | t.done(); 362 | }, 363 | 364 | 'should accept freeSpace': function(t) { 365 | var h = new Heap({ freeSpace: true }); 366 | t.ok(h._freeSpace); 367 | t.done(); 368 | }, 369 | 370 | 'if freeSpace should try to free on remove': function(t) { 371 | var h = new Heap({ freeSpace: true }); 372 | var spy = t.spy(h, '_freeSpace'); 373 | h.push(1); 374 | h.push(2); 375 | h.remove(); 376 | t.equal(spy.callCount, 1); 377 | t.done(); 378 | }, 379 | 380 | 'should always set _isBefore': function(t) { 381 | var fn = function(){}; 382 | var h1 = new Heap(); 383 | t.equal(typeof h1._isBefore, 'function'); 384 | var h2 = new Heap(fn); 385 | t.equal(typeof h2._isBefore, 'function'); 386 | t.ok(h2._isBefore != fn); 387 | var h3 = new Heap({ compar: fn }); 388 | t.equal(typeof h3._isBefore, 'function'); 389 | t.ok(h3._isBefore != fn); 390 | var h4 = new Heap({ comparBefore: fn }); 391 | t.equal(h4._isBefore, fn); 392 | t.done(); 393 | }, 394 | }, 395 | 396 | 'helpers': { 397 | '_trimArraySize should do nothing if array is small': function(t) { 398 | var h = new Heap(); 399 | var list = new Array(100); 400 | list[0] = 1234; 401 | list[99] = 99; 402 | h._trimArraySize(list, 1); 403 | t.equal(list.length, 100); 404 | t.equal(list[0], 1234); 405 | t.equal(list[99], 99); 406 | t.done(); 407 | }, 408 | 409 | '_trimArraySize should trim if array too large': function(t) { 410 | var h = new Heap(); 411 | var list = new Array(40005); 412 | list[0] = 1234; 413 | list[10001] = 567; 414 | h._trimArraySize(list, 10001); 415 | t.equal(list.length, 10002); 416 | t.equal(list[0], 1234); 417 | t.equal(list[10001], 567); 418 | t.done(); 419 | }, 420 | 421 | '_check should flag invalid heap': function(t) { 422 | var h = new Heap(); 423 | var h2 = new Heap(); 424 | h.push(1); h2.push(1); 425 | h.push(2); h2.push(2); 426 | // break h 427 | h._list[1] = 2; 428 | h._list[2] = 1; 429 | var stub = t.stub(console, 'log'); 430 | var ok = h._check(); 431 | var good = h2._check(); 432 | stub.restore(); 433 | t.assert(!ok); 434 | t.assert(good); 435 | t.done(); 436 | }, 437 | }, 438 | 439 | 'fuzz test': function(t) { 440 | if (process.env.NODE_COVERAGE) return t.done(); 441 | for (var nitems=2; nitems<8; nitems++) { 442 | for (var loop=0; loop<20000; loop++) { 443 | var cut = new Heap({size: 4}); 444 | for (var i=0; i> 0); 447 | t.ok(cut._check()); 448 | t.ok(cut.length, i+1); 449 | } 450 | for (var i=0; i