diff --git a/Recyclable.Collections/RecyclablePriorityQueue.cs b/Recyclable.Collections/RecyclablePriorityQueue.cs new file mode 100644 index 0000000..c4fe439 --- /dev/null +++ b/Recyclable.Collections/RecyclablePriorityQueue.cs @@ -0,0 +1,244 @@ +using System.Collections; +using System.Runtime.CompilerServices; +using System.Numerics; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + internal sealed class RecyclablePriorityQueue : IEnumerable, IDisposable + { + private static readonly bool _needsClearing = !typeof(T).IsValueType; + + private readonly IComparer _comparer; + +#nullable disable + private T[] _heap; +#nullable restore + private int _size; + private bool _disposed; + + public RecyclablePriorityQueue(int initialCapacity = RecyclableDefaults.InitialCapacity, IComparer? comparer = null) + { + if (initialCapacity < 1) + { + initialCapacity = 1; + } + + if (!BitOperations.IsPow2((uint)initialCapacity)) + { + initialCapacity = (int)BitOperations.RoundUpToPowerOf2((uint)initialCapacity); + } + + _heap = initialCapacity >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(initialCapacity) + : new T[initialCapacity]; + _size = 0; + _comparer = comparer ?? Comparer.Default; + } + + public int Count => _size; + public long LongCount => _size; + + public void Enqueue(T item) + { + if (_size == _heap.Length) + { + Grow(); + } + + int index = _size++; + _heap[index] = item; + MoveUp(index); + } + + public T Dequeue() + { + if (_size == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(_size), "Queue is empty"); + } + + var root = _heap[0]; + if (_needsClearing) + { + _heap[0] = default!; + } + + _size--; + if (_size > 0) + { + _heap[0] = _heap[_size]; + if (_needsClearing) + { + _heap[_size] = default!; + } + + MoveDown(0); + } + + return root; + } + + public bool TryDequeue(out T? item) + { + if (_size > 0) + { + item = Dequeue(); + return true; + } + + item = default; + return false; + } + + public T Peek() + { + if (_size == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(_size), "Queue is empty"); + } + + return _heap[0]; + } + + public void Clear() + { + if (_needsClearing) + { + Array.Clear(_heap, 0, _size); + } + + _size = 0; + } + + public Enumerator GetEnumerator() => new(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); + + private void Grow() + { + int newSize; + if (_heap.Length >= RecyclableDefaults.MaxPooledBlockSize) + { + newSize = RecyclableDefaults.MaxPooledBlockSize; + } + else + { + var doubled = _heap.Length << 1; + newSize = (int)Math.Min(doubled, RecyclableDefaults.MaxPooledBlockSize); + } + + var newHeap = newSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(newSize) + : new T[newSize]; + Array.Copy(_heap, newHeap, _size); + + if (_heap.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_heap, _needsClearing); + } + + _heap = newHeap; + } + + private void MoveUp(int index) + { + var item = _heap[index]; + while (index > 0) + { + int parent = (index - 1) >> 1; + if (_comparer.Compare(item, _heap[parent]) >= 0) + { + break; + } + + _heap[index] = _heap[parent]; + index = parent; + } + + _heap[index] = item; + } + + private void MoveDown(int index) + { + var item = _heap[index]; + int half = _size >> 1; + while (index < half) + { + int child = (index << 1) + 1; + int right = child + 1; + if (right < _size && _comparer.Compare(_heap[right], _heap[child]) < 0) + { + child = right; + } + + if (_comparer.Compare(_heap[child], item) >= 0) + { + break; + } + + _heap[index] = _heap[child]; + index = child; + } + + _heap[index] = item; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + if (_heap.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(_heap, _needsClearing); + } + + _heap = Array.Empty(); + _size = 0; + _disposed = true; + GC.SuppressFinalize(this); + } + + public struct Enumerator : IEnumerator + { + private readonly RecyclablePriorityQueue _queue; + private int _index; + private T? _current; + + internal Enumerator(RecyclablePriorityQueue queue) + { + _queue = queue; + _index = 0; + _current = default; + } + + public T Current => _current!; + object IEnumerator.Current => _current!; + + public bool MoveNext() + { + if (_index >= _queue._size) + { + return false; + } + + _current = _queue._heap[_index++]; + return true; + } + + public void Reset() + { + _index = 0; + _current = default; + } + + public void Dispose() + { + _current = default; + } + } + } +} diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs new file mode 100644 index 0000000..b9a1df2 --- /dev/null +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -0,0 +1,56 @@ +using FluentAssertions; +using Recyclable.Collections; + +namespace Recyclable.CollectionsTests +{ + public class RecyclablePriorityQueueTests + { + private static readonly int[] _testData = new[] { 4, 1, 3, 2, 5 }; + + [Fact] + public void DequeueShouldReturnItemsInSortedOrder() + { + using var queue = new RecyclablePriorityQueue(); + foreach (var item in _testData) + { + queue.Enqueue(item); + } + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().BeInAscendingOrder(); + } + + [Fact] + public void ShouldBeEmptyWhenNotInitialized() + { + using var queue = new RecyclablePriorityQueue(); + _ = queue.Should().BeEmpty(); + _ = queue.LongCount.Should().Be(0); + } + + [Fact] + public void ShouldBeEmptyAfterClear() + { + using var queue = new RecyclablePriorityQueue(); + foreach (var item in _testData) + { + queue.Enqueue(item); + } + + queue.Clear(); + _ = queue.Should().BeEmpty(); + } + + [Fact] + public void DequeueShouldThrowWhenNoElements() + { + using var queue = new RecyclablePriorityQueue(); + _ = Assert.Throws(() => queue.Dequeue()); + } + } +}