Skip to content

Commit 29a6c88

Browse files
committed
Adds [Heap]PriorityQueue.of constructor.
Introduces efficient "heapify" algorithm for converting an unsorted list to a heap-sorted list, using it for the `of` constructor, and after a large `addAll` operation, when it's presumed faster than just bubbling down all the new elements. Also rewrites `HeapPriorityQueue` to use a growable list as backing array, instead of implementing the same thing using the double-when-full algorithm, and still having to deal with nullable cells. The platform growable list implementation is assumed to efficiently avoid some of those `null` checks.
1 parent 7f9f597 commit 29a6c88

File tree

3 files changed

+123
-86
lines changed

3 files changed

+123
-86
lines changed

pkgs/collection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
`Map.entries`.
55
- Optimize equality and hash code for maps by using `update` and a `values`
66
iterator to avoid extra lookups.
7+
- Add `PriorityQueue.of` constructor and optimize adding many elements.
78

89
## 1.19.1
910

pkgs/collection/lib/src/priority_queue.dart

Lines changed: 93 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ abstract class PriorityQueue<E> {
3636
factory PriorityQueue([int Function(E, E)? comparison]) =
3737
HeapPriorityQueue<E>;
3838

39+
/// Create a new priority queue.
40+
///
41+
/// The [comparison] is a [Comparator] used to compare the priority of
42+
/// elements. An element that compares as less than another element has
43+
/// a higher priority.
44+
///
45+
/// If [comparison] is omitted, it defaults to [Comparable.compare]. If this
46+
/// is the case, `E` must implement [Comparable], and this is checked at
47+
/// runtime for every comparison.
48+
factory PriorityQueue.of(
49+
Iterable<E> elements, int Function(E, E) comparison) =
50+
HeapPriorityQueue<E>.of;
51+
3952
/// Number of elements in the queue.
4053
int get length;
4154

@@ -169,27 +182,16 @@ abstract class PriorityQueue<E> {
169182
/// * The [toSet] operation effectively adds each element to the new set, taking
170183
/// an expected O(n*log(n)) time.
171184
class HeapPriorityQueue<E> implements PriorityQueue<E> {
172-
/// Initial capacity of a queue when created, or when added to after a
173-
/// [clear].
174-
///
175-
/// Number can be any positive value. Picking a size that gives a whole
176-
/// number of "tree levels" in the heap is only done for aesthetic reasons.
177-
static const int _initialCapacity = 7;
178-
179185
/// The comparison being used to compare the priority of elements.
180186
final Comparator<E> comparison;
181187

182188
/// List implementation of a heap.
183-
List<E?> _queue = List<E?>.filled(_initialCapacity, null);
184-
185-
/// Number of elements in queue.
186-
///
187-
/// The heap is implemented in the first [_length] entries of [_queue].
188-
int _length = 0;
189+
List<E> _queue;
189190

190191
/// Modification count.
191192
///
192193
/// Used to detect concurrent modifications during iteration.
194+
/// Incremented whenever an element is added or removed.
193195
int _modificationCount = 0;
194196

195197
/// Create a new priority queue.
@@ -202,31 +204,72 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
202204
/// is the case, `E` must implement [Comparable], and this is checked at
203205
/// runtime for every comparison.
204206
HeapPriorityQueue([int Function(E, E)? comparison])
205-
: comparison = comparison ?? defaultCompare;
207+
: comparison = comparison ?? defaultCompare,
208+
_queue = <E>[];
209+
210+
/// Creates a new priority queue containing [elements].
211+
///
212+
/// The [comparison] is a [Comparator] used to compare the priority of
213+
/// elements. An element that compares as less than another element has
214+
/// a higher priority.
215+
HeapPriorityQueue.of(Iterable<E> elements, int Function(E, E) this.comparison)
216+
: _queue = elements.toList() {
217+
_heapify();
218+
}
206219

207-
E _elementAt(int index) => _queue[index] ?? (null as E);
220+
/// Converts an unordered list of elements to a heap-ordered list of elements.
221+
///
222+
/// Does so by ordering sub-trees iteratively, then bubbling their parent
223+
/// down into the two ordered subtrees.
224+
/// Trivially ignores the last half of elements, which have no children.
225+
/// Does a number of bubble-down steps that is bounded by the number
226+
/// of elements. Each bubble-down step does two comparisons.
227+
void _heapify() {
228+
// Last non-leaf node's index, negative for empty or one-element queue.
229+
var cursor = _queue.length ~/ 2 - 1;
230+
while (cursor >= 0) {
231+
_bubbleDown(_queue[cursor], cursor);
232+
cursor -= 1;
233+
}
234+
}
208235

209236
@override
210237
void add(E element) {
211238
_modificationCount++;
212-
_add(element);
239+
_queue.add(element);
240+
_bubbleUp(element, _queue.length - 1);
213241
}
214242

215243
@override
216244
void addAll(Iterable<E> elements) {
217-
var modified = 0;
218-
for (var element in elements) {
219-
modified = 1;
220-
_add(element);
245+
var endIndex = _queue.length;
246+
_queue.addAll(elements);
247+
var newLength = _queue.length;
248+
var addedCount = newLength - endIndex;
249+
if (addedCount == 0) return;
250+
_modificationCount++;
251+
// Approximation for when the time to bubble up all added elements,
252+
// taking approx. addedCount * (log2(newLength)-1) comparisons worst-case,
253+
// (bubble-up does one comparison per element per level),
254+
// is slower than just heapifying the entire heap, which does
255+
// newLength * 2 comparisons worst-case.
256+
// Uses `endIndex.bitLength` instead of `newLength.bitLength` because
257+
// if `addedCount` is greater than `newLength`, the bitLength won't matter
258+
// for any non-trivial heap, and if not, every added element is a leaf
259+
// element, so it only has to look at log2(endIndex) parents.
260+
if (addedCount * endIndex.bitLength >= newLength * 2) {
261+
_heapify();
262+
return;
263+
}
264+
for (var i = endIndex; i < newLength; i++) {
265+
_bubbleUp(_queue[i], i);
221266
}
222-
_modificationCount += modified;
223267
}
224268

225269
@override
226270
void clear() {
227271
_modificationCount++;
228-
_queue = const [];
229-
_length = 0;
272+
_queue.clear();
230273
}
231274

232275
@override
@@ -243,27 +286,24 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
243286
Iterable<E> get unorderedElements => _UnorderedElementsIterable<E>(this);
244287

245288
@override
246-
E get first {
247-
if (_length == 0) throw StateError('No element');
248-
return _elementAt(0);
249-
}
289+
E get first => _queue.first;
250290

251291
@override
252-
bool get isEmpty => _length == 0;
292+
bool get isEmpty => _queue.isEmpty;
253293

254294
@override
255-
bool get isNotEmpty => _length != 0;
295+
bool get isNotEmpty => _queue.isNotEmpty;
256296

257297
@override
258-
int get length => _length;
298+
int get length => _queue.length;
259299

260300
@override
261301
bool remove(E element) {
262302
var index = _locate(element);
263303
if (index < 0) return false;
264304
_modificationCount++;
265-
var last = _removeLast();
266-
if (index < _length) {
305+
var last = _queue.removeLast();
306+
if (index < _queue.length) {
267307
var comp = comparison(last, element);
268308
if (comp <= 0) {
269309
_bubbleUp(last, index);
@@ -284,19 +324,17 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
284324
Iterable<E> removeAll() {
285325
_modificationCount++;
286326
var result = _queue;
287-
var length = _length;
288-
_queue = const [];
289-
_length = 0;
290-
return result.take(length).cast();
327+
_queue = <E>[];
328+
return result.skip(0); // Hide list nature.
291329
}
292330

293331
@override
294332
E removeFirst() {
295-
if (_length == 0) throw StateError('No element');
333+
if (_queue.isEmpty) throw StateError('No element');
296334
_modificationCount++;
297-
var result = _elementAt(0);
298-
var last = _removeLast();
299-
if (_length > 0) {
335+
var result = _queue.first;
336+
var last = _queue.removeLast();
337+
if (_queue.length > 0) {
300338
_bubbleDown(last, 0);
301339
}
302340
return result;
@@ -306,34 +344,19 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
306344
List<E> toList() => _toUnorderedList()..sort(comparison);
307345

308346
@override
309-
Set<E> toSet() {
310-
var set = SplayTreeSet<E>(comparison);
311-
for (var i = 0; i < _length; i++) {
312-
set.add(_elementAt(i));
313-
}
314-
return set;
315-
}
347+
Set<E> toSet() => SplayTreeSet<E>(comparison)..addAll(_queue);
316348

317349
@override
318350
List<E> toUnorderedList() => _toUnorderedList();
319351

320-
List<E> _toUnorderedList() =>
321-
[for (var i = 0; i < _length; i++) _elementAt(i)];
352+
List<E> _toUnorderedList() => _queue.toList();
322353

323354
/// Returns some representation of the queue.
324355
///
325356
/// The format isn't significant, and may change in the future.
326357
@override
327358
String toString() {
328-
return _queue.take(_length).toString();
329-
}
330-
331-
/// Add element to the queue.
332-
///
333-
/// Grows the capacity if the backing list is full.
334-
void _add(E element) {
335-
if (_length == _queue.length) _grow();
336-
_bubbleUp(element, _length++);
359+
return _queue.skip(0).toString();
337360
}
338361

339362
/// Find the index of an object in the heap.
@@ -343,7 +366,7 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
343366
/// A matching object, `o`, must satisfy that
344367
/// `comparison(o, object) == 0 && o == object`.
345368
int _locate(E object) {
346-
if (_length == 0) return -1;
369+
if (_queue.isEmpty) return -1;
347370
// Count positions from one instead of zero. This gives the numbers
348371
// some nice properties. For example, all right children are odd,
349372
// their left sibling is even, and the parent is found by shifting
@@ -355,14 +378,14 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
355378
// in the heap will also have lower priority.
356379
do {
357380
var index = position - 1;
358-
var element = _elementAt(index);
381+
var element = _queue[index];
359382
var comp = comparison(element, object);
360383
if (comp <= 0) {
361384
if (comp == 0 && element == object) return index;
362385
// Element may be in subtree.
363386
// Continue with the left child, if it is there.
364387
var leftChildPosition = position * 2;
365-
if (leftChildPosition <= _length) {
388+
if (leftChildPosition <= _queue.length) {
366389
position = leftChildPosition;
367390
continue;
368391
}
@@ -375,18 +398,13 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
375398
}
376399
// Then go to the right sibling of the left-child.
377400
position += 1;
378-
} while (position > _length); // Happens if last element is a left child.
401+
} while (
402+
position > _queue.length); // Happens if last element is a left child.
379403
} while (position != 1); // At root again. Happens for right-most element.
380404
return -1;
381405
}
382406

383-
E _removeLast() {
384-
var newLength = _length - 1;
385-
var last = _elementAt(newLength);
386-
_queue[newLength] = null;
387-
_length = newLength;
388-
return last;
389-
}
407+
E _removeLast() => _queue.removeLast();
390408

391409
/// Place [element] in heap at [index] or above.
392410
///
@@ -396,7 +414,7 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
396414
void _bubbleUp(E element, int index) {
397415
while (index > 0) {
398416
var parentIndex = (index - 1) ~/ 2;
399-
var parent = _elementAt(parentIndex);
417+
var parent = _queue[parentIndex];
400418
if (comparison(element, parent) > 0) break;
401419
_queue[index] = parent;
402420
index = parentIndex;
@@ -411,10 +429,10 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
411429
/// swap it with the highest priority child.
412430
void _bubbleDown(E element, int index) {
413431
var rightChildIndex = index * 2 + 2;
414-
while (rightChildIndex < _length) {
432+
while (rightChildIndex < _queue.length) {
415433
var leftChildIndex = rightChildIndex - 1;
416-
var leftChild = _elementAt(leftChildIndex);
417-
var rightChild = _elementAt(rightChildIndex);
434+
var leftChild = _queue[leftChildIndex];
435+
var rightChild = _queue[rightChildIndex];
418436
var comp = comparison(leftChild, rightChild);
419437
int minChildIndex;
420438
E minChild;
@@ -435,8 +453,8 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
435453
rightChildIndex = index * 2 + 2;
436454
}
437455
var leftChildIndex = rightChildIndex - 1;
438-
if (leftChildIndex < _length) {
439-
var child = _elementAt(leftChildIndex);
456+
if (leftChildIndex < _queue.length) {
457+
var child = _queue[leftChildIndex];
440458
var comp = comparison(element, child);
441459
if (comp > 0) {
442460
_queue[index] = child;
@@ -445,17 +463,6 @@ class HeapPriorityQueue<E> implements PriorityQueue<E> {
445463
}
446464
_queue[index] = element;
447465
}
448-
449-
/// Grows the capacity of the list holding the heap.
450-
///
451-
/// Called when the list is full.
452-
void _grow() {
453-
var newCapacity = _queue.length * 2 + 1;
454-
if (newCapacity < _initialCapacity) newCapacity = _initialCapacity;
455-
var newQueue = List<E?>.filled(newCapacity, null);
456-
newQueue.setRange(0, _length, _queue);
457-
_queue = newQueue;
458-
}
459466
}
460467

461468
/// Implementation of [HeapPriorityQueue.unorderedElements].

pkgs/collection/test/priority_queue_test.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,35 @@ void testDefault() {
2323
});
2424
testInt(PriorityQueue<int>.new);
2525
testCustom(PriorityQueue<C>.new);
26+
27+
group('(Heap)PriorityQueue.of returns functional priority queue', () {
28+
List<int> extract(PriorityQueue<int> queue) {
29+
var result = <int>[];
30+
while (queue.isNotEmpty) {
31+
result.add(queue.removeFirst());
32+
}
33+
return result;
34+
}
35+
36+
for (var i = 0; i < 1024; i = i * 2 + 1) {
37+
test('size $i', () {
38+
var input = List<int>.generate(i, (x) => x);
39+
for (var j = 0; j < 5; j++) {
40+
var copy = (input.toList()..shuffle()).where((_) => true);
41+
{
42+
var queue = HeapPriorityQueue<int>.of(copy, (a, b) => a - b);
43+
var elements = extract(queue);
44+
expect(elements, input);
45+
}
46+
{
47+
var queue = HeapPriorityQueue<int>.of(copy, (a, b) => a - b);
48+
var elements = extract(queue);
49+
expect(elements, input);
50+
}
51+
}
52+
});
53+
}
54+
});
2655
}
2756

2857
void testInt(PriorityQueue<int> Function() create) {

0 commit comments

Comments
 (0)