// 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();
}
}
}