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