// 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 ProtoBuf;
using UnityEngine;
using WallstopStudios.UnityHelpers.Utils;
///
/// A sparse set data structure optimized for fast O(1) add, remove, contains, and dense iteration.
/// Perfect for ECS-style architectures, entity component storage, and scenarios where you need
/// fast membership testing combined with cache-friendly iteration over active elements.
/// Elements must be non-negative integers within a specified universe size.
///
///
///
///
[Serializable]
[ProtoContract(IgnoreListHandling = true)]
public sealed class SparseSet : IReadOnlyList
{
public struct SparseSetEnumerator : IEnumerator
{
private readonly int[] _dense;
private readonly int _count;
private int _index;
private int _current;
internal SparseSetEnumerator(int[] dense, int count)
{
_dense = dense;
_count = count;
_index = -1;
_current = default;
}
public bool MoveNext()
{
if (++_index < _count)
{
_current = _dense[_index];
return true;
}
_current = default;
return false;
}
public int Current => _current;
object IEnumerator.Current => Current;
public void Reset()
{
_index = -1;
_current = default;
}
public void Dispose() { }
}
[SerializeField]
[ProtoMember(1)]
private int[] _sparse;
[SerializeField]
[ProtoMember(2)]
private int[] _dense;
[SerializeField]
[ProtoMember(3)]
private int _count;
///
/// Gets the number of elements in the set.
///
public int Count => _count;
///
/// Gets whether the set is empty.
///
public bool IsEmpty => _count == 0;
///
/// Gets the maximum value that can be stored in this sparse set (universe size).
///
public int Capacity => _sparse.Length;
///
/// Gets the element at the specified index in dense order.
///
public int this[int index]
{
get
{
if (index < 0 || index >= _count)
{
throw new IndexOutOfRangeException(
$"{index} is outside of bounds [0, {_count})"
);
}
return _dense[index];
}
}
private SparseSet()
{
_sparse = Array.Empty();
_dense = Array.Empty();
_count = 0;
}
///
/// Constructs a sparse set with the specified universe size.
///
/// The maximum value that can be stored + 1.
public SparseSet(int universeSize)
{
if (universeSize <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(universeSize),
"Universe size must be positive."
);
}
_sparse = new int[universeSize];
_dense = new int[universeSize];
_count = 0;
}
///
/// Adds an element to the set in O(1) time.
/// If the element is already in the set, does nothing.
///
/// The value to add (must be in range [0, universeSize)).
/// True if added, false if already present.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAdd(int value)
{
if (value < 0 || value >= _sparse.Length)
{
return false;
}
if (Contains(value))
{
return false;
}
_dense[_count] = value;
_sparse[value] = _count;
_count++;
return true;
}
///
/// Removes an element from the set in O(1) time.
///
/// The value to remove.
/// True if removed, false if not present or invalid.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryRemove(int value)
{
if (value < 0 || value >= _sparse.Length || !Contains(value))
{
return false;
}
int indexInDense = _sparse[value];
int lastElement = _dense[_count - 1];
// Swap with last element
_dense[indexInDense] = lastElement;
_sparse[lastElement] = indexInDense;
_count--;
return true;
}
///
/// Checks if an element is in the set in O(1) time.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Contains(int value)
{
if (value < 0 || value >= _sparse.Length)
{
return false;
}
int index = _sparse[value];
return index < _count && _dense[index] == value;
}
///
/// Removes all elements from the set in O(1) time.
///
public void Clear()
{
_count = 0;
}
///
/// Attempts to get the element at the specified index.
///
public bool TryGet(int index, out int value)
{
if (index < 0 || index >= _count)
{
value = default;
return false;
}
value = _dense[index];
return true;
}
///
/// Copies all elements to an array.
///
public void CopyTo(int[] 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(_dense, 0, array, arrayIndex, _count);
}
///
/// Returns an array containing all elements.
///
public int[] ToArray()
{
int[] result = null;
_ = ToArray(ref result);
return result;
}
public int ToArray(ref int[] result)
{
if (result == null || result.Length < _count)
{
result = new int[_count];
}
Array.Copy(_dense, 0, result, 0, _count);
return _count;
}
///
/// Populates the provided list with all elements.
/// Clears the list before adding. Returns the same list for chaining.
///
public List ToList(List results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
results.Clear();
for (int i = 0; i < _count; i++)
{
results.Add(_dense[i]);
}
return results;
}
public SparseSetEnumerator GetEnumerator()
{
return new SparseSetEnumerator(_dense, _count);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
///
/// A generic sparse set that maps elements of type T to internal indices.
/// Provides O(1) operations with support for any element type.
///
[Serializable]
public sealed class SparseSet : IReadOnlyList
{
public struct SparseSetEnumerator : IEnumerator
{
private readonly T[] _elements;
private readonly int[] _dense;
private readonly int _count;
private PooledArray _pooledArray;
private int _index;
private T _current;
private bool _initialized;
internal SparseSetEnumerator(T[] elements, int[] dense, int count)
{
_elements = elements;
_dense = dense;
_count = count;
_index = -1;
_current = default;
_initialized = false;
_pooledArray = default;
// Rent array and populate on first use
if (count > 0)
{
_pooledArray = SystemArrayPool.Get(count, out T[] temp);
for (int i = 0; i < count; i++)
{
temp[i] = elements[dense[i]];
}
_initialized = true;
}
}
public bool MoveNext()
{
if (++_index < _count)
{
_current = _pooledArray.array[_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()
{
if (_initialized)
{
_pooledArray.Dispose();
_initialized = false;
}
}
}
private readonly Dictionary _elementToIndex;
private readonly T[] _elements;
private readonly int[] _sparse;
private readonly int[] _dense;
private int _count;
private int _nextIndex;
private readonly IEqualityComparer _comparer;
///
/// Gets the number of elements in the set.
///
public int Count => _count;
///
/// Gets whether the set is empty.
///
public bool IsEmpty => _count == 0;
///
/// Gets the maximum number of elements that can be stored.
///
public int Capacity => _sparse.Length;
///
/// Gets the element at the specified index in dense order.
///
public T this[int index]
{
get
{
if (index < 0 || index >= _count)
{
throw new IndexOutOfRangeException(
$"{index} is outside of bounds [0, {_count})"
);
}
int elementIndex = _dense[index];
return _elements[elementIndex];
}
}
///
/// Constructs a sparse set with the specified capacity.
///
public SparseSet(int capacity, IEqualityComparer comparer = null)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(capacity),
"Capacity must be positive."
);
}
_comparer = comparer ?? EqualityComparer.Default;
_elementToIndex = new Dictionary(_comparer);
_elements = new T[capacity];
_sparse = new int[capacity];
_dense = new int[capacity];
_count = 0;
_nextIndex = 0;
}
///
/// Adds an element to the set in O(1) amortized time.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAdd(T element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
if (_nextIndex >= _elements.Length)
{
return false; // Capacity reached
}
if (!_elementToIndex.TryAdd(element, _nextIndex + 1))
{
return false;
}
int index = _nextIndex++;
_elements[index] = element;
_dense[_count] = index;
_sparse[index] = _count;
_count++;
return true;
}
///
/// Removes an element from the set in O(1) time.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryRemove(T element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
if (!_elementToIndex.Remove(element, out int index))
{
return false;
}
int indexInDense = _sparse[index];
int lastDenseIndex = _dense[_count - 1];
// Swap with last element
_dense[indexInDense] = lastDenseIndex;
_sparse[lastDenseIndex] = indexInDense;
_elements[index] = default;
_count--;
return true;
}
///
/// Checks if an element is in the set in O(1) time.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Contains(T element)
{
if (element == null)
{
throw new ArgumentNullException(nameof(element));
}
return _elementToIndex.ContainsKey(element);
}
///
/// Removes all elements from the set.
///
public void Clear()
{
_elementToIndex.Clear();
Array.Clear(_elements, 0, _nextIndex);
_count = 0;
_nextIndex = 0;
}
///
/// Attempts to get the element at the specified index.
///
public bool TryGet(int index, out T element)
{
if (index < 0 || index >= _count)
{
element = default;
return false;
}
int elementIndex = _dense[index];
element = _elements[elementIndex];
return true;
}
///
/// Copies all elements to an array.
///
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.");
}
for (int i = 0; i < _count; i++)
{
int elementIndex = _dense[i];
array[arrayIndex + i] = _elements[elementIndex];
}
}
///
/// Returns an array containing all elements.
///
public T[] ToArray()
{
T[] result = new T[_count];
for (int i = 0; i < _count; i++)
{
int elementIndex = _dense[i];
result[i] = _elements[elementIndex];
}
return result;
}
///
/// Populates the provided list with all elements.
/// Clears the list before adding. Returns the same list for chaining.
///
public List ToList(List results)
{
if (results == null)
{
throw new ArgumentNullException(nameof(results));
}
results.Clear();
for (int i = 0; i < _count; i++)
{
int elementIndex = _dense[i];
results.Add(_elements[elementIndex]);
}
return results;
}
public SparseSetEnumerator GetEnumerator()
{
return new SparseSetEnumerator(_elements, _dense, _count);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}