Add PriorityQueue to System.Collections.Generic (#43957) (#46009)
authorPatryk Golebiowski <pgolebiowski@users.noreply.github.com>
Mon, 15 Feb 2021 19:54:37 +0000 (20:54 +0100)
committerGitHub <noreply@github.com>
Mon, 15 Feb 2021 19:54:37 +0000 (19:54 +0000)
* Add PriorityQueue to System.Collections.Generic (#43957)

This commit adds a new data structure, priority queue.

Fixes #43957

* (draft step, to squash) Modify API reference

In this commit, I modified the API reference using [these guidelines](https://github.com/dotnet/runtime/blob/4d784693ebc5f91c7eede32170046355ef3969b2/docs/coding-guidelines/updating-ref-source.md), following the advice from @danmosemsft.

The automatically generated code (with `dotnet msbuild /t:GenerateReferenceAssemblySource`) didn't build out of the box and I tweaked it manually.

* (draft step, to squash) Add tests for PriorityQueue

Added generic tests for <int, int> and <string, string>. Removed non-generic tests.

* (draft step, to squash) Add initial implementation

This commit adds the core of the heap implementation for the priority queue.

It doesn't implement the method `EnqueueDequeue`, as I'm not convinced by it. It also doesn't implement the method `CopyTo(Array array, int index)`, leaving this one for later.

* (draft step, to squash) Rename parameters

* (draft step, to squash) Replace `this.nodes` with `_nodes`

* (draft step, to squash) Use an array and handle sizing ourselves

* (draft step, to squash) Create UnorderedItemsCollection lazily

* (draft step, to squash) Deduplicate constructors

* (draft step, to squash) Replace excessive `var` with explicit types

* (draft step, to squash) Remove `this.` in front of methods

* (draft step, to squash) Improve out-of-range-argument exceptions

* (draft step, to squash) Use error messages from .resx

* (draft step, to squash) Use positive case first in try methods

* (draft step, to squash) Implement UnorderedItemsCollection.CopyTo

* (draft step, to squash) Optimize expressions involving Arity

* (draft step, to squash) Adjust implementation to be consistent with reference

* (draft step, to squash) Implement method `EnqueueDequeue`

* (draft step, to squash) Make EnsureCapacity return int

* (draft step, to squash) Simplify lazy initialization of _unorderedItems

* (draft step, to squash) Use `out _` discard for unused properties

* (draft step, to squash) Relax null checks on elements and priorities

* (draft step, to squash) Simplify method SetCapacity

* (draft step, to squash) Remove MethodImplOptions.AggressiveInlining attributed

* (draft step, to squash) Use Array.Empty if the initial capacity is zero

* (draft step, to squash) Simplify UnorderedItemsCollection.Enumerator declaration

* (draft step, to squash) Simplify GetEnumerator methods

* (draft step, to squash) Optimize EnqueueRange methods

* (draft step, to squash) Capitalize members of (TElement, TPriority)[]

* (draft step, to squash) Improve resize constants

* (draft step, to squash) Remove redundant `.this`

* (draft step, to squash) Optimize EnqueueDequeue

* (draft step, to squash) Reduce indentation

* (draft step, to squash) Simplify math expressions

* (draft step, to squash) Remove the PutAt helper method

* (draft step, to squash) Make the UnorderedItemsCollection constructor internal

* (draft step, to squash) Clear last node slot on removal

* (draft step, to squash) Improve next growth capacity computation

* (draft step, to squash) Optimize Enqueue method

* (draft step, to squash) Make UnorderedItemsCollection.CopyTo implemented explicitly on ICollection

* (draft step, to squash) Improve priority queue tests

* (draft step, to squash) Drop redundant casting

* (draft step, to squash) Cosmetic improvements

* (draft step, to squash) Change signature of UnorderedItemsCollection

* (draft step, to squash) Add test PriorityQueue_Generic_EnqueueDequeue_EqualToMin

* (draft step, to squash) Add tests of enqueue null functionality

* (draft step, to squash) Add test PriorityQueue_Generic_EnsureCapacity_Negative

* (draft step, to squash) Check underlying buffer length in tests with reflection

* (draft step, to squash) Check enumeration invalidation

* (draft step, to squash) Simplify code and improve documentation

src/libraries/System.Collections/ref/System.Collections.cs
src/libraries/System.Collections/src/System.Collections.csproj
src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs [new file with mode: 0644]
src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.Tests.cs [new file with mode: 0644]
src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.cs [new file with mode: 0644]
src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Tests.cs [new file with mode: 0644]
src/libraries/System.Collections/tests/System.Collections.Tests.csproj

index 405eea4..5409b03 100644 (file)
@@ -378,6 +378,51 @@ namespace System.Collections.Generic
             void System.Collections.IEnumerator.Reset() { }
         }
     }
+
+    public partial class PriorityQueue<TElement, TPriority>
+    {
+        public PriorityQueue() { }
+        public PriorityQueue(System.Collections.Generic.IComparer<TPriority>? comparer) { }
+        public PriorityQueue(System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)> items) { }
+        public PriorityQueue(System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)> items, System.Collections.Generic.IComparer<TPriority>? comparer) { }
+        public PriorityQueue(int initialCapacity) { }
+        public PriorityQueue(int initialCapacity, System.Collections.Generic.IComparer<TPriority>? comparer) { }
+        public System.Collections.Generic.IComparer<TPriority> Comparer { get { throw null; } }
+        public int Count { get { throw null; } }
+        public System.Collections.Generic.PriorityQueue<TElement, TPriority>.UnorderedItemsCollection UnorderedItems { get { throw null; } }
+        public void Clear() { }
+        public TElement Dequeue() { throw null; }
+        public void Enqueue(TElement element, TPriority priority) { }
+        public TElement EnqueueDequeue(TElement element, TPriority priority) { throw null; }
+        public void EnqueueRange(System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)> items) { }
+        public void EnqueueRange(System.Collections.Generic.IEnumerable<TElement> elements, TPriority priority) { }
+        public int EnsureCapacity(int capacity) { throw null; }
+        public TElement Peek() { throw null; }
+        public void TrimExcess() { }
+        public bool TryDequeue([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority) { throw null; }
+        public bool TryPeek([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TElement element, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out TPriority priority) { throw null; }
+        public sealed partial class UnorderedItemsCollection : System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)>, System.Collections.Generic.IReadOnlyCollection<(TElement element, TPriority priority)>, System.Collections.ICollection, System.Collections.IEnumerable
+        {
+            internal UnorderedItemsCollection(PriorityQueue<TElement, TPriority> queue) { }
+            public int Count { get { throw null; } }
+            bool System.Collections.ICollection.IsSynchronized { get { throw null; } }
+            object System.Collections.ICollection.SyncRoot { get { throw null; } }
+            void ICollection.CopyTo(System.Array array, int index) { }
+            public System.Collections.Generic.PriorityQueue<TElement, TPriority>.UnorderedItemsCollection.Enumerator GetEnumerator() { throw null; }
+            System.Collections.Generic.IEnumerator<(TElement element, TPriority priority)> System.Collections.Generic.IEnumerable<(TElement element, TPriority priority)>.GetEnumerator() { throw null; }
+            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+            public partial struct Enumerator : System.Collections.Generic.IEnumerator<(TElement element, TPriority priority)>, System.Collections.IEnumerator, System.IDisposable
+            {
+                (TElement element, TPriority priority) IEnumerator<(TElement element, TPriority priority)>.Current { get { throw null; } }
+                public void Dispose() { }
+                public bool MoveNext() { throw null; }
+                public (TElement element, TPriority priority) Current { get { throw null; } }
+                object System.Collections.IEnumerator.Current { get { throw null; } }
+                void System.Collections.IEnumerator.Reset() { }
+            }
+        }
+    }
+
     public partial class Queue<T> : System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.ICollection, System.Collections.IEnumerable
     {
         public Queue() { }
index 9f8e4b8..c8fd9f7 100644 (file)
@@ -13,6 +13,7 @@
     <Compile Include="$(CoreLibSharedDir)System\Collections\Generic\IDictionaryDebugView.cs"
              Link="Common\System\Collections\Generic\IDictionaryDebugView.cs" />
     <Compile Include="System\Collections\Generic\LinkedList.cs" />
+    <Compile Include="System\Collections\Generic\PriorityQueue.cs" />
     <Compile Include="System\Collections\Generic\Queue.cs" />
     <Compile Include="System\Collections\Generic\QueueDebugView.cs" />
     <Compile Include="System\Collections\Generic\SortedDictionary.cs" />
diff --git a/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs b/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs
new file mode 100644 (file)
index 0000000..80fca47
--- /dev/null
@@ -0,0 +1,715 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace System.Collections.Generic
+{
+    /// <summary>
+    /// Represents a data structure in which each element has an associated priority
+    /// that determines the order in which the pair is dequeued.
+    /// </summary>
+    /// <typeparam name="TElement">The type of the element.</typeparam>
+    /// <typeparam name="TPriority">The type of the priority.</typeparam>
+    public class PriorityQueue<TElement, TPriority>
+    {
+        /// <summary>
+        /// Represents an implicit heap-ordered complete d-ary tree, stored as an array.
+        /// </summary>
+        private (TElement Element, TPriority Priority)[] _nodes;
+
+        private UnorderedItemsCollection? _unorderedItems;
+
+        /// <summary>
+        /// The number of nodes in the heap.
+        /// </summary>
+        private int _size;
+
+        /// <summary>
+        /// Version updated on mutation to help validate enumerators operate on a consistent state.
+        /// </summary>
+        private int _version;
+
+        /// <summary>
+        /// When the underlying buffer for the heap nodes grows to accomodate more nodes,
+        /// this is the minimum the capacity will grow by.
+        /// </summary>
+        private const int MinimumElementsToGrowBy = 4;
+
+        /// <summary>
+        /// The index at which the heap root is maintained.
+        /// </summary>
+        private const int RootIndex = 0;
+
+        /// <summary>
+        /// Specifies the arity of the d-ary heap, which here is quaternary.
+        /// </summary>
+        private const int Arity = 4;
+
+        /// <summary>
+        /// The binary logarithm of <see cref="Arity" />.
+        /// </summary>
+        private const int Log2Arity = 2;
+
+        /// <summary>
+        /// Creates an empty priority queue.
+        /// </summary>
+        public PriorityQueue()
+            : this(initialCapacity: 0, comparer: null)
+        {
+        }
+
+        /// <summary>
+        /// Creates an empty priority queue with the specified initial capacity for its underlying array.
+        /// </summary>
+        public PriorityQueue(int initialCapacity)
+            : this(initialCapacity, comparer: null)
+        {
+        }
+
+        /// <summary>
+        /// Creates an empty priority queue with the specified priority comparer.
+        /// </summary>
+        public PriorityQueue(IComparer<TPriority>? comparer)
+            : this(initialCapacity: 0, comparer)
+        {
+        }
+
+        /// <summary>
+        /// Creates an empty priority queue with the specified priority comparer and
+        /// the specified initial capacity for its underlying array.
+        /// </summary>
+        public PriorityQueue(int initialCapacity, IComparer<TPriority>? comparer)
+        {
+            if (initialCapacity < 0)
+            {
+                throw new ArgumentOutOfRangeException(
+                    nameof(initialCapacity), initialCapacity, SR.ArgumentOutOfRange_NeedNonNegNum);
+            }
+
+            _nodes = (initialCapacity == 0)
+                ? Array.Empty<(TElement, TPriority)>()
+                : new (TElement, TPriority)[initialCapacity];
+
+            Comparer = comparer ?? Comparer<TPriority>.Default;
+        }
+
+        /// <summary>
+        /// Creates a priority queue populated with the specified elements and priorities.
+        /// </summary>
+        public PriorityQueue(IEnumerable<(TElement element, TPriority priority)> items)
+            : this(items, comparer: null)
+        {
+        }
+
+        /// <summary>
+        /// Creates a priority queue populated with the specified elements and priorities,
+        /// and with the specified priority comparer.
+        /// </summary>
+        public PriorityQueue(IEnumerable<(TElement element, TPriority priority)> items, IComparer<TPriority>? comparer)
+        {
+            if (items is null)
+            {
+                throw new ArgumentNullException(nameof(items));
+            }
+
+            _nodes = EnumerableHelpers.ToArray(items, out _size);
+            Comparer = comparer ?? Comparer<TPriority>.Default;
+
+            if (_size > 1)
+            {
+                Heapify();
+            }
+        }
+
+        /// <summary>
+        /// Gets the current amount of items in the priority queue.
+        /// </summary>
+        public int Count => _size;
+
+        /// <summary>
+        /// Gets the priority comparer of the priority queue.
+        /// </summary>
+        public IComparer<TPriority> Comparer { get; }
+
+        /// <summary>
+        /// Gets a collection that enumerates the elements of the queue.
+        /// </summary>
+        public UnorderedItemsCollection UnorderedItems => _unorderedItems ??= new UnorderedItemsCollection(this);
+
+        /// <summary>
+        /// Enqueues the specified element and associates it with the specified priority.
+        /// </summary>
+        public void Enqueue(TElement element, TPriority priority)
+        {
+            EnsureEnoughCapacityBeforeAddingNode();
+
+            // Virtually add the node at the end of the underlying array.
+            // Note that the node being enqueued does not need to be physically placed
+            // there at this point, as such an assignment would be redundant.
+            _size++;
+            _version++;
+
+            // Restore the heap order
+            int lastNodeIndex = GetLastNodeIndex();
+            MoveUp((element, priority), lastNodeIndex);
+        }
+
+        /// <summary>
+        /// Gets the element associated with the minimal priority.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">The queue is empty.</exception>
+        public TElement Peek()
+        {
+            if (_size == 0)
+            {
+                throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue);
+            }
+
+            return _nodes[RootIndex].Element;
+        }
+
+        /// <summary>
+        /// Dequeues the element associated with the minimal priority.
+        /// </summary>
+        /// <exception cref="InvalidOperationException">The queue is empty.</exception>
+        public TElement Dequeue()
+        {
+            if (_size == 0)
+            {
+                throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue);
+            }
+
+            TElement element = _nodes[RootIndex].Element;
+            Remove(RootIndex);
+            return element;
+        }
+
+        /// <summary>
+        /// Dequeues the element associated with the minimal priority
+        /// </summary>
+        /// <returns>
+        /// <see langword="true"/> if the priority queue is non-empty; <see langword="false"/> otherwise.
+        /// </returns>
+        public bool TryDequeue([MaybeNullWhen(false)] out TElement element, [MaybeNullWhen(false)] out TPriority priority)
+        {
+            if (_size != 0)
+            {
+                (element, priority) = _nodes[RootIndex];
+                Remove(RootIndex);
+                return true;
+            }
+
+            element = default;
+            priority = default;
+            return false;
+        }
+
+        /// <summary>
+        /// Gets the element associated with the minimal priority.
+        /// </summary>
+        /// <returns>
+        /// <see langword="true"/> if the priority queue is non-empty; <see langword="false"/> otherwise.
+        /// </returns>
+        public bool TryPeek([MaybeNullWhen(false)] out TElement element, [MaybeNullWhen(false)] out TPriority priority)
+        {
+            if (_size != 0)
+            {
+                (element, priority) = _nodes[RootIndex];
+                return true;
+            }
+
+            element = default;
+            priority = default;
+            return false;
+        }
+
+        /// <summary>
+        /// Combined enqueue/dequeue operation, generally more efficient than sequential Enqueue/Dequeue calls.
+        /// </summary>
+        public TElement EnqueueDequeue(TElement element, TPriority priority)
+        {
+            (TElement Element, TPriority Priority) root = _nodes[RootIndex];
+
+            if (Comparer.Compare(priority, root.Priority) <= 0)
+            {
+                return element;
+            }
+            else
+            {
+                (TElement Element, TPriority Priority) newRoot = (element, priority);
+                _nodes[RootIndex] = newRoot;
+
+                MoveDown(newRoot, RootIndex);
+                _version++;
+
+                return root.Element;
+            }
+        }
+
+        /// <summary>
+        /// Enqueues a collection of element/priority pairs.
+        /// </summary>
+        public void EnqueueRange(IEnumerable<(TElement Element, TPriority Priority)> items)
+        {
+            if (items is null)
+            {
+                throw new ArgumentNullException(nameof(items));
+            }
+
+            if (_size == 0)
+            {
+                _nodes = EnumerableHelpers.ToArray(items, out _size);
+
+                if (_size > 1)
+                {
+                    Heapify();
+                }
+            }
+            else
+            {
+                foreach ((TElement element, TPriority priority) in items)
+                {
+                    Enqueue(element, priority);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Enqueues a collection of elements, each associated with the specified priority.
+        /// </summary>
+        public void EnqueueRange(IEnumerable<TElement> elements, TPriority priority)
+        {
+            if (elements is null)
+            {
+                throw new ArgumentNullException(nameof(elements));
+            }
+
+            if (_size == 0)
+            {
+                using (IEnumerator<TElement> enumerator = elements.GetEnumerator())
+                {
+                    if (enumerator.MoveNext())
+                    {
+                        _nodes = new (TElement, TPriority)[MinimumElementsToGrowBy];
+                        _nodes[0] = (enumerator.Current, priority);
+                        _size = 1;
+
+                        while (enumerator.MoveNext())
+                        {
+                            EnsureEnoughCapacityBeforeAddingNode();
+                            _nodes[_size++] = (enumerator.Current, priority);
+                        }
+
+                        if (_size > 1)
+                        {
+                            Heapify();
+                        }
+                    }
+                }
+            }
+            else
+            {
+                foreach (TElement element in elements)
+                {
+                    Enqueue(element, priority);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Removes all items from the priority queue.
+        /// </summary>
+        public void Clear()
+        {
+            if (RuntimeHelpers.IsReferenceOrContainsReferences<(TElement, TPriority)>())
+            {
+                // Clear the elements so that the gc can reclaim the references
+                Array.Clear(_nodes, 0, _size);
+            }
+            _size = 0;
+            _version++;
+        }
+
+        /// <summary>
+        /// Ensures that the priority queue has the specified capacity
+        /// and resizes its underlying array if necessary.
+        /// </summary>
+        public int EnsureCapacity(int capacity)
+        {
+            if (capacity < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(capacity), capacity, SR.ArgumentOutOfRange_NeedNonNegNum);
+            }
+
+            if (_nodes.Length < capacity)
+            {
+                SetCapacity(Math.Max(capacity, ComputeCapacityForNextGrowth()));
+            }
+
+            return _nodes.Length;
+        }
+
+        /// <summary>
+        /// Sets the capacity to the actual number of items in the priority queue,
+        /// if that is less than 90 percent of current capacity.
+        /// </summary>
+        public void TrimExcess()
+        {
+            int threshold = (int)(_nodes.Length * 0.9);
+            if (_size < threshold)
+            {
+                SetCapacity(_size);
+            }
+        }
+
+        private void EnsureEnoughCapacityBeforeAddingNode()
+        {
+            Debug.Assert(_size <= _nodes.Length);
+            if (_size == _nodes.Length)
+            {
+                SetCapacity(ComputeCapacityForNextGrowth());
+            }
+        }
+
+        private int ComputeCapacityForNextGrowth()
+        {
+            const int GrowthFactor = 2;
+            const int MaxArrayLength = 0X7FEFFFFF;
+
+            int newCapacity = Math.Max(_nodes.Length * GrowthFactor, _nodes.Length + MinimumElementsToGrowBy);
+
+            // Allow the structure to grow to maximum possible capacity (~2G elements) before encountering overflow.
+            // Note that this check works even when _nodes.Length overflowed thanks to the (uint) cast.
+
+            if ((uint)newCapacity > MaxArrayLength)
+            {
+                newCapacity = MaxArrayLength;
+            }
+
+            return newCapacity;
+        }
+
+        /// <summary>
+        /// Grows or shrinks the array holding nodes. Capacity must be >= _size.
+        /// </summary>
+        private void SetCapacity(int capacity)
+        {
+            Array.Resize(ref _nodes, capacity);
+            _version++;
+        }
+
+        /// <summary>
+        /// Removes the node at the specified index.
+        /// </summary>
+        private void Remove(int indexOfNodeToRemove)
+        {
+            // The idea is to replace the specified node by the very last
+            // node and shorten the array by one.
+
+            int lastNodeIndex = GetLastNodeIndex();
+            (TElement Element, TPriority Priority) lastNode = _nodes[lastNodeIndex];
+            _nodes[lastNodeIndex] = default;
+            _size--;
+            _version++;
+
+            // In case we wanted to remove the node that was the last one,
+            // we are done.
+
+            if (indexOfNodeToRemove == lastNodeIndex)
+            {
+                return;
+            }
+
+            // Our last node was erased from the array and needs to be
+            // inserted again. Of course, we will overwrite the node we
+            // wanted to remove. After that operation, we will need
+            // to restore the heap property (in general).
+
+            (TElement Element, TPriority Priority) nodeToRemove = _nodes[indexOfNodeToRemove];
+
+            int relation = Comparer.Compare(lastNode.Priority, nodeToRemove.Priority);
+            _nodes[indexOfNodeToRemove] = lastNode;
+
+            if (relation < 0)
+            {
+                MoveUp(lastNode, indexOfNodeToRemove);
+            }
+            else
+            {
+                MoveDown(lastNode, indexOfNodeToRemove);
+            }
+        }
+
+        /// <summary>
+        /// Gets the index of the last node in the heap.
+        /// </summary>
+        private int GetLastNodeIndex() => _size - 1;
+
+        /// <summary>
+        /// Gets the index of an element's parent.
+        /// </summary>
+        private int GetParentIndex(int index) => (index - 1) >> Log2Arity;
+
+        /// <summary>
+        /// Gets the index of the first child of an element.
+        /// </summary>
+        private int GetFirstChildIndex(int index) => Arity * index + 1;
+
+        /// <summary>
+        /// Converts an unordered list into a heap.
+        /// </summary>
+        private void Heapify()
+        {
+            // Leaves of the tree are in fact 1-element heaps, for which there
+            // is no need to correct them. The heap property needs to be restored
+            // only for higher nodes, starting from the first node that has children.
+            // It is the parent of the very last element in the array.
+
+            int lastNodeIndex = GetLastNodeIndex();
+            int lastParentWithChildren = GetParentIndex(lastNodeIndex);
+
+            for (int index = lastParentWithChildren; index >= 0; --index)
+            {
+                MoveDown(_nodes[index], index);
+            }
+        }
+
+        /// <summary>
+        /// Moves a node up in the tree to restore heap order.
+        /// </summary>
+        private void MoveUp((TElement element, TPriority priority) node, int nodeIndex)
+        {
+            // Instead of swapping items all the way to the root, we will perform
+            // a similar optimization as in the insertion sort.
+
+            while (nodeIndex > 0)
+            {
+                int parentIndex = GetParentIndex(nodeIndex);
+                (TElement Element, TPriority Priority) parent = _nodes[parentIndex];
+
+                if (Comparer.Compare(node.priority, parent.Priority) < 0)
+                {
+                    _nodes[nodeIndex] = parent;
+                    nodeIndex = parentIndex;
+                }
+                else
+                {
+                    break;
+                }
+            }
+
+            _nodes[nodeIndex] = node;
+        }
+
+        /// <summary>
+        /// Moves a node down in the tree to restore heap order.
+        /// </summary>
+        private void MoveDown((TElement element, TPriority priority) node, int nodeIndex)
+        {
+            // The node to move down will not actually be swapped every time.
+            // Rather, values on the affected path will be moved up, thus leaving a free spot
+            // for this value to drop in. Similar optimization as in the insertion sort.
+
+            int i;
+            while ((i = GetFirstChildIndex(nodeIndex)) < _size)
+            {
+                // Check if the current node (pointed by 'nodeIndex') should really be extracted
+                // first, or maybe one of its children should be extracted earlier.
+                (TElement Element, TPriority Priority) topChild = _nodes[i];
+                int childrenIndexesLimit = Math.Min(i + Arity, _size);
+                int topChildIndex = i;
+
+                while (++i < childrenIndexesLimit)
+                {
+                    (TElement Element, TPriority Priority) child = _nodes[i];
+                    if (Comparer.Compare(child.Priority, topChild.Priority) < 0)
+                    {
+                        topChild = child;
+                        topChildIndex = i;
+                    }
+                }
+
+                // In case no child needs to be extracted earlier than the current node,
+                // there is nothing more to do - the right spot was found.
+                if (Comparer.Compare(node.priority, topChild.Priority) <= 0)
+                {
+                    break;
+                }
+
+                // Move the top child up by one node and now investigate the
+                // node that was considered to be the top child (recursive).
+                _nodes[nodeIndex] = topChild;
+                nodeIndex = topChildIndex;
+            }
+
+            _nodes[nodeIndex] = node;
+        }
+
+        public sealed class UnorderedItemsCollection : IReadOnlyCollection<(TElement element, TPriority priority)>, ICollection
+        {
+            private readonly PriorityQueue<TElement, TPriority> _queue;
+
+            internal UnorderedItemsCollection(PriorityQueue<TElement, TPriority> queue)
+            {
+                _queue = queue;
+            }
+
+            public int Count => _queue._size;
+            object ICollection.SyncRoot => this;
+            bool ICollection.IsSynchronized => false;
+
+            void ICollection.CopyTo(Array array, int index)
+            {
+                if (array is null)
+                {
+                    throw new ArgumentNullException(nameof(array));
+                }
+
+                if (array.Rank != 1)
+                {
+                    throw new ArgumentException(SR.Arg_RankMultiDimNotSupported, nameof(array));
+                }
+
+                if (array.GetLowerBound(0) != 0)
+                {
+                    throw new ArgumentException(SR.Arg_NonZeroLowerBound, nameof(array));
+                }
+
+                if (index < 0 || index > array.Length)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(index), index, SR.ArgumentOutOfRange_Index);
+                }
+
+                if (array.Length - index < _queue._size)
+                {
+                    throw new ArgumentException(SR.Argument_InvalidOffLen);
+                }
+
+                try
+                {
+                    Array.Copy(_queue._nodes, 0, array, index, _queue._size);
+                }
+                catch (ArrayTypeMismatchException)
+                {
+                    throw new ArgumentException(SR.Argument_InvalidArrayType, nameof(array));
+                }
+            }
+
+            public struct Enumerator : IEnumerator<(TElement element, TPriority priority)>
+            {
+                private readonly PriorityQueue<TElement, TPriority> _queue;
+                private readonly int _version;
+
+                private int _index;
+                private (TElement element, TPriority priority)? _currentElement;
+
+                private const int FirstCallToEnumerator = -2;
+                private const int EndOfEnumeration = -1;
+
+                internal Enumerator(PriorityQueue<TElement, TPriority> queue)
+                {
+                    _queue = queue;
+                    _version = queue._version;
+                    _index = FirstCallToEnumerator;
+                    _currentElement = default;
+                }
+
+                public void Dispose()
+                {
+                    _index = EndOfEnumeration;
+                }
+
+                public bool MoveNext()
+                {
+                    if (_version != _queue._version)
+                    {
+                        throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion);
+                    }
+
+                    if (_index == FirstCallToEnumerator)
+                    {
+                        if (_queue._size > 0)
+                        {
+                            _index = 0;
+                            _currentElement = _queue._nodes[_index];
+                            return true;
+                        }
+                        else
+                        {
+                            _index = EndOfEnumeration;
+                            return false;
+                        }
+                    }
+
+                    if (_index == EndOfEnumeration)
+                    {
+                        return false;
+                    }
+
+                    // advance enumerator
+                    _index++;
+
+                    if (_index < _queue._size)
+                    {
+                        _currentElement = _queue._nodes[_index];
+                        return true;
+                    }
+                    else
+                    {
+                        _index = EndOfEnumeration;
+                        _currentElement = default;
+                        return false;
+                    }
+                }
+
+                public (TElement element, TPriority priority) Current
+                {
+                    get
+                    {
+                        if (_index < 0)
+                        {
+                            ThrowEnumerationNotStartedOrEnded();
+                        }
+                        return _currentElement!.Value;
+                    }
+                }
+
+                private void ThrowEnumerationNotStartedOrEnded()
+                {
+                    Debug.Assert(_index == FirstCallToEnumerator || _index == EndOfEnumeration);
+
+                    string message = _index == FirstCallToEnumerator
+                        ? SR.InvalidOperation_EnumNotStarted
+                        : SR.InvalidOperation_EnumEnded;
+
+                    throw new InvalidOperationException(message);
+                }
+
+                object IEnumerator.Current => Current;
+
+                void IEnumerator.Reset()
+                {
+                    if (_version != _queue._version)
+                    {
+                        throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion);
+                    }
+
+                    _index = FirstCallToEnumerator;
+                    _currentElement = default;
+                }
+            }
+
+            public Enumerator GetEnumerator()
+                => new Enumerator(_queue);
+
+            IEnumerator<(TElement element, TPriority priority)> IEnumerable<(TElement element, TPriority priority)>.GetEnumerator()
+                => GetEnumerator();
+
+            IEnumerator IEnumerable.GetEnumerator()
+                => GetEnumerator();
+        }
+    }
+}
diff --git a/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.Tests.cs b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.Tests.cs
new file mode 100644 (file)
index 0000000..7100b9f
--- /dev/null
@@ -0,0 +1,356 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+
+namespace System.Collections.Tests
+{
+    public abstract class PriorityQueue_Generic_Tests<TElement, TPriority> : TestBase<(TElement, TPriority)>
+    {
+        #region Helper methods
+
+        protected IEnumerable<(TElement, TPriority)> GenericIEnumerableFactory(int count)
+        {
+            const int MagicValue = 34;
+            int seed = count * MagicValue;
+            for (int i = 0; i < count; i++)
+            {
+                yield return CreateT(seed++);
+            }
+        }
+
+        protected PriorityQueue<TElement, TPriority> GenericPriorityQueueFactory(
+            int initialCapacity, int countOfItemsToGenerate, out List<(TElement element, TPriority priority)> generatedItems)
+        {
+            generatedItems = this.GenericIEnumerableFactory(countOfItemsToGenerate).ToList();
+
+            var queue = new PriorityQueue<TElement, TPriority>(initialCapacity);
+            foreach (var (element, priority) in generatedItems)
+            {
+                queue.Enqueue(element, priority);
+            }
+
+            return queue;
+        }
+
+        #endregion
+
+        #region Constructors
+
+        [Fact]
+        public void PriorityQueue_Generic_Constructor()
+        {
+            var queue = new PriorityQueue<TElement, TPriority>();
+
+            Assert.Equal(expected: 0, queue.Count);
+            Assert.Empty(queue.UnorderedItems);
+            Assert.Equal(queue.Comparer, Comparer<TPriority>.Default);
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidCollectionSizes))]
+        public void PriorityQueue_Generic_Constructor_int(int initialCapacity)
+        {
+            var queue = new PriorityQueue<TElement, TPriority>(initialCapacity);
+
+            Assert.Empty(queue.UnorderedItems);
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_Constructor_int_Negative_ThrowsArgumentOutOfRangeException()
+        {
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("initialCapacity", () => new PriorityQueue<TElement, TPriority>(-1));
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("initialCapacity", () => new PriorityQueue<TElement, TPriority>(int.MinValue));
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_Constructor_IComparer()
+        {
+            IComparer<TPriority> comparer = Comparer<TPriority>.Default;
+            var queue = new PriorityQueue<TElement, TPriority>(comparer);
+
+            Assert.Equal(comparer, queue.Comparer);
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_Constructor_IComparer_Null()
+        {
+            var queue = new PriorityQueue<TElement, TPriority>((IComparer<TPriority>)null);
+            Assert.Equal(Comparer<TPriority>.Default, queue.Comparer);
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidCollectionSizes))]
+        public void PriorityQueue_Generic_Constructor_int_IComparer(int initialCapacity)
+        {
+            IComparer<TPriority> comparer = Comparer<TPriority>.Default;
+            var queue = new PriorityQueue<TElement, TPriority>(initialCapacity);
+
+            Assert.Empty(queue.UnorderedItems);
+            Assert.Equal(comparer, queue.Comparer);
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidCollectionSizes))]
+        public void PriorityQueue_Generic_Constructor_IEnumerable(int count)
+        {
+            HashSet<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToHashSet();
+            PriorityQueue<TElement, TPriority> queue = new PriorityQueue<TElement, TPriority>(itemsToEnqueue);
+            Assert.True(itemsToEnqueue.SetEquals(queue.UnorderedItems));
+        }
+
+        #endregion
+
+        #region Enqueue, Dequeue, Peek
+
+        [Theory]
+        [MemberData(nameof(ValidCollectionSizes))]
+        public void PriorityQueue_Generic_Enqueue(int count)
+        {
+            PriorityQueue<TElement, TPriority> queue = GenericPriorityQueueFactory(count, count, out var generatedItems);
+            HashSet<(TElement, TPriority)> expectedItems = generatedItems.ToHashSet();
+
+            Assert.Equal(count, queue.Count);
+            var actualItems = queue.UnorderedItems.ToArray();
+            Assert.True(expectedItems.SetEquals(actualItems));
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_Dequeue_EmptyCollection()
+        {
+            var queue = new PriorityQueue<TElement, TPriority>();
+
+            Assert.False(queue.TryDequeue(out _, out _));
+            Assert.Throws<InvalidOperationException>(() => queue.Dequeue());
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_Peek_EmptyCollection()
+        {
+            var queue = new PriorityQueue<TElement, TPriority>();
+
+            Assert.False(queue.TryPeek(out _, out _));
+            Assert.Throws<InvalidOperationException>(() => queue.Peek());
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidPositiveCollectionSizes))]
+        public void PriorityQueue_Generic_Peek_PositiveCount(int count)
+        {
+            IReadOnlyCollection<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToArray();
+            (TElement element, TPriority priority) expectedPeek = itemsToEnqueue.First();
+            PriorityQueue<TElement, TPriority> queue = new PriorityQueue<TElement, TPriority>();
+
+            foreach (var (element, priority) in itemsToEnqueue)
+            {
+                if (queue.Comparer.Compare(priority, expectedPeek.priority) < 0)
+                {
+                    expectedPeek = (element, priority);
+                }
+
+                queue.Enqueue(element, priority);
+
+                var actualPeekElement = queue.Peek();
+                var actualTryPeekSuccess = queue.TryPeek(out TElement actualTryPeekElement, out TPriority actualTryPeekPriority);
+
+                Assert.Equal(expectedPeek.element, actualPeekElement);
+                Assert.True(actualTryPeekSuccess);
+                Assert.Equal(expectedPeek.element, actualTryPeekElement);
+                Assert.Equal(expectedPeek.priority, actualTryPeekPriority);
+            }
+        }
+
+        [Theory]
+        [InlineData(0, 5)]
+        [InlineData(1, 1)]
+        [InlineData(3, 100)]
+        public void PriorityQueue_Generic_PeekAndDequeue(int initialCapacity, int count)
+        {
+            PriorityQueue<TElement, TPriority> queue = this.GenericPriorityQueueFactory(initialCapacity, count, out var generatedItems);
+
+            var expectedPeekPriorities = generatedItems
+                .Select(x => x.priority)
+                .OrderBy(x => x, queue.Comparer)
+                .ToArray();
+
+            for (var i = 0; i < count; ++i)
+            {
+                var expectedPeekPriority = expectedPeekPriorities[i];
+
+                var actualTryPeekSuccess = queue.TryPeek(out TElement actualTryPeekElement, out TPriority actualTryPeekPriority);
+                var actualTryDequeueSuccess = queue.TryDequeue(out TElement actualTryDequeueElement, out TPriority actualTryDequeuePriority);
+
+                Assert.True(actualTryPeekSuccess);
+                Assert.True(actualTryDequeueSuccess);
+                Assert.Equal(expectedPeekPriority, actualTryPeekPriority);
+                Assert.Equal(expectedPeekPriority, actualTryDequeuePriority);
+            }
+
+            Assert.Equal(expected: 0, queue.Count);
+            Assert.False(queue.TryPeek(out _, out _));
+            Assert.False(queue.TryDequeue(out _, out _));
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidCollectionSizes))]
+        public void PriorityQueue_Generic_EnqueueRange_IEnumerable(int count)
+        {
+            HashSet<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToHashSet();
+            PriorityQueue<TElement, TPriority> queue = new PriorityQueue<TElement, TPriority>();
+
+            queue.EnqueueRange(itemsToEnqueue);
+
+            Assert.True(itemsToEnqueue.SetEquals(queue.UnorderedItems));
+        }
+
+        #endregion
+
+        #region Clear
+
+        [Theory]
+        [MemberData(nameof(ValidCollectionSizes))]
+        public void PriorityQueue_Generic_Clear(int count)
+        {
+            PriorityQueue<TElement, TPriority> queue = this.GenericPriorityQueueFactory(initialCapacity: 0, count, out _);
+
+            Assert.Equal(count, queue.Count);
+            queue.Clear();
+            Assert.Equal(expected: 0, queue.Count);
+        }
+
+        #endregion
+
+        #region EnsureCapacity, TrimExcess
+
+        [Fact]
+        public void PriorityQueue_Generic_EnsureCapacity_Negative()
+        {
+            PriorityQueue<TElement, TPriority> queue = new PriorityQueue<TElement, TPriority>();
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("capacity", () => queue.EnsureCapacity(-1));
+            AssertExtensions.Throws<ArgumentOutOfRangeException>("capacity", () => queue.EnsureCapacity(int.MinValue));
+        }
+
+        [Theory]
+        [InlineData(0, 0)]
+        [InlineData(0, 5)]
+        [InlineData(1, 1)]
+        [InlineData(3, 100)]
+        public void PriorityQueue_Generic_TrimExcess_ValidQueueThatHasntBeenRemovedFrom(int initialCapacity, int count)
+        {
+            PriorityQueue<TElement, TPriority> queue = this.GenericPriorityQueueFactory(initialCapacity, count, out _);
+            queue.TrimExcess();
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidCollectionSizes))]
+        public void PriorityQueue_Generic_TrimExcess_Repeatedly(int count)
+        {
+            PriorityQueue<TElement, TPriority> queue = this.GenericPriorityQueueFactory(initialCapacity: 0, count, out _);
+
+            Assert.Equal(count, queue.Count);
+            queue.TrimExcess();
+            queue.TrimExcess();
+            queue.TrimExcess();
+            Assert.Equal(count, queue.Count);
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidPositiveCollectionSizes))]
+        public void PriorityQueue_Generic_EnsureCapacityAndTrimExcess(int count)
+        {
+            IReadOnlyCollection<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToArray();
+            PriorityQueue<TElement, TPriority> queue = new PriorityQueue<TElement, TPriority>();
+            int expectedCount = 0;
+            Random random = new Random(Seed: 34);
+            int getNextEnsureCapacity() => random.Next(0, count * 2);
+            void trimAndEnsureCapacity()
+            {
+                queue.TrimExcess();
+
+                int capacityAfterEnsureCapacity = queue.EnsureCapacity(getNextEnsureCapacity());
+                Assert.Equal(capacityAfterEnsureCapacity, GetUnderlyingBufferCapacity(queue));
+
+                int capacityAfterTrimExcess = (queue.Count < (int)(capacityAfterEnsureCapacity * 0.9)) ? queue.Count : capacityAfterEnsureCapacity;
+                queue.TrimExcess();
+                Assert.Equal(capacityAfterTrimExcess, GetUnderlyingBufferCapacity(queue));
+            };
+
+            foreach (var (element, priority) in itemsToEnqueue)
+            {
+                trimAndEnsureCapacity();
+                queue.Enqueue(element, priority);
+                expectedCount++;
+                Assert.Equal(expectedCount, queue.Count);
+            }
+
+            while (expectedCount > 0)
+            {
+                queue.Dequeue();
+                trimAndEnsureCapacity();
+                expectedCount--;
+                Assert.Equal(expectedCount, queue.Count);
+            }
+
+            trimAndEnsureCapacity();
+            Assert.Equal(0, queue.Count);
+        }
+
+        private static int GetUnderlyingBufferCapacity(PriorityQueue<TElement, TPriority> queue)
+        {
+            FieldInfo nodesType = queue.GetType().GetField("_nodes", BindingFlags.NonPublic | BindingFlags.Instance);
+            var nodes = ((TElement Element, TPriority Priority)[])nodesType.GetValue(queue);
+            return nodes.Length;
+        }
+
+        #endregion
+
+        #region Enumeration
+
+        [Theory]
+        [MemberData(nameof(ValidPositiveCollectionSizes))]
+        public void PriorityQueue_Enumeration_OrderingIsConsistent(int count)
+        {
+            PriorityQueue<TElement, TPriority> queue = this.GenericPriorityQueueFactory(initialCapacity: 0, count, out _);
+
+            (TElement, TPriority)[] firstEnumeration = queue.UnorderedItems.ToArray();
+            (TElement, TPriority)[] secondEnumeration = queue.UnorderedItems.ToArray();
+
+            Assert.Equal(firstEnumeration.Length, count);
+            Assert.True(firstEnumeration.SequenceEqual(secondEnumeration));
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidPositiveCollectionSizes))]
+        public void PriorityQueue_Enumeration_InvalidationOnModifiedCollection(int count)
+        {
+            IReadOnlyCollection<(TElement, TPriority)> itemsToEnqueue = this.GenericIEnumerableFactory(count).ToArray();
+            PriorityQueue<TElement, TPriority> queue = new PriorityQueue<TElement, TPriority>();
+            queue.EnqueueRange(itemsToEnqueue.Take(count - 1));
+            var enumerator = queue.UnorderedItems.GetEnumerator();
+
+            (TElement element, TPriority priority) = itemsToEnqueue.Last();
+            queue.Enqueue(element, priority);
+            Assert.Throws<InvalidOperationException>(() => enumerator.MoveNext());
+        }
+
+        [Theory]
+        [MemberData(nameof(ValidPositiveCollectionSizes))]
+        public void PriorityQueue_Enumeration_InvalidationOnModifiedCapacity(int count)
+        {
+            PriorityQueue<TElement, TPriority> queue = this.GenericPriorityQueueFactory(initialCapacity: 0, count, out _);
+            var enumerator = queue.UnorderedItems.GetEnumerator();
+
+            int capacityBefore = GetUnderlyingBufferCapacity(queue);
+            queue.EnsureCapacity(count * 2 + 4);
+            int capacityAfter = GetUnderlyingBufferCapacity(queue);
+
+            Assert.NotEqual(capacityBefore, capacityAfter);
+            Assert.Throws<InvalidOperationException>(() => enumerator.MoveNext());
+        }
+
+        #endregion
+    }
+}
diff --git a/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.cs b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Generic.cs
new file mode 100644 (file)
index 0000000..624649b
--- /dev/null
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Collections.Tests
+{
+    public class PriorityQueue_Generic_Tests_string_string : PriorityQueue_Generic_Tests<string, string>
+    {
+        protected override (string, string) CreateT(int seed)
+        {
+            var element = this.CreateString(seed);
+            var priority = this.CreateString(seed);
+            return (element, priority);
+        }
+
+        protected string CreateString(int seed)
+        {
+            int stringLength = seed % 10 + 5;
+            Random rand = new Random(seed);
+            byte[] bytes = new byte[stringLength];
+            rand.NextBytes(bytes);
+            return Convert.ToBase64String(bytes);
+        }
+    }
+
+    public class PriorityQueue_Generic_Tests_int_int : PriorityQueue_Generic_Tests<int, int>
+    {
+        protected override (int, int) CreateT(int seed)
+        {
+            var element = this.CreateInt(seed);
+            var priority = this.CreateInt(seed);
+            return (element, priority);
+        }
+
+        protected int CreateInt(int seed) => new Random(seed).Next();
+    }
+}
diff --git a/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Tests.cs b/src/libraries/System.Collections/tests/Generic/PriorityQueue/PriorityQueue.Tests.cs
new file mode 100644 (file)
index 0000000..10ef979
--- /dev/null
@@ -0,0 +1,99 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using Xunit;
+
+namespace System.Collections.Tests
+{
+    public class PriorityQueue_NonGeneric_Tests : TestBase
+    {
+        protected PriorityQueue<string, int> SmallPriorityQueueFactory(out HashSet<(string, int)> items)
+        {
+            items = new HashSet<(string, int)>
+            {
+                ("one", 1),
+                ("two", 2),
+                ("three", 3)
+            };
+            var queue = new PriorityQueue<string, int>(items);
+
+            return queue;
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_EnqueueDequeue_SmallerThanMin()
+        {
+            PriorityQueue<string, int> queue = SmallPriorityQueueFactory(out HashSet<(string, int)> enqueuedItems);
+
+            string actualElement = queue.EnqueueDequeue("zero", 0);
+
+            Assert.Equal("zero", actualElement);
+            Assert.True(enqueuedItems.SetEquals(queue.UnorderedItems));
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_EnqueueDequeue_LargerThanMin()
+        {
+            PriorityQueue<string, int> queue = SmallPriorityQueueFactory(out HashSet<(string, int)> enqueuedItems);
+
+            string actualElement = queue.EnqueueDequeue("four", 4);
+
+            Assert.Equal("one", actualElement);
+            Assert.Equal("two", queue.Dequeue());
+            Assert.Equal("three", queue.Dequeue());
+            Assert.Equal("four", queue.Dequeue());
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_EnqueueDequeue_EqualToMin()
+        {
+            PriorityQueue<string, int> queue = SmallPriorityQueueFactory(out HashSet<(string, int)> enqueuedItems);
+
+            string actualElement = queue.EnqueueDequeue("one-not-to-enqueue", 1);
+
+            Assert.Equal("one-not-to-enqueue", actualElement);
+            Assert.True(enqueuedItems.SetEquals(queue.UnorderedItems));
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_Constructor_IEnumerable_Null()
+        {
+            (string, int)[] itemsToEnqueue = new(string, int)[] { (null, 0), ("one", 1) } ;
+            PriorityQueue<string, int> queue = new PriorityQueue<string, int>(itemsToEnqueue);
+            Assert.Null(queue.Dequeue());
+            Assert.Equal("one", queue.Dequeue());
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_Enqueue_Null()
+        {
+            PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
+
+            queue.Enqueue(element: null, 1);
+            queue.Enqueue(element: "zero", 0);
+            queue.Enqueue(element: "two", 2);
+
+            Assert.Equal("zero", queue.Dequeue());
+            Assert.Null(queue.Dequeue());
+            Assert.Equal("two", queue.Dequeue());
+        }
+
+        [Fact]
+        public void PriorityQueue_Generic_EnqueueRange_Null()
+        {
+            PriorityQueue<string, int> queue = new PriorityQueue<string, int>();
+
+            queue.EnqueueRange(new string[] { null, null, null }, 0);
+            queue.EnqueueRange(new string[] { "not null" }, 1);
+            queue.EnqueueRange(new string[] { null, null, null }, 0);
+
+            for (int i = 0; i < 6; ++i)
+            {
+                Assert.Null(queue.Dequeue());
+            }
+
+            Assert.Equal("not null", queue.Dequeue());
+        }
+    }
+}
index 412f68f..eb64600 100644 (file)
@@ -95,6 +95,9 @@
     <Compile Include="Generic\List\List.Generic.Tests.Find.cs" />
     <Compile Include="Generic\List\List.Generic.Tests.Remove.cs" />
     <Compile Include="Generic\List\List.Generic.Tests.Sort.cs" />
+    <Compile Include="Generic\PriorityQueue\PriorityQueue.Generic.cs" />
+    <Compile Include="Generic\PriorityQueue\PriorityQueue.Generic.Tests.cs" />
+    <Compile Include="Generic\PriorityQueue\PriorityQueue.Tests.cs" />
     <Compile Include="Generic\Queue\Queue.Generic.cs" />
     <Compile Include="Generic\Queue\Queue.Generic.Tests.cs" />
     <Compile Include="Generic\Queue\Queue.Tests.cs" />