-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background and Motivation
Adding a priority queue has been a feature long requested by the community, however so far we've fallen short of finalizing a design, primarily due to a lack of consensus on certain high-level features: namely, the support for priority updates and any API or performance considerations this feature entails.
To better identify what a popular .NET priority queue should look like, I have been going through .NET codebases for common usage patterns as well as running benchmarks against various prototypes. The key takeaway is that 90% of the use cases examined do not require priority updates. It also turns out that implementations without update support can be 2-3x faster compared to ones that do support it. Please refer to the original issue for more details on my investigations.
This issue presents a finalized priority queue API proposal, informed by the above findings. The goal is to introduce a PriorityQueue
class that has a simple design, caters to the majority of our users' requirements and is as efficient as possible.
A prototype implementing the proposal can be found here.
Design Decisions
- Class is named
PriorityQueue
instead ofHeap
. - Implements a min priority queue (element with lowest priority dequeued first).
- Priority ordinals are passed as separate values (rather than being encapsulated by the element).
- Uses an array-backed quaternary heap.
- No support for priority updates.
- Not a thread-safe collection.
- Not a stable queue (elements enqueued with equal priority not guaranteed to be dequeued in the same order).
- The type does not implement IEnumerable or ICollection. Unlike
Queue<T>
, PriorityQueue cannot efficiently enumerate elements by ascending priority. We therefore expose the enumerable as a separateUnorderedItems
property, to more effectively communicate this behaviour.
Implementation Checklist (Definition of Done)
- Implement product code & high coverage unit tests. (In PR Add PriorityQueue to System.Collections.Generic (#43957) #46009)
- Add performance benchmarks (In PR Add PriorityQueue benchmarks performance#1665).
- (Optional) Add property-based tests for randomized testing (see the prototype repo examples).
- Add detailed API docs (in the form of triple-slash comments).
Port API docs to the dotnet/dotnet-api-docs repo using the docs porting tool(covered by System.Collections: Backport MS Docs documentation to triple slash #48984).
Proposed API
namespace System.Collections.Generic
{
public class PriorityQueue<TElement, TPriority> // NB does not implement IEnumerable or ICollection
{
/// <summary>
/// Creates an empty PriorityQueue instance.
/// </summary>
public PriorityQueue();
/// <summary>
/// Creates a PriorityQueue instance with specified initial capacity in its backing array.
/// </summary>
public PriorityQueue(int initialCapacity);
/// <summary>
/// Creates a PriorityQueue instance with specified priority comparer.
/// </summary>
public PriorityQueue(IComparer<TPriority>? comparer);
public PriorityQueue(int initialCapacity, IComparer<TPriority>? comparer);
/// <summary>
/// Creates a PriorityQueue populated with the specified values and priorities.
/// </summary>
public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> values);
public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> values, IComparer<TPriority>? comparer);
/// <summary>
/// Gets the current element count in the queue.
/// </summary>
public int Count { get; }
/// <summary>
/// Gets the priority comparer of the queue.
/// </summary>
public IComparer<TPriority> Comparer { get; }
/// <summary>
/// Enqueues the element with specified priority.
/// </summary>
public void Enqueue(TElement element, TPriority priority);
/// <summary>
/// Gets the element with minimal priority, if it exists.
/// </summary>
/// <exception cref="InvalidOperationException">The queue is empty.</exception>
public TElement Peek();
/// <summary>
/// Dequeues the element with minimal priority, if it exists.
/// </summary>
/// <exception cref="InvalidOperationException">The queue is empty.</exception>
public TElement Dequeue();
/// <summary>
/// Try-variants of Dequeue and Peek methods.
/// </summary>
public bool TryDequeue([MaybeNullWhen(false)] out TElement element, [MaybeNullWhen(false)] out TPriority priority);
public bool TryPeek([MaybeNullWhen(false)] out TElement element, [MaybeNullWhen(false)] out TPriority priority);
/// <summary>
/// Combined enqueue/dequeue operation, generally more efficient than sequential Enqueue/Dequeue calls.
/// </summary>
public TElement EnqueueDequeue(TElement element, TPriority priority);
/// <summary>
/// Enqueues a sequence of element/priority pairs to the queue.
/// </summary>
public void EnqueueRange(IEnumerable<(TElement Element, TPriority Priority)> values);
/// <summary>
/// Enqueues a sequence of elements with provided priority.
/// </summary>
public void EnqueueRange(IEnumerable<TElement> values, TPriority priority);
/// <summary>
/// Removes all objects from the PriorityQueue.
/// </summary>
public void Clear();
/// <summary>
/// Ensures that the PriorityQueue can hold the specified capacity and resizes its underlying buffer if necessary.
/// </summary>
public void EnsureCapacity(int capacity);
/// <summary>
/// Sets capacity to the actual number of elements in the queue, if that is less than 90 percent of current capacity.
/// </summary>
public void TrimExcess();
/// <summary>
/// Gets a collection that enumerates the elements of the queue.
/// </summary>
public UnorderedItemsCollection UnorderedItems { get; }
public class UnorderedItemsCollection : IReadOnlyCollection<(TElement Element, TPriority Priority)>, ICollection
{
public struct Enumerator : IEnumerator<(TElement TElement, TPriority Priority)>, IEnumerator { }
public Enumerator GetEnumerator();
}
}
}
Usage Examples
var pq = new PriorityQueue<string, int>();
pq.Enqueue("John", 1940);
pq.Enqueue("Paul", 1942);
pq.Enqueue("George", 1943);
pq.Enqueue("Ringo", 1940);
Assert.Equal("John", pq.Dequeue());
Assert.Equal("Ringo", pq.Dequeue());
Assert.Equal("Paul", pq.Dequeue());
Assert.Equal("George", pq.Dequeue());
Alternative Designs
We recognize the need for heaps that support efficient priority updates, so we will consider introducing a separate specialized class that addresses this requirement at a later stage. Please refer to the original issue for a more in-depth analysis on the alternatives.
Open Questions
- Should we use
KeyValuePair<TPriority, TElement>
instead of(TPriority Priority, TElement Element)
?- We will use tuple types instead of KeyValuePair.
- Should enumeration of elements be sorted by priority?
- No. We will expose the enumerable as separate property whose name emphasizes that it is unsorted.
- Should we include a
bool Contains(TElement element);
method?- We should consider adding this. Will be O(n) operation. Another issue is taking custom element equality into consideration.
- Should we include a
void Remove(TElement element);
method?- No. Can be ambiguous operation in the presence of duplicate elements.