diff --git a/pkgs/collection/CHANGELOG.md b/pkgs/collection/CHANGELOG.md index 0123f87f..5164fca3 100644 --- a/pkgs/collection/CHANGELOG.md +++ b/pkgs/collection/CHANGELOG.md @@ -5,6 +5,10 @@ `Map.entries`. - Explicitly mark `BoolList` as `abstract interface` - Address diagnostics from `strict_top_level_inference`. +- 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. +- Address diagnostics from `strict_top_level_inference`. ## 1.19.1 diff --git a/pkgs/collection/lib/src/priority_queue.dart b/pkgs/collection/lib/src/priority_queue.dart index 11b0348a..df5b43b4 100644 --- a/pkgs/collection/lib/src/priority_queue.dart +++ b/pkgs/collection/lib/src/priority_queue.dart @@ -22,9 +22,9 @@ import 'utils.dart'; /// always give equal objects the same priority, /// otherwise [contains] or [remove] might not work correctly. abstract class PriorityQueue { - /// Creates an empty [PriorityQueue]. + /// Creates an empty priority queue. /// - /// The created [PriorityQueue] is a plain [HeapPriorityQueue]. + /// The created `PriorityQueue` is a plain [HeapPriorityQueue]. /// /// The [comparison] is a [Comparator] used to compare the priority of /// elements. An element that compares as less than another element has @@ -36,6 +36,20 @@ abstract class PriorityQueue { factory PriorityQueue([int Function(E, E)? comparison]) = HeapPriorityQueue; + /// Creates a new [HeapPriorityQueue] 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. + /// + /// Unlike [PriorityQueue.new], the [comparison] cannot be omitted. + /// If the elements are comparable to each other, use [Comparable.compare] + /// as the comparison function, or use a more specialized function + /// if one is available. + factory PriorityQueue.of( + Iterable elements, int Function(E, E) comparison) = + HeapPriorityQueue.of; + /// Number of elements in the queue. int get length; @@ -151,45 +165,41 @@ abstract class PriorityQueue { /// /// The elements are kept in a heap structure, /// where the element with the highest priority is immediately accessible, -/// and modifying a single element takes -/// logarithmic time in the number of elements on average. +/// and modifying a single element takes, on average, +/// logarithmic time in the number of elements. /// /// * The [add] and [removeFirst] operations take amortized logarithmic time, -/// O(log(n)), but may occasionally take linear time when growing the capacity -/// of the heap. -/// * The [addAll] operation works as doing repeated [add] operations. +/// O(log(*N*)) where *N* is the number of elements, but may occasionally +/// take linear time when growing the capacity of the heap. +/// * The [addAll] operation works by doing repeated [add] operations. +/// May be more efficient in some cases. /// * The [first] getter takes constant time, O(1). /// * The [clear] and [removeAll] methods also take constant time, O(1). /// * The [contains] and [remove] operations may need to search the entire -/// queue for the elements, taking O(n) time. -/// * The [toList] operation effectively sorts the elements, taking O(n*log(n)) -/// time. +/// queue for the elements, taking O(*N*) time. +/// * The [toList] operation effectively sorts the elements, +/// taking O(n * log(*N*)) time. /// * The [toUnorderedList] operation copies, but does not sort, the elements, /// and is linear, O(n). -/// * The [toSet] operation effectively adds each element to the new set, taking -/// an expected O(n*log(n)) time. +/// * The [toSet] operation effectively adds each element to the new +/// [SplayTreeSet], taking an expected O(n * log(*N*)) time. +/// +/// The [comparison] function is used to order elements, with earlier elements +/// having higher priority. That is, elements are extracted from the queue +/// in ascending [comparison] order. +/// If two elements have the same priority, their ordering is unspecified +/// and may be arbitrary. 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; + final int Function(E, E) 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 +212,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 = []; - E _elementAt(int index) => _queue[index] ?? (null as E); + /// 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, this.comparison) + : _queue = elements.toList() { + _heapify(); + } + + /// 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 +294,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); @@ -276,27 +324,25 @@ class HeapPriorityQueue implements PriorityQueue { /// Removes all the elements from this queue and returns them. /// - /// The returned iterable has no specified order. - /// The operation does not copy the elements, - /// but instead keeps them in the existing heap structure, - /// and iterates over that directly. + /// The [HeapPriorityQueue] returns a [List] of its elements, + /// with no guaranteed order. + /// + /// If the elements are not needed, use [clear] instead. @override - Iterable removeAll() { + List removeAll() { _modificationCount++; var result = _queue; - var length = _length; - _queue = const []; - _length = 0; - return result.take(length).cast(); + _queue = []; + return result; } @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.isNotEmpty) { _bubbleDown(last, 0); } return result; @@ -306,34 +352,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 +374,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 +386,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,19 +406,12 @@ 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; - } - /// Place [element] in heap at [index] or above. /// /// Put element into the empty cell at `index`. @@ -396,7 +420,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 +435,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 +459,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 +469,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..a7aafc4f 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) { @@ -278,9 +307,9 @@ void testConcurrentModification() { var q = HeapPriorityQueue((a, b) => a - b) ..addAll([6, 4, 2, 3, 5, 8]); var e = q.unorderedElements; - q.add(12); // Modifiation before creating iterator is not a problem. + q.add(12); // Modification before creating iterator is not a problem. var it = e.iterator; - q.add(7); // Modification after creatig iterator is a problem. + q.add(7); // Modification after creating iterator is a problem. expect(it.moveNext, throwsConcurrentModificationError); it = e.iterator; // New iterator is not affected. @@ -294,9 +323,9 @@ void testConcurrentModification() { var q = HeapPriorityQueue((a, b) => a - b) ..addAll([6, 4, 2, 3, 5, 8]); var e = q.unorderedElements; - q.addAll([12]); // Modifiation before creating iterator is not a problem. + q.addAll([12]); // Modification before creating iterator is not a problem. var it = e.iterator; - q.addAll([7]); // Modification after creatig iterator is a problem. + q.addAll([7]); // Modification after creating iterator is a problem. expect(it.moveNext, throwsConcurrentModificationError); it = e.iterator; // New iterator is not affected. expect(it.moveNext(), true); @@ -311,10 +340,10 @@ void testConcurrentModification() { ..addAll([6, 4, 2, 3, 5, 8]); var e = q.unorderedElements; expect(q.removeFirst(), - 2); // Modifiation before creating iterator is not a problem. + 2); // Modification before creating iterator is not a problem. var it = e.iterator; expect(q.removeFirst(), - 3); // Modification after creatig iterator is a problem. + 3); // Modification after creating iterator is a problem. expect(it.moveNext, throwsConcurrentModificationError); it = e.iterator; // New iterator is not affected.