diff --git a/pkgs/collection/CHANGELOG.md b/pkgs/collection/CHANGELOG.md index 2891b0dc..74f968fd 100644 --- a/pkgs/collection/CHANGELOG.md +++ b/pkgs/collection/CHANGELOG.md @@ -4,6 +4,7 @@ `Map.entries`. - Optimize equality and hash code for maps by using `update` and a `values` iterator to avoid extra lookups. +- Add `PriorityQueue.of` constructor and optimize adding many elements. ## 1.19.1 diff --git a/pkgs/collection/lib/src/priority_queue.dart b/pkgs/collection/lib/src/priority_queue.dart index 11b0348a..2055cdfd 100644 --- a/pkgs/collection/lib/src/priority_queue.dart +++ b/pkgs/collection/lib/src/priority_queue.dart @@ -36,6 +36,19 @@ abstract class PriorityQueue { factory PriorityQueue([int Function(E, E)? comparison]) = HeapPriorityQueue; + /// Create a new priority queue. + /// + /// The [comparison] is a [Comparator] used to compare the priority of + /// elements. An element that compares as less than another element has + /// a higher priority. + /// + /// If [comparison] is omitted, it defaults to [Comparable.compare]. If this + /// is the case, `E` must implement [Comparable], and this is checked at + /// runtime for every comparison. + factory PriorityQueue.of( + Iterable elements, int Function(E, E) comparison) = + HeapPriorityQueue.of; + /// Number of elements in the queue. int get length; @@ -169,27 +182,16 @@ abstract class PriorityQueue { /// * The [toSet] operation effectively adds each element to the new set, taking /// an expected O(n*log(n)) time. class HeapPriorityQueue implements PriorityQueue { - /// Initial capacity of a queue when created, or when added to after a - /// [clear]. - /// - /// Number can be any positive value. Picking a size that gives a whole - /// number of "tree levels" in the heap is only done for aesthetic reasons. - static const int _initialCapacity = 7; - /// The comparison being used to compare the priority of elements. final Comparator comparison; /// List implementation of a heap. - List _queue = List.filled(_initialCapacity, null); - - /// Number of elements in queue. - /// - /// The heap is implemented in the first [_length] entries of [_queue]. - int _length = 0; + List _queue; /// Modification count. /// /// Used to detect concurrent modifications during iteration. + /// Incremented whenever an element is added or removed. int _modificationCount = 0; /// Create a new priority queue. @@ -202,31 +204,72 @@ class HeapPriorityQueue implements PriorityQueue { /// is the case, `E` must implement [Comparable], and this is checked at /// runtime for every comparison. HeapPriorityQueue([int Function(E, E)? comparison]) - : comparison = comparison ?? defaultCompare; + : comparison = comparison ?? defaultCompare, + _queue = []; + + /// Creates a new priority queue containing [elements]. + /// + /// The [comparison] is a [Comparator] used to compare the priority of + /// elements. An element that compares as less than another element has + /// a higher priority. + HeapPriorityQueue.of(Iterable elements, int Function(E, E) this.comparison) + : _queue = elements.toList() { + _heapify(); + } - E _elementAt(int index) => _queue[index] ?? (null as E); + /// Converts an unordered list of elements to a heap-ordered list of elements. + /// + /// Does so by ordering sub-trees iteratively, then bubbling their parent + /// down into the two ordered subtrees. + /// Trivially ignores the last half of elements, which have no children. + /// Does a number of bubble-down steps that is bounded by the number + /// of elements. Each bubble-down step does two comparisons. + void _heapify() { + // Last non-leaf node's index, negative for empty or one-element queue. + var cursor = _queue.length ~/ 2 - 1; + while (cursor >= 0) { + _bubbleDown(_queue[cursor], cursor); + cursor -= 1; + } + } @override void add(E element) { _modificationCount++; - _add(element); + _queue.add(element); + _bubbleUp(element, _queue.length - 1); } @override void addAll(Iterable elements) { - var modified = 0; - for (var element in elements) { - modified = 1; - _add(element); + var endIndex = _queue.length; + _queue.addAll(elements); + var newLength = _queue.length; + var addedCount = newLength - endIndex; + if (addedCount == 0) return; + _modificationCount++; + // Approximation for when the time to bubble up all added elements, + // taking approx. addedCount * (log2(newLength)-1) comparisons worst-case, + // (bubble-up does one comparison per element per level), + // is slower than just heapifying the entire heap, which does + // newLength * 2 comparisons worst-case. + // Uses `endIndex.bitLength` instead of `newLength.bitLength` because + // if `addedCount` is greater than `newLength`, the bitLength won't matter + // for any non-trivial heap, and if not, every added element is a leaf + // element, so it only has to look at log2(endIndex) parents. + if (addedCount * endIndex.bitLength >= newLength * 2) { + _heapify(); + return; + } + for (var i = endIndex; i < newLength; i++) { + _bubbleUp(_queue[i], i); } - _modificationCount += modified; } @override void clear() { _modificationCount++; - _queue = const []; - _length = 0; + _queue.clear(); } @override @@ -243,27 +286,24 @@ class HeapPriorityQueue implements PriorityQueue { Iterable get unorderedElements => _UnorderedElementsIterable(this); @override - E get first { - if (_length == 0) throw StateError('No element'); - return _elementAt(0); - } + E get first => _queue.first; @override - bool get isEmpty => _length == 0; + bool get isEmpty => _queue.isEmpty; @override - bool get isNotEmpty => _length != 0; + bool get isNotEmpty => _queue.isNotEmpty; @override - int get length => _length; + int get length => _queue.length; @override bool remove(E element) { var index = _locate(element); if (index < 0) return false; _modificationCount++; - var last = _removeLast(); - if (index < _length) { + var last = _queue.removeLast(); + if (index < _queue.length) { var comp = comparison(last, element); if (comp <= 0) { _bubbleUp(last, index); @@ -284,19 +324,17 @@ class HeapPriorityQueue implements PriorityQueue { Iterable removeAll() { _modificationCount++; var result = _queue; - var length = _length; - _queue = const []; - _length = 0; - return result.take(length).cast(); + _queue = []; + return result.skip(0); // Hide list nature. } @override E removeFirst() { - if (_length == 0) throw StateError('No element'); + if (_queue.isEmpty) throw StateError('No element'); _modificationCount++; - var result = _elementAt(0); - var last = _removeLast(); - if (_length > 0) { + var result = _queue.first; + var last = _queue.removeLast(); + if (_queue.length > 0) { _bubbleDown(last, 0); } return result; @@ -306,34 +344,19 @@ class HeapPriorityQueue implements PriorityQueue { List toList() => _toUnorderedList()..sort(comparison); @override - Set toSet() { - var set = SplayTreeSet(comparison); - for (var i = 0; i < _length; i++) { - set.add(_elementAt(i)); - } - return set; - } + Set toSet() => SplayTreeSet(comparison)..addAll(_queue); @override List toUnorderedList() => _toUnorderedList(); - List _toUnorderedList() => - [for (var i = 0; i < _length; i++) _elementAt(i)]; + List _toUnorderedList() => _queue.toList(); /// Returns some representation of the queue. /// /// The format isn't significant, and may change in the future. @override String toString() { - return _queue.take(_length).toString(); - } - - /// Add element to the queue. - /// - /// Grows the capacity if the backing list is full. - void _add(E element) { - if (_length == _queue.length) _grow(); - _bubbleUp(element, _length++); + return _queue.skip(0).toString(); } /// Find the index of an object in the heap. @@ -343,7 +366,7 @@ class HeapPriorityQueue implements PriorityQueue { /// A matching object, `o`, must satisfy that /// `comparison(o, object) == 0 && o == object`. int _locate(E object) { - if (_length == 0) return -1; + if (_queue.isEmpty) return -1; // Count positions from one instead of zero. This gives the numbers // some nice properties. For example, all right children are odd, // their left sibling is even, and the parent is found by shifting @@ -355,14 +378,14 @@ class HeapPriorityQueue implements PriorityQueue { // in the heap will also have lower priority. do { var index = position - 1; - var element = _elementAt(index); + var element = _queue[index]; var comp = comparison(element, object); if (comp <= 0) { if (comp == 0 && element == object) return index; // Element may be in subtree. // Continue with the left child, if it is there. var leftChildPosition = position * 2; - if (leftChildPosition <= _length) { + if (leftChildPosition <= _queue.length) { position = leftChildPosition; continue; } @@ -375,18 +398,13 @@ class HeapPriorityQueue implements PriorityQueue { } // Then go to the right sibling of the left-child. position += 1; - } while (position > _length); // Happens if last element is a left child. + } while ( + position > _queue.length); // Happens if last element is a left child. } while (position != 1); // At root again. Happens for right-most element. return -1; } - E _removeLast() { - var newLength = _length - 1; - var last = _elementAt(newLength); - _queue[newLength] = null; - _length = newLength; - return last; - } + E _removeLast() => _queue.removeLast(); /// Place [element] in heap at [index] or above. /// @@ -396,7 +414,7 @@ class HeapPriorityQueue implements PriorityQueue { void _bubbleUp(E element, int index) { while (index > 0) { var parentIndex = (index - 1) ~/ 2; - var parent = _elementAt(parentIndex); + var parent = _queue[parentIndex]; if (comparison(element, parent) > 0) break; _queue[index] = parent; index = parentIndex; @@ -411,10 +429,10 @@ class HeapPriorityQueue implements PriorityQueue { /// swap it with the highest priority child. void _bubbleDown(E element, int index) { var rightChildIndex = index * 2 + 2; - while (rightChildIndex < _length) { + while (rightChildIndex < _queue.length) { var leftChildIndex = rightChildIndex - 1; - var leftChild = _elementAt(leftChildIndex); - var rightChild = _elementAt(rightChildIndex); + var leftChild = _queue[leftChildIndex]; + var rightChild = _queue[rightChildIndex]; var comp = comparison(leftChild, rightChild); int minChildIndex; E minChild; @@ -435,8 +453,8 @@ class HeapPriorityQueue implements PriorityQueue { rightChildIndex = index * 2 + 2; } var leftChildIndex = rightChildIndex - 1; - if (leftChildIndex < _length) { - var child = _elementAt(leftChildIndex); + if (leftChildIndex < _queue.length) { + var child = _queue[leftChildIndex]; var comp = comparison(element, child); if (comp > 0) { _queue[index] = child; @@ -445,17 +463,6 @@ class HeapPriorityQueue implements PriorityQueue { } _queue[index] = element; } - - /// Grows the capacity of the list holding the heap. - /// - /// Called when the list is full. - void _grow() { - var newCapacity = _queue.length * 2 + 1; - if (newCapacity < _initialCapacity) newCapacity = _initialCapacity; - var newQueue = List.filled(newCapacity, null); - newQueue.setRange(0, _length, _queue); - _queue = newQueue; - } } /// Implementation of [HeapPriorityQueue.unorderedElements]. diff --git a/pkgs/collection/test/priority_queue_test.dart b/pkgs/collection/test/priority_queue_test.dart index f07a1a39..abf7b692 100644 --- a/pkgs/collection/test/priority_queue_test.dart +++ b/pkgs/collection/test/priority_queue_test.dart @@ -23,6 +23,35 @@ void testDefault() { }); testInt(PriorityQueue.new); testCustom(PriorityQueue.new); + + group('(Heap)PriorityQueue.of returns functional priority queue', () { + List extract(PriorityQueue queue) { + var result = []; + while (queue.isNotEmpty) { + result.add(queue.removeFirst()); + } + return result; + } + + for (var i = 0; i < 1024; i = i * 2 + 1) { + test('size $i', () { + var input = List.generate(i, (x) => x); + for (var j = 0; j < 5; j++) { + var copy = (input.toList()..shuffle()).where((_) => true); + { + var queue = HeapPriorityQueue.of(copy, (a, b) => a - b); + var elements = extract(queue); + expect(elements, input); + } + { + var queue = HeapPriorityQueue.of(copy, (a, b) => a - b); + var elements = extract(queue); + expect(elements, input); + } + } + }); + } + }); } void testInt(PriorityQueue Function() create) {