// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.DataStructure { using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using WallstopStudios.UnityHelpers.Core.Helper; /// /// A highly optimized, array-backed generic heap implementation supporting both min-heap and max-heap ordering. /// Uses dynamic resizing with geometric growth for efficient amortized insertions. /// Allocation-free enumerator and minimal heap operations ensure optimal performance. /// /// /// openSet = new Heap(Comparer.Create((a, b) => a.F.CompareTo(b.F))); /// openSet.Push(startNode); /// while (!openSet.IsEmpty && openSet.TryPop(out PathNode current)) /// { /// // A* exploration... /// } /// ]]> /// /// The type of elements in the heap. Must be comparable. [Serializable] public sealed class Heap : IReadOnlyList { public struct HeapEnumerator : IEnumerator { private readonly T[] _items; private readonly int _count; private int _index; private T _current; internal HeapEnumerator(T[] items, int count) { _items = items; _count = count; _index = -1; _current = default; } public bool MoveNext() { if (++_index < _count) { _current = _items[_index]; return true; } _current = default; return false; } public T Current => _current; object IEnumerator.Current => Current; public void Reset() { _index = -1; _current = default; } public void Dispose() { } } private const int DefaultCapacity = 16; private const int MinimumGrowth = 4; private T[] _items; private int _count; private readonly IComparer _comparer; /// /// Gets the number of elements in the heap. /// public int Count => _count; /// /// Gets whether the heap is empty. /// public bool IsEmpty => _count == 0; /// /// Gets the current capacity of the underlying array. /// public int Capacity => _items.Length; /// /// Gets the element at the specified index in heap order (not sorted order). /// public T this[int index] { get { if (index < 0 || index >= _count) { throw new IndexOutOfRangeException( $"{index} is outside of bounds [0, {_count})" ); } return _items[index]; } } /// /// Attempts to get the element at the specified index in heap order (not sorted order). /// /// The index to access. /// The element at the index if valid. /// True if the index is valid, false otherwise. public bool TryGet(int index, out T result) { if (index < 0 || index >= _count) { result = default; return false; } result = _items[index]; return true; } public Heap() : this(Comparer.Default) { } /// /// Constructs a heap with the default comparer (min-heap for natural ordering). /// /// Initial capacity of the heap. public Heap(int capacity) : this(Comparer.Default, capacity) { } /// /// Constructs a heap with a custom comparer. /// /// The comparer to use for ordering elements. /// Initial capacity of the heap. public Heap(IComparer comparer, int capacity = DefaultCapacity) { if (capacity <= 0) { throw new ArgumentOutOfRangeException( nameof(capacity), "Capacity must be positive." ); } _items = new T[capacity]; _count = 0; _comparer = comparer ?? Comparer.Default; } /// /// Constructs a heap from an existing collection (heapifies in O(n) time). /// /// The collection to heapify. /// The comparer to use for ordering elements. public Heap(IEnumerable items, IComparer comparer = null) { if (items == null) { throw new ArgumentNullException(nameof(items)); } _comparer = comparer ?? Comparer.Default; _count = 0; switch (items) { case IReadOnlyList readonlyList: { int capacity = Math.Max(DefaultCapacity, readonlyList.Count); _items = new T[capacity]; for (int i = 0; i < readonlyList.Count; i++) { _items[_count++] = readonlyList[i]; } break; } case ICollection collection: { int capacity = Math.Max(DefaultCapacity, collection.Count); _items = new T[capacity]; collection.CopyTo(_items, 0); _count = collection.Count; break; } case IReadOnlyCollection readOnlyCollection: { int capacity = Math.Max(DefaultCapacity, readOnlyCollection.Count); _items = new T[capacity]; foreach (T item in readOnlyCollection) { _items[_count++] = item; } break; } default: { _items = new T[DefaultCapacity]; foreach (T item in items) { if (_count == _items.Length) { int newCapacity = ComputeGrowth(_items.Length); Resize(newCapacity); } _items[_count++] = item; } break; } } // Floyd's heap construction algorithm - O(n) for (int i = (_count - 1) >> 1; i >= 0; i--) { HeapifyDown(i); } } /// /// Creates a min-heap with the default comparer. /// public static Heap CreateMinHeap( IComparer comparer = null, int capacity = DefaultCapacity ) { return new Heap(comparer, capacity); } /// /// Creates a max-heap with a reversed comparer. /// public static Heap CreateMaxHeap( IComparer comparer = null, int capacity = DefaultCapacity ) { return comparer == null ? new Heap(ReverseComparer.Instance, capacity) : new Heap(new ReverseComparer(comparer), capacity); } /// /// Creates a min-heap from an existing collection. /// public static Heap CreateMinHeap(IEnumerable items, IComparer comparer = null) { return new Heap(items, comparer ?? Comparer.Default); } /// /// Creates a max-heap from an existing collection. /// public static Heap CreateMaxHeap(IEnumerable items, IComparer comparer = null) { return comparer == null ? new Heap(items, ReverseComparer.Instance) : new Heap(items, new ReverseComparer(comparer)); } /// /// Adds an element to the heap in O(log n) time. /// public void Add(T item) { if (_count == _items.Length) { int newCapacity = ComputeGrowth(_items.Length); Resize(newCapacity); } _items[_count] = item; HeapifyUp(_count); _count++; } /// /// Attempts to peek at the root element without removing it. /// /// True if the heap is not empty, false otherwise. public bool TryPeek(out T result) { if (_count == 0) { result = default; return false; } result = _items[0]; return true; } /// /// Attempts to remove and return the root element in O(log n) time. /// /// True if the heap was not empty, false otherwise. public bool TryPop(out T result) { if (_count == 0) { result = default; return false; } result = _items[0]; _count--; if (_count > 0) { _items[0] = _items[_count]; HeapifyDown(0); } _items[_count] = default; return true; } /// /// Removes all elements from the heap. /// public void Clear() { Array.Clear(_items, 0, _count); _count = 0; } /// /// Checks if the heap contains a specific element in O(log(n)) time. /// public bool Contains(T item) { for (int i = 0; i < _count; i++) { if (_comparer.Compare(_items[i], item) == 0) { return true; } } return false; } /// /// Copies the heap elements to an array (not in sorted order). /// public void CopyTo(T[] array, int arrayIndex) { if (array == null) { throw new ArgumentNullException(nameof(array)); } if (arrayIndex < 0 || arrayIndex > array.Length) { throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } if (array.Length - arrayIndex < _count) { throw new ArgumentException("Destination array is not large enough."); } Array.Copy(_items, 0, array, arrayIndex, _count); } /// /// Returns an array containing all elements (not in sorted order). /// public T[] ToArray() { T[] result = null; _ = ToArray(ref result); return result; } public int ToArray(ref T[] result) { if (result == null || result.Length < _count) { result = new T[_count]; } Array.Copy(_items, 0, result, 0, _count); return _count; } /// /// Trims excess capacity to match the current count. /// public void TrimExcess() { int threshold = (int)(_items.Length * 0.9); if (_count < threshold) { int newCapacity = Math.Max(DefaultCapacity, _count); Resize(newCapacity); } } /// /// Attempts to update the priority of an element at the specified index in O(log n) time. /// After updating, the heap property is restored. /// /// The index of the element to update. /// The new value for the element. /// True if the index was valid and the update succeeded, false otherwise. public bool TryUpdatePriority(int index, T newValue) { if (index < 0 || index >= _count) { return false; } T oldValue = _items[index]; _items[index] = newValue; int comparison = _comparer.Compare(newValue, oldValue); if (comparison < 0) { // Priority increased (smaller value in min-heap), bubble up HeapifyUp(index); } else if (comparison > 0) { // Priority decreased (larger value in min-heap), bubble down HeapifyDown(index); } // If equal, no need to do anything return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HeapifyUp(int index) { T item = _items[index]; while (index > 0) { int parentIndex = (index - 1) >> 1; T parent = _items[parentIndex]; if (_comparer.Compare(item, parent) >= 0) { break; } _items[index] = parent; index = parentIndex; } _items[index] = item; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HeapifyDown(int index) { T item = _items[index]; int halfCount = _count >> 1; while (index < halfCount) { int leftChild = (index << 1) + 1; int rightChild = leftChild + 1; int smallestChild = leftChild; if ( rightChild < _count && _comparer.Compare(_items[rightChild], _items[leftChild]) < 0 ) { smallestChild = rightChild; } if (_comparer.Compare(item, _items[smallestChild]) <= 0) { break; } _items[index] = _items[smallestChild]; index = smallestChild; } _items[index] = item; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int ComputeGrowth(int currentCapacity) { // Use 1.5x growth strategy (capacity + capacity/2) // Ensures better memory reuse than 2x while maintaining O(1) amortized growth // Minimum growth of MinimumGrowth to avoid tiny increments for small arrays int growth = currentCapacity + (currentCapacity >> 1); int newCapacity = currentCapacity + Math.Max(growth - currentCapacity, MinimumGrowth); // Handle overflow by capping at Array.MaxLength if ((uint)newCapacity > 0X7FFFFFC7) // Array.MaxLength { newCapacity = 0X7FFFFFC7; } return newCapacity; } private void Resize(int newCapacity) { if (newCapacity <= _count) { return; } Array.Resize(ref _items, newCapacity); } public HeapEnumerator GetEnumerator() { return new HeapEnumerator(_items, _count); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } }