diff --git a/docs/specs/priority-queue.md b/docs/specs/priority-queue.md index 078b4ea5f0f..46f29940fa4 100644 --- a/docs/specs/priority-queue.md +++ b/docs/specs/priority-queue.md @@ -1,4 +1,4 @@ -# Priority queue proposal +# Priority queue proposal ## User data @@ -6,59 +6,59 @@ Before anything is proposed, let's start with what can be prioritized... ### Input types -We want to be able to support all of the following configurations: +There are two possible scenarios in which to use a priority queue: -1. User data *is separate* from the priority (two physical instances). -2. User data *contains* the priority (as one or more properties). -3. User data *is* the priority (implements `IComparable`). -4. Rare case: priority is obtainable via some other logic (resides in an object -different from the user data). +1. Each item of user data is *assigned* a *separate* priority (priority is a separate object from the elements in the collection.) We will sometimes call this the *extrinsic priority* scenario. +2. User data either *is itself a priority*, or has some other *natural* priority which can be derived from the element. (It might have a priority property, a it might be a *computed* priority using some function mapping elements to priorities.) We will sometimes call this the *intrinsic priority* scenario. -In this entire document, I will refer to the cases above with **(1)**, **(2)**, -**(3)**, and **(4)**. +### Uniqueness and changes in priority. -### Ideal solution +This implementation of priority queue is actually designed to provide a DICTIONARY-like or SET-like interface, where each element is unique (like a key), and priorities are mutable. This allows you to easily use the data structure for many popular scenarios requiring priority queues. +All elements are equality comparable, and each distinct element (according to equality comparison) can have at most a unique occurence in the queue, and be assigned a single priority value. +That priority value may be updated, over time. -Obviously, our solution should be flexible enough to (respectively): +This means there are really two types of comparators (comparison operators) involved in the definition of a priority queue class - one for comparing *elements* pairwise for equality, and one for comparing *element priorities* pairwise to see which is higher priority. It is unnecessary, and usually undesirable for the same comparator to be used for both priority comparisons and equality comparisons. -1. Simply accept two separate instances. The user should not be forced to create -a wrapper class for the two types only because of our API limitations. -2. Accept an element that already has the priority in it, without duplication -(no copying). -3. Be able to use `IComparable` and don't expect an additional priority. -4. Be able to execute some additional logic that retrieves the priority for a -given element. +### Correct notification of priority changes (single and multiple) -### Our approach +In the case of elements which have an *assigned* priority, the priority is updated by explicitly calling a method updating the priority of the element with a new value. In this case it does not matter whether element priorities are mutated simultaneously, or in sequence. +In the case of elements which have a *natural* priority (not an *assigned* one), but where some event can cause priority of multiple elements to change at one time, it is necessary for the priority queue to be notified *simultaneously* or *atomically* about ALL elements whose priority has changed, +so that it can maintain internal data structure invariant when it performs the update-priority operation. -In order to be able to consume all of that, we need two types of priority -queues: +### Workarounds for uniqueness -* `PriorityQueue`, -* `PriorityQueue`. +If you need to implement a priority queue scenario where duplicates are allowed, it is possible to do this by e.g. using a 'priority-queue of priority-queues' approach. + +## Classes + +There are actually two separate PriorityQueue classes. PriorityQueue implements the *assigned* or *extrinsic priority* scenario, and PriorityQueue implements the *natural* or *intrinsic priority* scenario. ## `PriorityQueue` ```csharp -public class PriorityQueue : IQueue +public class PriorityQueue : ISet, ICollection, IEnumerable, IEnumerable, { public PriorityQueue(); - public PriorityQueue(IComparer comparer); + public PriorityQueue(IComparer priorityComparer); + public PriorityQueue(IComparer priorityComparer, IEqualityComparer equalityComparer); public PriorityQueue(IEnumerable collection); - public PriorityQueue(IEnumerable collection, IComparer comparer); + public PriorityQueue(IEnumerable collection, IComparer priorityComparer); + public PriorityQueue(IEnumerable collection, IComparer priorityComparer, IEqualityComparer equalityComparer); + + public IComparer PriorityComparer { get; } + public IComparer EqualityComparer { get; } - public IComparer Comparer { get; } public int Count { get; } + public bool IsReadOnly { get; } - public bool IsEmpty { get; } + public bool Add(T element); public void Clear(); public bool Contains(T element); - - public void Enqueue(T element); + public void CopyTo(T[] element, int index); + public bool Remove(T element); public T Peek(); public T Dequeue(); - public bool Remove(T element); public bool TryPeek(out T element); public bool TryDequeue(out T element); @@ -66,6 +66,8 @@ public class PriorityQueue : IQueue public IEnumerator GetEnumerator(); IEnumerator IEnumerable.GetEnumerator(); + // + various other ISet interface methods (ExceptWith, Overlaps, IsProperSubsetOf, IsSubsetOf, IsSuperSetOf, IntersectWith, UnionWith, etc) + public struct Enumerator : IEnumerator { public T Current { get; } @@ -77,9 +79,9 @@ public class PriorityQueue : IQueue } ``` -### Scenarios +### Examples -#### (2) +#### (1) Custom class with a priority inside: @@ -99,22 +101,25 @@ var comparer = Comparer.Create((a, b) => }); ``` -And simply uses our priority queue: +And uses our priority queue with the default equalityComparer: ```csharp -var queue = new PriorityQueue(comparer); +var equalityComparer = EqualityComparer..Default; +var queue = new PriorityQueue(comparer, equalityComparer); queue.Enqueue(new MyClass()); ``` -#### (3) +#### (2) -Already comparable type: +Already comparable (and equality-comparable, because all objects are!) type, where the objects themselves are the priorities: ```csharp public class MyClass : IComparable { - public int CompareTo(MyClass other) => /* some logic */ + public int CompareTo(MyClass other) => /* some logic for comparing priorities*/ + public int GetHashCode() => /* if overriding object equality comparison! */ + public bool Equals(object other) => /* if overriding object equality comparison! */ } ``` @@ -124,46 +129,53 @@ Then simply call the default constructor (`Comparer.Default` is assumed): var queue = new PriorityQueue(); ``` -#### (4) - -Priority for `MyClass` is obtainable from some other objects, for example a -dictionary. It is done analogically to **(2)**, simply by some custom logic in -the comparer. - ## `PriorityQueue` ```csharp public class PriorityQueue : - IEnumerable, - IEnumerable<(TElement element, TPriority priority)>, - IReadOnlyCollection<(TElement element, TPriority priority)> + IDictionary, + ICollection>, + IEnumerable>, + IEnumerable { public PriorityQueue(); - public PriorityQueue(IComparer comparer); + public PriorityQueue(IComparer priorityComparer); + public PriorityQueue(IComparer priorityComparer, IEqualityComparer equalityComparer); public PriorityQueue(IEnumerable<(TElement, TPriority)> collection); - public PriorityQueue(IEnumerable<(TElement, TPriority)> collection, IComparer comparer); + public PriorityQueue(IEnumerable<(TElement, TPriority)> collection, IComparer priorityComparer); + public PriorityQueue(IEnumerable<(TElement, TPriority)> collection, IComparer priorityComparer, IEqualityComparer equalityComparer); - public IComparer Comparer { get; } + public IComparer PriorityComparer { get; } public int Count { get; } + public bool IsReadOnly { get; } + + public TPriority Item[TElement] { get; set; } + public ICollection Keys { get; } + public ICollection Values { get; } - public bool IsEmpty { get; } public void Clear(); public bool Contains(TElement element); - public bool Contains(TElement element, out TPriority priority); - public void Enqueue(TElement element, TPriority priority); + public bool Add(TElement element, TPriority priority); - public (TElement element, TPriority priority) Peek(); + public TElement Peek(TElement element); public bool TryPeek(out TElement element, out TPriority priority); public bool TryPeek(out TElement element); - public (TElement element, TPriority priority) Dequeue(); + public TElement Dequeue(); public bool TryDequeue(out TElement element, out TPriority priority); public bool TryDequeue(out TElement element); public bool Remove(TElement element); public bool Remove(TElement element, out TPriority priority); + public bool TryGetValue(TElement element, out TPriority priority); + + public bool ICollection>.Add(KeyValuePair value); + public bool ICollection>.Contains(KeyValuePair value + public bool ICollection>.Remove(KeyValuePair value); + public bool IDictionary.ContainsKey(TElement key); + public IEnumerator<(TElement element, TPriority priority)> GetEnumerator(); IEnumerator IEnumerable.GetEnumerator(); @@ -202,80 +214,13 @@ queue.Enqueue(userData, priority); To both priority queues: -* If the `IComparer` is not delivered, `Comparer.Default` is summoned. +* If the `IComparer` is not delivered, `Comparer.Default` is used. +* If the `IEqualityComparer` is not delivered, `EqualityComparer.Default` is used. * `Peek` and `Dequeue` throw an exception if the collection is empty. -* `TryPeek` and `TryDequeue` only return false. +* `TryPeek` and `TryDequeue` return false if the collection is empty. +* `Add` returns false if the element was already in the set. * `Remove` returns false if the element to remove is not found. -* `Remove` removes only the first occurrence of the specified element. - -## `IQueue` - -With the design above, we can address some voices regarding the introduction of -`IQueue`: - -```csharp -public interface IQueue : - IEnumerable, - IEnumerable, - IReadOnlyCollection -{ - int Count { get; } - - void Clear(); - bool IsEmpty { get; } - - void Enqueue(T element); - - T Peek(); - T Dequeue(); - - bool TryPeek(out T element); - bool TryDequeue(out T element); -} -``` - -### Notes -* Only `PriorityQueue` would implement this interface. -* `IsEmpty` needs to be added to `Queue`. - - -## Open questions - -1. Do we really need `IQueue`? -2. In priority queues, we have `Peek`, and `TryPeek`. There is `Dequeue` and -`TryDequeue`. Should there also be `Remove` and `TryRemove` following the same -pattern or only `bool Remove(T)` (as it is now)? -3. Do we want to be able to remove and update priorities of elements in O(log n) -instead of O(n)? Also, to be able to do conduct such operations on unique nodes -(instead of *whichever is found first*)? This would require us to use some sort -of a handle: - -```csharp -void Enqueue(TElement element, TPriority priority, out object handle); - -void Update(object handle, TPriority priority); - -void Remove(object handle); -``` -4. Do we want to provide an interface for priority queues in the future? If we -release two `PriorityQueue` classes, it may be hard to create an interface for -them. -5. If we don't want to add the concept of handles in our priority queues, we are -basically locking our solution on less efficient and less correct support for -updating / removing arbitrary elements from the collection (problem in Java). -It is additionally more problematic if we don't add a proper interface. A -solution could be to add a proper support for the heaps family (`IHeap` + -possibility of various implementations) in `System.Collections.Specialized`. - * Developers could write their third-party solutions based on a single, - standardized interface. Their code can depend on an interface rather than an - implementation (`PriorityQueue` or `PriorityQueue` — which to choose - as an argument?). - * If such a functionality is added to `CoreFXExtensions`, there would be no - common ground for third-party libraries. - * Decision where this would eventually land (`CoreFX` or not) directly - impacts whether missing features in our support for priority queues are an - issue or not. If we add heaps to `System.Collections.Specialized`, priority - queues can be lacking more power and enhanceability. If we don't, there is an - issue. +## No more open issues +Yay! \ No newline at end of file