Add `toUnorderedList` to `PriorityQueue`. (#152)
Add `toUnorderedList` to `PriorityQueue`.
Also change `HeapPriorityQueue` to use `==` for
`contains` and `remove` checks.
Fix a bad null-safety migration for HeapPriorityQueue,
and add tests to ensure it works with `null` values.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e3a846..0c76f58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 1.15.0-nullsafety.3
+
+* Make `HeapPriorityQueue`'s `remove` and `contains` methods
+ use `==` for equality checks.
+ Previously used `comparison(a, b) == 0` as criteria, but it's possible
+ to have multiple elements with the same priority in a queue, so that
+ could remove the wrong element.
+ Still requires that objects that are `==` also have the same priority.
+
## 1.15.0-nullsafety.2
Update for the 2.10 dev sdk.
diff --git a/lib/src/priority_queue.dart b/lib/src/priority_queue.dart
index d4355b0..6abce79 100644
--- a/lib/src/priority_queue.dart
+++ b/lib/src/priority_queue.dart
@@ -9,6 +9,18 @@
/// A priority queue is a priority based work-list of elements.
///
/// The queue allows adding elements, and removing them again in priority order.
+/// The same object can be added to the queue more than once.
+/// There is no specified ordering for objects with the same priority
+/// (where the `comparison` function returns zero).
+///
+/// Operations which care about object equality, [contains] and [remove],
+/// use [Object.==] for testing equality.
+/// In most situations this will be the same as identity ([identical]),
+/// but there are types, like [String], where users can reasonably expect
+/// distinct objects to represent the same value.
+/// If elements override [Object.==], the `comparison` function must
+/// always give equal objects the same priority,
+/// otherwise [contains] or [remove] might not work correctly.
abstract class PriorityQueue<E> {
/// Creates an empty [PriorityQueue].
///
@@ -36,6 +48,14 @@
/// Checks if [object] is in the queue.
///
/// Returns true if the element is found.
+ ///
+ /// Uses the [Object.==] of elements in the queue to check
+ /// for whether they are equal to [object].
+ /// Equal objects objects must have the same priority
+ /// according to the [comparison] function.
+ /// That is, if `a == b` then `comparison(a, b) == 0`.
+ /// If that is not the case, this check might fail to find
+ /// an object.
bool contains(E object);
/// Adds element to the queue.
@@ -63,10 +83,21 @@
/// The queue must not be empty when this method is called.
E removeFirst();
- /// Removes an element that compares equal to [element] in the queue.
+ /// Removes an element of the queue that compares equal to [element].
///
/// Returns true if an element is found and removed,
/// and false if no equal element is found.
+ ///
+ /// If the queue contains more than one object equal to [element],
+ /// only one of them is removed.
+ ///
+ /// Uses the [Object.==] of elements in the queue to check
+ /// for whether they are equal to [element].
+ /// Equal objects objects must have the same priority
+ /// according to the [comparison] function.
+ /// That is, if `a == b` then `comparison(a, b) == 0`.
+ /// If that is not the case, this check might fail to find
+ /// an object.
bool remove(E element);
/// Removes all the elements from this queue and returns them.
@@ -85,6 +116,14 @@
/// removed from this queue using [removeFirst].
List<E> toList();
+ /// Returns a list of the elements of this queue in no specific order.
+ ///
+ /// The queue is not modified.
+ ///
+ /// The order of the elements is implementation specific.
+ /// The order may differ between different calls on the same queue.
+ List<E> toUnorderedList();
+
/// Return a comparator based set using the comparator of this queue.
///
/// The queue is not modified.
@@ -115,6 +154,8 @@
/// 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.
class HeapPriorityQueue<E> implements PriorityQueue<E> {
@@ -148,6 +189,8 @@
HeapPriorityQueue([int Function(E, E)? comparison])
: comparison = comparison ?? defaultCompare<E>();
+ E _elementAt(int index) => _queue[index] ?? (null as E);
+
@override
void add(E element) {
_add(element);
@@ -167,14 +210,12 @@
}
@override
- bool contains(E object) {
- return _locate(object) >= 0;
- }
+ bool contains(E object) => _locate(object) >= 0;
@override
E get first {
- if (_length == 0) throw StateError('No such element');
- return _queue[0]!;
+ if (_length == 0) throw StateError('No element');
+ return _elementAt(0);
}
@override
@@ -213,8 +254,8 @@
@override
E removeFirst() {
- if (_length == 0) throw StateError('No such element');
- var result = _queue[0]!;
+ if (_length == 0) throw StateError('No element');
+ var result = _elementAt(0);
var last = _removeLast();
if (_length > 0) {
_bubbleDown(last, 0);
@@ -223,18 +264,23 @@
}
@override
- List<E> toList() =>
- List<E>.generate(_length, (i) => _queue[i]!)..sort(comparison);
+ List<E> toList() => _toUnorderedList()..sort(comparison);
@override
Set<E> toSet() {
var set = SplayTreeSet<E>(comparison);
for (var i = 0; i < _length; i++) {
- set.add(_queue[i]!);
+ set.add(_elementAt(i));
}
return set;
}
+ @override
+ List<E> toUnorderedList() => _toUnorderedList();
+
+ List<E> _toUnorderedList() =>
+ [for (var i = 0; i < _length; i++) _elementAt(i)];
+
/// Returns some representation of the queue.
///
/// The format isn't significant, and may change in the future.
@@ -254,6 +300,9 @@
/// Find the index of an object in the heap.
///
/// Returns -1 if the object is not found.
+ ///
+ /// A matching object, `o`, must satisfy that
+ /// `comparison(o, object) == 0 && o == object`.
int _locate(E object) {
if (_length == 0) return -1;
// Count positions from one instead of zero. This gives the numbers
@@ -267,10 +316,10 @@
// in the heap will also have lower priority.
do {
var index = position - 1;
- var element = _queue[index]!;
+ var element = _elementAt(index);
var comp = comparison(element, object);
- if (comp == 0) return index;
- if (comp < 0) {
+ 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;
@@ -294,7 +343,7 @@
E _removeLast() {
var newLength = _length - 1;
- var last = _queue[newLength]!;
+ var last = _elementAt(newLength);
_queue[newLength] = null;
_length = newLength;
return last;
@@ -308,7 +357,7 @@
void _bubbleUp(E element, int index) {
while (index > 0) {
var parentIndex = (index - 1) ~/ 2;
- var parent = _queue[parentIndex]!;
+ var parent = _elementAt(parentIndex);
if (comparison(element, parent) > 0) break;
_queue[index] = parent;
index = parentIndex;
@@ -325,8 +374,8 @@
var rightChildIndex = index * 2 + 2;
while (rightChildIndex < _length) {
var leftChildIndex = rightChildIndex - 1;
- var leftChild = _queue[leftChildIndex]!;
- var rightChild = _queue[rightChildIndex]!;
+ var leftChild = _elementAt(leftChildIndex);
+ var rightChild = _elementAt(rightChildIndex);
var comp = comparison(leftChild, rightChild);
int minChildIndex;
E minChild;
@@ -348,7 +397,7 @@
}
var leftChildIndex = rightChildIndex - 1;
if (leftChildIndex < _length) {
- var child = _queue[leftChildIndex]!;
+ var child = _elementAt(leftChildIndex);
var comp = comparison(element, child);
if (comp > 0) {
_queue[index] = child;
diff --git a/lib/src/union_set.dart b/lib/src/union_set.dart
index 208a2a5..2fe6e3e 100644
--- a/lib/src/union_set.dart
+++ b/lib/src/union_set.dart
@@ -71,6 +71,7 @@
var result = set.lookup(element);
if (result != null) return result;
}
+ return null;
}
@override
diff --git a/pubspec.yaml b/pubspec.yaml
index e45737f..cd57aca 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,11 +1,11 @@
name: collection
-version: 1.15.0-nullsafety.2
+version: 1.15.0-nullsafety.3
description: Collections and utilities functions and classes related to collections.
homepage: https://www.github.com/dart-lang/collection
environment:
- sdk: '>=2.10.0-0 <2.10.0'
+ sdk: '>=2.10.0-78 <2.10.0'
dev_dependencies:
pedantic: ^1.0.0
diff --git a/test/priority_queue_test.dart b/test/priority_queue_test.dart
index b38b3aa..191857d 100644
--- a/test/priority_queue_test.dart
+++ b/test/priority_queue_test.dart
@@ -12,6 +12,8 @@
testDefault();
testInt(() => HeapPriorityQueue<int>());
testCustom((comparator) => HeapPriorityQueue<C>(comparator));
+ testDuplicates();
+ testNullable();
}
void testDefault() {
@@ -157,6 +159,108 @@
expect(q.isEmpty, isTrue);
}
+void testDuplicates() {
+ // Check how the heap handles duplicate, or equal-but-not-identical, values.
+ test('duplicates', () {
+ var q = HeapPriorityQueue<C>(compare);
+ var c1 = C(0);
+ var c2 = C(0);
+
+ // Can contain the same element more than once.
+ expect(c1, equals(c2));
+ expect(c1, isNot(same(c2)));
+ q.add(c1);
+ q.add(c1);
+ expect(q.length, 2);
+ expect(q.contains(c1), true);
+ expect(q.contains(c2), true);
+ expect(q.remove(c2), true);
+ expect(q.length, 1);
+ expect(q.removeFirst(), same(c1));
+
+ // Can contain equal elements.
+ q.add(c1);
+ q.add(c2);
+ expect(q.length, 2);
+ expect(q.contains(c1), true);
+ expect(q.contains(c2), true);
+ expect(q.remove(c1), true);
+ expect(q.length, 1);
+ expect(q.first, anyOf(same(c1), same(c2)));
+ });
+}
+
+void testNullable() {
+ // Check that the queue works with a nullable type, and a comparator
+ // which accepts `null`.
+ // Compares `null` before instances of `C`.
+ int nullCompareFirst(C? a, C? b) => a == null
+ ? b == null
+ ? 0
+ : -1
+ : b == null
+ ? 1
+ : compare(a, b);
+
+ int nullCompareLast(C? a, C? b) => a == null
+ ? b == null
+ ? 0
+ : 1
+ : b == null
+ ? -1
+ : compare(a, b);
+
+ var c1 = C(1);
+ var c2 = C(2);
+ var c3 = C(3);
+
+ test('nulls first', () {
+ var q = HeapPriorityQueue<C?>(nullCompareFirst);
+ q.add(c2);
+ q.add(c1);
+ q.add(null);
+ expect(q.length, 3);
+ expect(q.contains(null), true);
+ expect(q.contains(c1), true);
+ expect(q.contains(c3), false);
+
+ expect(q.removeFirst(), null);
+ expect(q.length, 2);
+ expect(q.contains(null), false);
+ q.add(null);
+ expect(q.length, 3);
+ expect(q.contains(null), true);
+ q.add(null);
+ expect(q.length, 4);
+ expect(q.contains(null), true);
+ expect(q.remove(null), true);
+ expect(q.length, 3);
+ expect(q.toList(), [null, c1, c2]);
+ });
+
+ test('nulls last', () {
+ var q = HeapPriorityQueue<C?>(nullCompareLast);
+ q.add(c2);
+ q.add(c1);
+ q.add(null);
+ expect(q.length, 3);
+ expect(q.contains(null), true);
+ expect(q.contains(c1), true);
+ expect(q.contains(c3), false);
+ expect(q.first, c1);
+
+ q.add(null);
+ expect(q.length, 4);
+ expect(q.contains(null), true);
+ q.add(null);
+ expect(q.length, 5);
+ expect(q.contains(null), true);
+ expect(q.remove(null), true);
+ expect(q.length, 4);
+ expect(q.toList(), [c1, c2, null, null]);
+ });
+}
+
// Custom class.
// Class is comparable, comparators match normal and inverse order.
int compare(C c1, C c2) => c1.value - c2.value;