Skip to content

Commit 6f6f7f7

Browse files
committed
lib: add internal PriorityQueue class
An efficient JS implementation of a binary heap on top of an array with worst-case O(log n) runtime for all operations, including arbitrary item removal (unlike O(n) for most binary heap array implementations). PR-URL: #20555 Fixes: #16105 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
1 parent a5aad24 commit 6f6f7f7

File tree

4 files changed

+227
-0
lines changed

4 files changed

+227
-0
lines changed

benchmark/util/priority-queue.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
const bench = common.createBenchmark(main, {
6+
n: [1e6]
7+
}, { flags: ['--expose-internals'] });
8+
9+
function main({ n, type }) {
10+
const PriorityQueue = require('internal/priority_queue');
11+
const queue = new PriorityQueue();
12+
bench.start();
13+
for (var i = 0; i < n; i++)
14+
queue.insert(Math.random() * 1e7 | 0);
15+
for (i = 0; i < n; i++)
16+
queue.shift();
17+
bench.end(n);
18+
}

lib/internal/priority_queue.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict';
2+
3+
const kCompare = Symbol('compare');
4+
const kHeap = Symbol('heap');
5+
const kSetPosition = Symbol('setPosition');
6+
const kSize = Symbol('size');
7+
8+
// The PriorityQueue is a basic implementation of a binary heap that accepts
9+
// a custom sorting function via its constructor. This function is passed
10+
// the two nodes to compare, similar to the native Array#sort. Crucially
11+
// this enables priority queues that are based on a comparison of more than
12+
// just a single criteria.
13+
14+
module.exports = class PriorityQueue {
15+
constructor(comparator, setPosition) {
16+
if (comparator !== undefined)
17+
this[kCompare] = comparator;
18+
if (setPosition !== undefined)
19+
this[kSetPosition] = setPosition;
20+
21+
this[kHeap] = new Array(64);
22+
this[kSize] = 0;
23+
}
24+
25+
[kCompare](a, b) {
26+
return a - b;
27+
}
28+
29+
insert(value) {
30+
const heap = this[kHeap];
31+
let pos = ++this[kSize];
32+
33+
if (heap.length === pos)
34+
heap.length *= 2;
35+
36+
const compare = this[kCompare];
37+
const setPosition = this[kSetPosition];
38+
while (pos > 1) {
39+
const parent = heap[pos / 2 | 0];
40+
if (compare(parent, value) <= 0)
41+
break;
42+
heap[pos] = parent;
43+
if (setPosition !== undefined)
44+
setPosition(parent, pos);
45+
pos = pos / 2 | 0;
46+
}
47+
heap[pos] = value;
48+
if (setPosition !== undefined)
49+
setPosition(value, pos);
50+
}
51+
52+
peek() {
53+
return this[kHeap][1];
54+
}
55+
56+
percolateDown(pos) {
57+
const compare = this[kCompare];
58+
const setPosition = this[kSetPosition];
59+
const heap = this[kHeap];
60+
const size = this[kSize];
61+
const item = heap[pos];
62+
63+
while (pos * 2 <= size) {
64+
let childIndex = pos * 2 + 1;
65+
if (childIndex > size || compare(heap[pos * 2], heap[childIndex]) < 0)
66+
childIndex = pos * 2;
67+
const child = heap[childIndex];
68+
if (compare(item, child) <= 0)
69+
break;
70+
if (setPosition !== undefined)
71+
setPosition(child, pos);
72+
heap[pos] = child;
73+
pos = childIndex;
74+
}
75+
heap[pos] = item;
76+
if (setPosition !== undefined)
77+
setPosition(item, pos);
78+
}
79+
80+
removeAt(pos) {
81+
const heap = this[kHeap];
82+
const size = --this[kSize];
83+
heap[pos] = heap[size + 1];
84+
heap[size + 1] = undefined;
85+
86+
if (size > 0)
87+
this.percolateDown(1);
88+
}
89+
90+
remove(value) {
91+
const heap = this[kHeap];
92+
const pos = heap.indexOf(value);
93+
if (pos < 1)
94+
return false;
95+
96+
this.removeAt(pos);
97+
98+
return true;
99+
}
100+
101+
shift() {
102+
const heap = this[kHeap];
103+
const value = heap[1];
104+
if (value === undefined)
105+
return;
106+
107+
this.removeAt(1);
108+
109+
return value;
110+
}
111+
};

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
'lib/internal/safe_globals.js',
124124
'lib/internal/net.js',
125125
'lib/internal/os.js',
126+
'lib/internal/priority_queue.js',
126127
'lib/internal/process/esm_loader.js',
127128
'lib/internal/process/methods.js',
128129
'lib/internal/process/next_tick.js',

test/parallel/test-priority-queue.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
require('../common');
5+
6+
const assert = require('assert');
7+
const PriorityQueue = require('internal/priority_queue');
8+
9+
{
10+
// Checks that the queue is fundamentally correct.
11+
const queue = new PriorityQueue();
12+
for (let i = 15; i > 0; i--)
13+
queue.insert(i);
14+
15+
for (let i = 1; i < 16; i++) {
16+
assert.strictEqual(queue.peek(), i);
17+
assert.strictEqual(queue.shift(), i);
18+
}
19+
20+
assert.strictEqual(queue.shift(), undefined);
21+
22+
// Reverse the order.
23+
for (let i = 1; i < 16; i++)
24+
queue.insert(i);
25+
26+
for (let i = 1; i < 16; i++) {
27+
assert.strictEqual(queue.shift(), i);
28+
}
29+
30+
assert.strictEqual(queue.shift(), undefined);
31+
}
32+
33+
{
34+
// Checks that the queue is capable of resizing and fitting more elements.
35+
const queue = new PriorityQueue();
36+
for (let i = 2048; i > 0; i--)
37+
queue.insert(i);
38+
39+
for (let i = 1; i < 2049; i++) {
40+
assert.strictEqual(queue.shift(), i);
41+
}
42+
43+
assert.strictEqual(queue.shift(), undefined);
44+
}
45+
46+
{
47+
// Checks that remove works as expected.
48+
const queue = new PriorityQueue();
49+
for (let i = 16; i > 0; i--)
50+
queue.insert(i);
51+
52+
const removed = [5, 10, 15];
53+
for (const id of removed)
54+
assert(queue.remove(id));
55+
56+
assert(!queue.remove(100));
57+
assert(!queue.remove(-100));
58+
59+
for (let i = 1; i < 17; i++) {
60+
if (removed.indexOf(i) < 0)
61+
assert.strictEqual(queue.shift(), i);
62+
}
63+
64+
assert.strictEqual(queue.shift(), undefined);
65+
}
66+
67+
{
68+
// Make a max heap with a custom sort function.
69+
const queue = new PriorityQueue((a, b) => b - a);
70+
for (let i = 1; i < 17; i++)
71+
queue.insert(i);
72+
73+
for (let i = 16; i > 0; i--) {
74+
assert.strictEqual(queue.shift(), i);
75+
}
76+
77+
assert.strictEqual(queue.shift(), undefined);
78+
}
79+
80+
{
81+
// Make a min heap that accepts objects as values, which necessitates
82+
// a custom sorting function. In addition, add a setPosition function
83+
// as 2nd param which provides a reference to the node in the heap
84+
// and allows speedy deletions.
85+
const queue = new PriorityQueue((a, b) => {
86+
return a.value - b.value;
87+
}, (node, pos) => (node.position = pos));
88+
for (let i = 1; i < 17; i++)
89+
queue.insert({ value: i, position: null });
90+
91+
for (let i = 1; i < 17; i++) {
92+
assert.strictEqual(queue.peek().value, i);
93+
queue.removeAt(queue.peek().position);
94+
}
95+
96+
assert.strictEqual(queue.peek(), undefined);
97+
}

0 commit comments

Comments
 (0)