Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Developers can use a built in PriorityQueue type #43957

@eiriktsarpalis

Description

@eiriktsarpalis

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 of Heap.
  • 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 separate UnorderedItems property, to more effectively communicate this behaviour.

Implementation Checklist (Definition of Done)

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.

Metadata

Metadata

Labels

Bottom Up WorkNot part of a theme, epic, or user storyCost:SWork that requires one engineer up to 1 weekPriority:3Work that is nice to haveTeam:LibrariesUser StoryA single user-facing feature. Can be grouped under an epic.api-approvedAPI was approved in API review, it can be implementedarea-System.Collections

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions