#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace System.Collections.Generic { // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // ported from: // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections/src/System/Collections/Generic/PriorityQueue.cs internal sealed class PriorityQueueDebugView { private readonly PriorityQueue _queue; private readonly bool _sort; public PriorityQueueDebugView(PriorityQueue queue) { ArgumentNullException.ThrowIfNull(queue); _queue = queue; _sort = true; } public PriorityQueueDebugView(PriorityQueue.UnorderedItemsCollection collection) { _queue = collection?._queue ?? throw new System.ArgumentNullException(nameof(collection)); } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public (TElement Element, TPriority Priority)[] Items { get { List<(TElement Element, TPriority Priority)> list = new(_queue.UnorderedItems); if (_sort) list.Sort((i1, i2) => _queue.Comparer.Compare(i1.Priority, i2.Priority)); return list.ToArray(); } } } [SuppressMessage("ReSharper", "InconsistentNaming")] internal static class SR { internal const string ArgumentOutOfRange_NeedNonNegNum = "Non-negative number required."; internal const string ArgumentOutOfRange_IndexMustBeLessOrEqual = "Index must be less or equal"; internal const string InvalidOperation_EmptyQueue = "The queue is empty."; internal const string InvalidOperation_EnumFailedVersion = "Collection modified while iterating over it."; internal const string Arg_NonZeroLowerBound = "Non-zero lower bound required."; internal const string Arg_RankMultiDimNotSupported = "Multi-dimensional arrays not supported."; internal const string Argument_InvalidArrayType = "Invalid array type."; internal const string Argument_InvalidOffLen = "Invalid offset or length."; } internal static class ArgumentNullException { public static void ThrowIfNull(object o) { if (o == null) throw new System.ArgumentNullException(); // hard to do it differently without C# 10's features } } internal static class ArrayEx { internal const int MaxLength = int.MaxValue; } /// /// Internal helper functions for working with enumerables. /// internal static class EnumerableHelpers { /// Converts an enumerable to an array using the same logic as List{T}. /// The enumerable to convert. /// The number of items stored in the resulting array, 0-indexed. /// /// The resulting array. The length of the array may be greater than , /// which is the actual number of elements in the array. /// internal static T[] ToArray(IEnumerable source, out int length) { if (source is ICollection ic) { int count = ic.Count; if (count != 0) { // Allocate an array of the desired size, then copy the elements into it. Note that this has the same // issue regarding concurrency as other existing collections like List. If the collection size // concurrently changes between the array allocation and the CopyTo, we could end up either getting an // exception from overrunning the array (if the size went up) or we could end up not filling as many // items as 'count' suggests (if the size went down). This is only an issue for concurrent collections // that implement ICollection, which as of .NET 4.6 is just ConcurrentDictionary. var arr = new T[count]; ic.CopyTo(arr, 0); length = count; return arr; } } else { using (var en = source.GetEnumerator()) { if (en.MoveNext()) { const int DefaultCapacity = 4; var arr = new T[DefaultCapacity]; arr[0] = en.Current; int count = 1; while (en.MoveNext()) { if (count == arr.Length) { // This is the same growth logic as in List: // If the array is currently empty, we make it a default size. Otherwise, we attempt to // double the size of the array. Doubling will overflow once the size of the array reaches // 2^30, since doubling to 2^31 is 1 larger than Int32.MaxValue. In that case, we instead // constrain the length to be Array.MaxLength (this overflow check works because of the // cast to uint). int newLength = count << 1; if ((uint)newLength > ArrayEx.MaxLength) newLength = ArrayEx.MaxLength <= count ? count + 1 : ArrayEx.MaxLength; Array.Resize(ref arr, newLength); } arr[count++] = en.Current; } length = count; return arr; } } } length = 0; return Array.Empty(); } } /// /// Represents a min priority queue. /// /// Specifies the type of elements in the queue. /// Specifies the type of priority associated with enqueued elements. /// /// Implements an array-backed quaternary min-heap. Each element is enqueued with an associated priority /// that determines the dequeue order: elements with the lowest priority get dequeued first. /// [DebuggerDisplay("Count = {Count}")] [DebuggerTypeProxy(typeof(PriorityQueueDebugView<,>))] public class PriorityQueue { /// /// Specifies the arity of the d-ary heap, which here is quaternary. /// It is assumed that this value is a power of 2. /// private const int Arity = 4; /// /// The binary logarithm of . /// private const int Log2Arity = 2; /// /// Custom comparer used to order the heap. /// private readonly IComparer? _comparer; /// /// Represents an implicit heap-ordered complete d-ary tree, stored as an array. /// private (TElement Element, TPriority Priority)[] _nodes; /// /// The number of nodes in the heap. /// private int _size; /// /// Lazily-initialized collection used to expose the contents of the queue. /// private UnorderedItemsCollection? _unorderedItems; /// /// Version updated on mutation to help validate enumerators operate on a consistent state. /// private int _version; #if DEBUG static PriorityQueue() { Debug.Assert(Log2Arity > 0 && Math.Pow(2, Log2Arity) == Arity); } #endif /// /// Initializes a new instance of the class. /// public PriorityQueue() { _nodes = Array.Empty<(TElement, TPriority)>(); _comparer = InitializeComparer(null); } /// /// Initializes a new instance of the class /// with the specified initial capacity. /// /// Initial capacity to allocate in the underlying heap array. /// /// The specified was negative. /// public PriorityQueue(int initialCapacity) : this(initialCapacity, null) { } /// /// Initializes a new instance of the class /// with the specified custom priority comparer. /// /// /// Custom comparer dictating the ordering of elements. /// Uses if the argument is . /// public PriorityQueue(IComparer? comparer) { _nodes = Array.Empty<(TElement, TPriority)>(); _comparer = InitializeComparer(comparer); } /// /// Initializes a new instance of the class /// with the specified initial capacity and custom priority comparer. /// /// Initial capacity to allocate in the underlying heap array. /// /// Custom comparer dictating the ordering of elements. /// Uses if the argument is . /// /// /// The specified was negative. /// public PriorityQueue(int initialCapacity, IComparer? comparer) { if (initialCapacity < 0) { throw new ArgumentOutOfRangeException( nameof(initialCapacity), initialCapacity, SR.ArgumentOutOfRange_NeedNonNegNum); } _nodes = new (TElement, TPriority)[initialCapacity]; _comparer = InitializeComparer(comparer); } /// /// Initializes a new instance of the class /// that is populated with the specified elements and priorities. /// /// The pairs of elements and priorities with which to populate the queue. /// /// The specified argument was . /// /// /// Constructs the heap using a heapify operation, /// which is generally faster than enqueuing individual elements sequentially. /// public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> items) : this(items, null) { } /// /// Initializes a new instance of the class /// that is populated with the specified elements and priorities, /// and with the specified custom priority comparer. /// /// The pairs of elements and priorities with which to populate the queue. /// /// Custom comparer dictating the ordering of elements. /// Uses if the argument is . /// /// /// The specified argument was . /// /// /// Constructs the heap using a heapify operation, /// which is generally faster than enqueuing individual elements sequentially. /// public PriorityQueue(IEnumerable<(TElement Element, TPriority Priority)> items, IComparer? comparer) { ArgumentNullException.ThrowIfNull(items); _nodes = EnumerableHelpers.ToArray(items, out _size); _comparer = InitializeComparer(comparer); if (_size > 1) Heapify(); } /// /// Gets the number of elements contained in the . /// public int Count => _size; /// /// Gets the priority comparer used by the . /// public IComparer Comparer => _comparer ?? Comparer.Default; /// /// Gets a collection that enumerates the elements of the queue in an unordered manner. /// /// /// The enumeration does not order items by priority, since that would require N * log(N) time and N space. /// Items are instead enumerated following the internal array heap layout. /// public UnorderedItemsCollection UnorderedItems => _unorderedItems ??= new UnorderedItemsCollection(this); /// /// Adds the specified element with associated priority to the . /// /// The element to add to the . /// The priority with which to associate the new element. public void Enqueue(TElement element, TPriority priority) { // 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. int currentSize = _size++; _version++; if (_nodes.Length == currentSize) Grow(currentSize + 1); if (_comparer == null) MoveUpDefaultComparer((element, priority), currentSize); else MoveUpCustomComparer((element, priority), currentSize); } /// /// Returns the minimal element from the without removing it. /// /// The is empty. /// The minimal element of the . public TElement Peek() { if (_size == 0) throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue); return _nodes[0].Element; } /// /// Removes and returns the minimal element from the . /// /// The queue is empty. /// The minimal element of the . public TElement Dequeue() { if (_size == 0) throw new InvalidOperationException(SR.InvalidOperation_EmptyQueue); var element = _nodes[0].Element; RemoveRootNode(); return element; } /// /// Removes the minimal element from the , /// and copies it to the parameter, /// and its associated priority to the parameter. /// /// The removed element. /// The priority associated with the removed element. /// /// if the element is successfully removed; /// if the is empty. /// public bool TryDequeue([MaybeNullWhen(false)] out TElement element, [MaybeNullWhen(false)] out TPriority priority) { if (_size != 0) { (element, priority) = _nodes[0]; RemoveRootNode(); return true; } element = default; priority = default; return false; } /// /// Returns a value that indicates whether there is a minimal element in the /// , /// and if one is present, copies it to the parameter, /// and its associated priority to the parameter. /// The element is not removed from the . /// /// The minimal element in the queue. /// The priority associated with the minimal element. /// /// if there is a minimal element; /// if the is empty. /// public bool TryPeek([MaybeNullWhen(false)] out TElement element, [MaybeNullWhen(false)] out TPriority priority) { if (_size != 0) { (element, priority) = _nodes[0]; return true; } element = default; priority = default; return false; } /// /// Adds the specified element with associated priority to the , /// and immediately removes the minimal element, returning the result. /// /// The element to add to the . /// The priority with which to associate the new element. /// The minimal element removed after the enqueue operation. /// /// Implements an insert-then-extract heap operation that is generally more efficient /// than sequencing Enqueue and Dequeue operations: in the worst case scenario only one /// shift-down operation is required. /// public TElement EnqueueDequeue(TElement element, TPriority priority) { if (_size != 0) { var root = _nodes[0]; if (_comparer == null) { if (Comparer.Default.Compare(priority, root.Priority) > 0) { MoveDownDefaultComparer((element, priority), 0); _version++; return root.Element; } } else { if (_comparer.Compare(priority, root.Priority) > 0) { MoveDownCustomComparer((element, priority), 0); _version++; return root.Element; } } } return element; } /// /// Enqueues a sequence of element/priority pairs to the . /// /// The pairs of elements and priorities to add to the queue. /// /// The specified argument was . /// public void EnqueueRange(IEnumerable<(TElement Element, TPriority Priority)> items) { ArgumentNullException.ThrowIfNull(items); int count = 0; var collection = items as ICollection<(TElement Element, TPriority Priority)>; if (collection is not null && (count = collection.Count) > _nodes.Length - _size) Grow(_size + count); if (_size == 0) { // build using Heapify() if the queue is empty. if (collection is not null) { collection.CopyTo(_nodes, 0); _size = count; } else { int i = 0; (TElement, TPriority)[] nodes = _nodes; foreach (var (element, priority) in items) { if (nodes.Length == i) { Grow(i + 1); nodes = _nodes; } nodes[i++] = (element, priority); } _size = i; } _version++; if (_size > 1) Heapify(); } else foreach (var (element, priority) in items) Enqueue(element, priority); } /// /// Enqueues a sequence of elements pairs to the , /// all associated with the specified priority. /// /// The elements to add to the queue. /// The priority to associate with the new elements. /// /// The specified argument was . /// public void EnqueueRange(IEnumerable elements, TPriority priority) { ArgumentNullException.ThrowIfNull(elements); int count; if (elements is ICollection<(TElement Element, TPriority Priority)> collection && (count = collection.Count) > _nodes.Length - _size) Grow(_size + count); if (_size == 0) { // build using Heapify() if the queue is empty. int i = 0; (TElement, TPriority)[] nodes = _nodes; foreach (var element in elements) { if (nodes.Length == i) { Grow(i + 1); nodes = _nodes; } nodes[i++] = (element, priority); } _size = i; _version++; if (i > 1) Heapify(); } else foreach (var element in elements) Enqueue(element, priority); } /// /// Removes all items from the . /// 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++; } /// /// Ensures that the can hold up to /// items without further expansion of its backing storage. /// /// The minimum capacity to be used. /// /// The specified is negative. /// /// The current capacity of the . public int EnsureCapacity(int capacity) { if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity), capacity, SR.ArgumentOutOfRange_NeedNonNegNum); if (_nodes.Length < capacity) { Grow(capacity); _version++; } return _nodes.Length; } /// /// Sets the capacity to the actual number of items in the , /// if that is less than 90 percent of current capacity. /// /// /// This method can be used to minimize a collection's memory overhead /// if no new elements will be added to the collection. /// public void TrimExcess() { int threshold = (int)(_nodes.Length * 0.9); if (_size < threshold) { Array.Resize(ref _nodes, _size); _version++; } } /// /// Grows the priority queue to match the specified min capacity. /// private void Grow(int minCapacity) { Debug.Assert(_nodes.Length < minCapacity); const int GrowFactor = 2; const int MinimumGrow = 4; int newcapacity = GrowFactor * _nodes.Length; // Allow the queue 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 > ArrayEx.MaxLength) newcapacity = ArrayEx.MaxLength; // Ensure minimum growth is respected. newcapacity = Math.Max(newcapacity, _nodes.Length + MinimumGrow); // If the computed capacity is still less than specified, set to the original argument. // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. if (newcapacity < minCapacity) newcapacity = minCapacity; Array.Resize(ref _nodes, newcapacity); } /// /// Removes the node from the root of the heap /// private void RemoveRootNode() { int lastNodeIndex = --_size; _version++; if (lastNodeIndex > 0) { var lastNode = _nodes[lastNodeIndex]; if (_comparer == null) MoveDownDefaultComparer(lastNode, 0); else MoveDownCustomComparer(lastNode, 0); } if (RuntimeHelpers.IsReferenceOrContainsReferences<(TElement, TPriority)>()) _nodes[lastNodeIndex] = default; } /// /// Gets the index of an element's parent. /// private static int GetParentIndex(int index) => (index - 1) >> Log2Arity; /// /// Gets the index of the first child of an element. /// private static int GetFirstChildIndex(int index) => (index << Log2Arity) + 1; /// /// Converts an unordered list into a heap. /// 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. var nodes = _nodes; int lastParentWithChildren = GetParentIndex(_size - 1); if (_comparer == null) { for (int index = lastParentWithChildren; index >= 0; --index) MoveDownDefaultComparer(nodes[index], index); } else { for (int index = lastParentWithChildren; index >= 0; --index) MoveDownCustomComparer(nodes[index], index); } } /// /// Moves a node up in the tree to restore heap order. /// private void MoveUpDefaultComparer((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. Debug.Assert(_comparer is null); Debug.Assert(0 <= nodeIndex && nodeIndex < _size); var nodes = _nodes; while (nodeIndex > 0) { int parentIndex = GetParentIndex(nodeIndex); var parent = nodes[parentIndex]; if (Comparer.Default.Compare(node.Priority, parent.Priority) < 0) { nodes[nodeIndex] = parent; nodeIndex = parentIndex; } else break; } nodes[nodeIndex] = node; } /// /// Moves a node up in the tree to restore heap order. /// private void MoveUpCustomComparer((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. Debug.Assert(_comparer is not null); Debug.Assert(0 <= nodeIndex && nodeIndex < _size); var comparer = _comparer; var nodes = _nodes; while (nodeIndex > 0) { int parentIndex = GetParentIndex(nodeIndex); var parent = nodes[parentIndex]; if (comparer != null && comparer.Compare(node.Priority, parent.Priority) < 0) { nodes[nodeIndex] = parent; nodeIndex = parentIndex; } else break; } nodes[nodeIndex] = node; } /// /// Moves a node down in the tree to restore heap order. /// private void MoveDownDefaultComparer((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. Debug.Assert(_comparer is null); Debug.Assert(0 <= nodeIndex && nodeIndex < _size); var nodes = _nodes; int size = _size; int i; while ((i = GetFirstChildIndex(nodeIndex)) < size) { // Find the child node with the minimal priority var minChild = nodes[i]; int minChildIndex = i; int childIndexUpperBound = Math.Min(i + Arity, size); while (++i < childIndexUpperBound) { var nextChild = nodes[i]; if (Comparer.Default.Compare(nextChild.Priority, minChild.Priority) < 0) { minChild = nextChild; minChildIndex = i; } } // Heap property is satisfied; insert node in this location. if (Comparer.Default.Compare(node.Priority, minChild.Priority) <= 0) break; // Move the minimal child up by one node and // continue recursively from its location. nodes[nodeIndex] = minChild; nodeIndex = minChildIndex; } nodes[nodeIndex] = node; } /// /// Moves a node down in the tree to restore heap order. /// private void MoveDownCustomComparer((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. Debug.Assert(_comparer is not null); Debug.Assert(0 <= nodeIndex && nodeIndex < _size); var comparer = _comparer; var nodes = _nodes; int size = _size; int i; while ((i = GetFirstChildIndex(nodeIndex)) < size) { // Find the child node with the minimal priority var minChild = nodes[i]; int minChildIndex = i; int childIndexUpperBound = Math.Min(i + Arity, size); while (++i < childIndexUpperBound) { var nextChild = nodes[i]; if (comparer != null && comparer.Compare(nextChild.Priority, minChild.Priority) < 0) { minChild = nextChild; minChildIndex = i; } } // Heap property is satisfied; insert node in this location. if (comparer != null && comparer.Compare(node.Priority, minChild.Priority) <= 0) break; // Move the minimal child up by one node and continue recursively from its location. nodes[nodeIndex] = minChild; nodeIndex = minChildIndex; } nodes[nodeIndex] = node; } /// /// Initializes the custom comparer to be used internally by the heap. /// private static IComparer? InitializeComparer(IComparer? comparer) { if (typeof(TPriority).IsValueType) { if (comparer == Comparer.Default) // if the user manually specifies the default comparer, // revert to using the optimized path. return null; return comparer; } // Currently the JIT doesn't optimize direct Comparer.Default.Compare // calls for reference types, so we want to cache the comparer instance instead. // TODO https://github.com/dotnet/runtime/issues/10050: Update if this changes in the future. return comparer ?? Comparer.Default; } /// /// Enumerates the contents of a , without any ordering guarantees. /// [DebuggerDisplay("Count = {Count}")] [DebuggerTypeProxy(typeof(PriorityQueueDebugView<,>))] public sealed class UnorderedItemsCollection : IReadOnlyCollection<(TElement Element, TPriority Priority)>, ICollection { internal readonly PriorityQueue _queue; internal UnorderedItemsCollection(PriorityQueue queue) { _queue = queue; } object ICollection.SyncRoot => this; bool ICollection.IsSynchronized => false; void ICollection.CopyTo(Array array, int index) { ArgumentNullException.ThrowIfNull(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_IndexMustBeLessOrEqual); } 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 int Count => _queue._size; IEnumerator<(TElement Element, TPriority Priority)> IEnumerable<(TElement Element, TPriority Priority)>. GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Returns an enumerator that iterates through the . /// /// An for the . public Enumerator GetEnumerator() => new(_queue); /// /// Enumerates the element and priority pairs of a , /// without any ordering guarantees. /// public struct Enumerator : IEnumerator<(TElement Element, TPriority Priority)> { private readonly PriorityQueue _queue; private readonly int _version; private int _index; internal Enumerator(PriorityQueue queue) { _queue = queue; _index = 0; _version = queue._version; Current = default; } /// /// Releases all resources used by the . /// public void Dispose() { } /// /// Advances the enumerator to the next element of the . /// /// /// if the enumerator was successfully advanced to the next element; /// if the enumerator has passed the end of the collection. /// public bool MoveNext() { var localQueue = _queue; if (_version == localQueue._version && (uint)_index < (uint)localQueue._size) { Current = localQueue._nodes[_index]; _index++; return true; } return MoveNextRare(); } private bool MoveNextRare() { if (_version != _queue._version) throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); _index = _queue._size + 1; Current = default; return false; } /// /// Gets the element at the current position of the enumerator. /// public (TElement Element, TPriority Priority) Current { get; private set; } object IEnumerator.Current => Current; void IEnumerator.Reset() { if (_version != _queue._version) throw new InvalidOperationException(SR.InvalidOperation_EnumFailedVersion); _index = 0; Current = default; } } } } }