├── .github └── workflows │ └── node.yml ├── .gitignore ├── LICENSE ├── README.md ├── bench.js ├── index.d.ts ├── index.js ├── package.json └── test.js /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Node 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v2 9 | 10 | - name: Setup Node 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: 14 14 | 15 | - name: Install dependencies 16 | run: npm install 17 | 18 | - name: Run tests 19 | run: npm test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | yarn.lock 3 | pnpm-lock.yaml 4 | pacakge-lock.json 5 | node_modules 6 | index.umd.js 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2022, Vladimir Agafonkin 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flatqueue 2 | 3 | A very fast and tiny binary heap priority queue in JavaScript. 4 | 5 | Similar to [tinyqueue](https://github.com/mourner/tinyqueue/), 6 | but stores the queue as two flat arrays of items and their numeric priority values respectively 7 | (without a way to specify a comparator function). 8 | This makes the queue more limited, but several times faster. 9 | 10 | [![Build Status](https://github.com/mourner/flatqueue/workflows/Node/badge.svg?branch=master)](https://github.com/mourner/flatqueue/actions) 11 | [![Minzipped Size](https://badgen.net/bundlephobia/minzip/flatqueue)](https://esm.run/flatqueue) 12 | [![Simply Awesome](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects) 13 | 14 | ## Usage 15 | 16 | ```js 17 | const q = new FlatQueue(); 18 | 19 | for (let i = 0; i < items.length; i++) { 20 | // Push an item index and its priority value. You can push other values as well, 21 | // but storing only integers is much faster due to JavaScript engine optimizations. 22 | q.push(i, items[i].priority); 23 | } 24 | 25 | q.peekValue(); // Read the top item's priority value 26 | q.peek(); // Read the top item 27 | q.pop(); // Remove and return the top item 28 | ``` 29 | 30 | ## Install 31 | 32 | Install with `npm install flatqueue`, then use as a module: 33 | 34 | ```js 35 | import FlatQueue from 'flatqueue'; 36 | ``` 37 | 38 | Alternatively, use as a module in a browser directly: 39 | 40 | ```html 41 | 49 | ``` 50 | 51 | ## API 52 | 53 | ### `new FlatQueue()` 54 | 55 | Creates an empty queue object with the following methods and properties: 56 | 57 | ### `push(item, priority)` 58 | 59 | Adds `item` to the queue with the specified `priority`. 60 | 61 | `priority` must be a number. Items are sorted and returned from low to high priority. 62 | Multiple items with the same priority value can be added to the queue, but the queue is not stable 63 | (items with the same priority are not guaranteed to be popped in iteration order). 64 | 65 | ### `pop()` 66 | 67 | Removes and returns the item from the head of this queue, which is one of the items with the lowest priority. 68 | If this queue is empty, returns `undefined`. 69 | 70 | ### `peek()` 71 | 72 | Returns the item from the head of this queue without removing it. 73 | If this queue is empty, returns `undefined`. 74 | 75 | ### `peekValue()` 76 | 77 | Returns the priority value of the item at the head of this queue without removing it. 78 | If this queue is empty, returns `undefined`. 79 | 80 | ### `clear()` 81 | 82 | Removes all items from the queue. 83 | 84 | ### `shrink()` 85 | 86 | Shrinks the internal arrays to `this.length`. 87 | 88 | `pop()` and `clear()` calls don't free memory automatically to avoid unnecessary resize operations. 89 | This also means that items that have been added to the queue can't be garbage collected 90 | until a new item is pushed in their place, or this method is called. 91 | 92 | ### `length` 93 | 94 | Number of items in the queue. Read-only. 95 | 96 | ### `ids` 97 | 98 | An underlying array of items. Note that it can be bigger than the `length` as it's not eagerly cleared. 99 | 100 | ### `values` 101 | 102 | An underlying array of priority values. Note that it can be bigger than the `length` as it's not eagerly cleared. 103 | 104 | ### Using typed arrays 105 | 106 | If you know the maximum queue size beforehand, you can override the queue to use typed arrays for better performance and memory footprint. This makes it match the performance of the popular [heapify](https://github.com/luciopaiva/heapify) library. 107 | 108 | ```js 109 | const q = new FlatQueue(); 110 | q.ids = new Uint16Array(32); 111 | q.values = new Uint32Array(32); 112 | ``` 113 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | 2 | import FlatQueue from './index.js'; 3 | import TinyQueue from 'tinyqueue'; 4 | 5 | const N = 1000000; 6 | const K = 1000; 7 | 8 | const data = []; 9 | for (let i = 0; i < N; i++) data[i] = {value: Math.random()}; 10 | 11 | 12 | const q = new TinyQueue([], (a, b) => a.value - b.value); 13 | 14 | console.time(`tinyqueue push ${N}`); 15 | for (let i = 0; i < N; i++) q.push(data[i]); 16 | console.timeEnd(`tinyqueue push ${N}`); 17 | 18 | console.time(`tinyqueue pop ${N}`); 19 | for (let i = 0; i < N; i++) q.pop(); 20 | console.timeEnd(`tinyqueue pop ${N}`); 21 | 22 | console.time(`tinyqueue push/pop ${N}`); 23 | for (let i = 0; i < N; i += K) { 24 | for (let j = 0; j < K; j++) q.push(data[i + j]); 25 | for (let j = 0; j < K; j++) q.pop(); 26 | } 27 | console.timeEnd(`tinyqueue push/pop ${N}`); 28 | 29 | 30 | const f = new FlatQueue(); 31 | 32 | console.time(`flatqueue push ${N}`); 33 | for (let i = 0; i < N; i++) f.push(i, data[i].value); 34 | console.timeEnd(`flatqueue push ${N}`); 35 | 36 | console.time(`flatqueue pop ${N}`); 37 | for (let i = 0; i < N; i++) f.pop(); 38 | console.timeEnd(`flatqueue pop ${N}`); 39 | 40 | console.time(`flatqueue push/pop ${N}`); 41 | for (let i = 0; i < N; i += K) { 42 | for (let j = 0; j < K; j++) f.push(i, data[i + j].value); 43 | for (let j = 0; j < K; j++) f.pop(); 44 | } 45 | console.timeEnd(`flatqueue push/pop ${N}`); 46 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export default class FlatQueue { 2 | /** 3 | * Number of items in the queue. 4 | */ 5 | readonly length: number; 6 | 7 | constructor(); 8 | 9 | /** 10 | * Removes all items from the queue. 11 | */ 12 | clear(): void; 13 | 14 | /** 15 | * Adds `item` to the queue with the specified `priority`. 16 | * 17 | * `priority` must be a number. Items are sorted and returned from low to 18 | * high priority. Multiple items with the same priority value can be added 19 | * to the queue, but there is no guaranteed order between these items. 20 | */ 21 | push(item: T, priority: number): void; 22 | 23 | /** 24 | * Removes and returns the item from the head of this queue, which is one of 25 | * the items with the lowest priority. If this queue is empty, returns 26 | * `undefined`. 27 | */ 28 | pop(): T | undefined; 29 | 30 | /** 31 | * Returns the item from the head of this queue without removing it. If this 32 | * queue is empty, returns `undefined`. 33 | */ 34 | peek(): T | undefined; 35 | 36 | /** 37 | * Returns the priority value of the item at the head of this queue without 38 | * removing it. If this queue is empty, returns `undefined`. 39 | */ 40 | peekValue(): number | undefined; 41 | 42 | /** 43 | * Shrinks the internal arrays to `this.length`. 44 | * 45 | * `pop()` and `clear()` calls don't free memory automatically to avoid 46 | * unnecessary resize operations. This also means that items that have been 47 | * added to the queue can't be garbage collected until a new item is pushed 48 | * in their place, or this method is called. 49 | */ 50 | shrink(): void; 51 | } 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | export default class FlatQueue { 3 | 4 | constructor() { 5 | this.ids = []; 6 | this.values = []; 7 | this.length = 0; 8 | } 9 | 10 | clear() { 11 | this.length = 0; 12 | } 13 | 14 | push(id, value) { 15 | let pos = this.length++; 16 | 17 | while (pos > 0) { 18 | const parent = (pos - 1) >> 1; 19 | const parentValue = this.values[parent]; 20 | if (value >= parentValue) break; 21 | this.ids[pos] = this.ids[parent]; 22 | this.values[pos] = parentValue; 23 | pos = parent; 24 | } 25 | 26 | this.ids[pos] = id; 27 | this.values[pos] = value; 28 | } 29 | 30 | pop() { 31 | if (this.length === 0) return undefined; 32 | 33 | const top = this.ids[0]; 34 | this.length--; 35 | 36 | if (this.length > 0) { 37 | const id = this.ids[0] = this.ids[this.length]; 38 | const value = this.values[0] = this.values[this.length]; 39 | const halfLength = this.length >> 1; 40 | let pos = 0; 41 | 42 | while (pos < halfLength) { 43 | let left = (pos << 1) + 1; 44 | const right = left + 1; 45 | let bestIndex = this.ids[left]; 46 | let bestValue = this.values[left]; 47 | const rightValue = this.values[right]; 48 | 49 | if (right < this.length && rightValue < bestValue) { 50 | left = right; 51 | bestIndex = this.ids[right]; 52 | bestValue = rightValue; 53 | } 54 | if (bestValue >= value) break; 55 | 56 | this.ids[pos] = bestIndex; 57 | this.values[pos] = bestValue; 58 | pos = left; 59 | } 60 | 61 | this.ids[pos] = id; 62 | this.values[pos] = value; 63 | } 64 | 65 | return top; 66 | } 67 | 68 | peek() { 69 | if (this.length === 0) return undefined; 70 | return this.ids[0]; 71 | } 72 | 73 | peekValue() { 74 | if (this.length === 0) return undefined; 75 | return this.values[0]; 76 | } 77 | 78 | shrink() { 79 | this.ids.length = this.values.length = this.length; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flatqueue", 3 | "version": "2.0.3", 4 | "description": "The smallest and simplest JavaScript priority queue", 5 | "author": "Vladimir Agafonkin", 6 | "license": "ISC", 7 | "type": "module", 8 | "main": "index.umd.js", 9 | "module": "index.js", 10 | "sideEffects": false, 11 | "exports": "./index.js", 12 | "types": "./index.d.ts", 13 | "devDependencies": { 14 | "eslint": "^8.12.0", 15 | "eslint-config-mourner": "^3.0.0", 16 | "rollup": "^2.70.1", 17 | "tinyqueue": "^2.0.3", 18 | "uvu": "^0.5.3" 19 | }, 20 | "scripts": { 21 | "pretest": "eslint index.js test.js", 22 | "test": "node test.js", 23 | "build": "rollup index.js -o index.umd.js -n FlatQueue -f umd", 24 | "prepublishOnly": "npm run build" 25 | }, 26 | "eslintConfig": { 27 | "extends": "mourner" 28 | }, 29 | "files": [ 30 | "index.js", 31 | "index.umd.js", 32 | "index.d.ts" 33 | ], 34 | "engines": { 35 | "node": ">= 12.17.0" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/mourner/flatqueue.git" 40 | }, 41 | "keywords": [ 42 | "algorithms", 43 | "data structures", 44 | "priority queue", 45 | "binary heap" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 2 | import {test} from 'uvu'; 3 | import * as assert from 'uvu/assert'; 4 | 5 | import FlatQueue from './index.js'; 6 | 7 | const data = []; 8 | for (let i = 0; i < 100; i++) { 9 | data.push(Math.floor(100 * Math.random())); 10 | } 11 | 12 | const sorted = data.slice().sort((a, b) => a - b); 13 | 14 | test('maintains a priority queue', () => { 15 | const queue = new FlatQueue(); 16 | for (let i = 0; i < data.length; i++) queue.push(i, data[i]); 17 | 18 | assert.is(queue.peekValue(), sorted[0]); 19 | assert.is(data[queue.peek()], sorted[0]); 20 | 21 | const result = []; 22 | while (queue.length) result.push(data[queue.pop()]); 23 | 24 | assert.equal(result, sorted); 25 | }); 26 | 27 | test('handles edge cases with few elements', () => { 28 | const queue = new FlatQueue(); 29 | 30 | queue.push(0, 2); 31 | queue.push(1, 1); 32 | queue.pop(); 33 | queue.pop(); 34 | queue.pop(); 35 | queue.push(2, 2); 36 | queue.push(3, 1); 37 | assert.is(queue.pop(), 3); 38 | assert.is(queue.pop(), 2); 39 | assert.is(queue.pop(), undefined); 40 | assert.is(queue.peek(), undefined); 41 | assert.is(queue.peekValue(), undefined); 42 | }); 43 | 44 | test('shrinks internal arrays when calling shrink', () => { 45 | const queue = new FlatQueue(); 46 | 47 | for (let i = 0; i < 10; i++) queue.push(i, i); 48 | 49 | while (queue.length) queue.pop(); 50 | 51 | assert.is(queue.ids.length, 10); 52 | assert.is(queue.values.length, 10); 53 | 54 | queue.shrink(); 55 | 56 | assert.is(queue.ids.length, 0); 57 | assert.is(queue.values.length, 0); 58 | }); 59 | 60 | test.run(); 61 | --------------------------------------------------------------------------------