// 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.Text;
using UnityEngine;
using WallstopStudios.UnityHelpers.Utils;
///
/// A highly optimized, array-backed trie implementation for fast prefix search and exact word lookup.
/// Preallocates storage based on total characters in the input set and uses integer indices for traversal,
/// minimizing memory allocations and indirections. Provides allocation-free prefix search aside from returned string allocations.
///
///
///
///
public sealed class Trie : IEnumerable
{
private const int Poison = -1;
private readonly char[] _chars;
private readonly int[] _firstChild;
private readonly int[] _nextSibling;
private readonly bool[] _isWord;
private readonly int _maxWordLength;
private int _nodeCount;
///
/// Constructs the Trie from the provided collection of words.
///
/// All possible words to insert into the Trie.
public Trie(IEnumerable words)
{
if (words == null)
{
throw new ArgumentNullException(nameof(words));
}
IReadOnlyList wordList = words as IReadOnlyList;
PooledResource> pooledListResource = default;
bool usedPooledList = false;
try
{
if (wordList == null)
{
pooledListResource = Buffers.List.Get(out List pooledList);
pooledList.AddRange(words);
wordList = pooledList;
usedPooledList = true;
}
int maxWordLength;
if (wordList.Count > 0)
{
maxWordLength = wordList[0].Length;
for (int i = 1; i < wordList.Count; ++i)
{
maxWordLength = Mathf.Max(maxWordLength, wordList[i].Length);
}
}
else
{
maxWordLength = 0;
}
int capacity = 1;
for (int i = 0; i < wordList.Count; ++i)
{
capacity += wordList[i].Length;
}
_chars = new char[capacity];
_firstChild = new int[capacity];
_nextSibling = new int[capacity];
_isWord = new bool[capacity];
Array.Fill(_firstChild, Poison);
Array.Fill(_nextSibling, Poison);
_maxWordLength = maxWordLength;
_nodeCount = 1; // root node index
for (int i = 0; i < wordList.Count; ++i)
{
string word = wordList[i];
Insert(word);
}
}
finally
{
if (usedPooledList)
{
pooledListResource.Dispose();
}
}
}
// Inserts a single word into the Trie
private void Insert(string word)
{
int node = 0;
foreach (char c in word)
{
int prev = Poison;
int child = _firstChild[node];
while (child != Poison && _chars[child] != c)
{
prev = child;
child = _nextSibling[child];
}
if (child == Poison)
{
child = _nodeCount++;
_chars[child] = c;
_firstChild[child] = Poison;
_nextSibling[child] = Poison;
if (prev == Poison)
{
_firstChild[node] = child;
}
else
{
_nextSibling[prev] = child;
}
}
node = child;
}
_isWord[node] = true;
}
///
/// Determines whether the exact word exists in the Trie.
///
public bool Contains(string word)
{
int node = 0;
foreach (char c in word)
{
int child = _firstChild[node];
while (child != Poison && _chars[child] != c)
{
child = _nextSibling[child];
}
if (child == Poison)
{
return false;
}
node = child;
}
return _isWord[node];
}
///
/// Collects up to maxResults words that start with the given prefix.
/// Results are added into the provided list (which is cleared at the start).
/// Returns the number of results added.
///
public List GetWordsWithPrefix(
string prefix,
List results,
int maxResults = int.MaxValue
)
{
results.Clear();
if (maxResults <= 0)
{
return results;
}
int node = 0;
foreach (char c in prefix)
{
int child = _firstChild[node];
while (child != Poison && _chars[child] != c)
{
child = _nextSibling[child];
}
if (child == Poison)
{
return results;
}
node = child;
}
using PooledResource builderResource = Buffers.GetStringBuilder(
Mathf.Max(_maxWordLength, prefix?.Length ?? 0),
out StringBuilder builder
);
builder.Clear();
builder.Append(prefix);
Collect(node, results, maxResults, builder);
return results;
}
// Recursive collection without allocations
private void Collect(int node, List results, int maxResults, StringBuilder builder)
{
if (results.Count >= maxResults)
{
return;
}
if (_isWord[node])
{
results.Add(builder.ToString());
if (results.Count >= maxResults)
{
return;
}
}
for (int child = _firstChild[node]; child != Poison; child = _nextSibling[child])
{
builder.Append(_chars[child]);
Collect(child, results, maxResults, builder);
builder.Length--;
if (results.Count >= maxResults)
{
return;
}
}
}
///
/// Returns a value-based enumerator for efficient iteration without heap allocations.
///
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new EnumeratorObject(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new EnumeratorObject(this);
}
///
/// Value-based enumerator for efficient foreach iteration without heap allocations.
/// Implements IDisposable to return pooled StringBuilders, Stack, and List.
///
public struct Enumerator : IDisposable
{
private readonly Trie _trie;
private readonly PooledResource<
Stack<(int node, PooledResource sbResource, int sbLength)>
> _stackResource;
private readonly Stack<(
int node,
PooledResource sbResource,
int sbLength
)> _stack;
private readonly PooledResource>> _listResource;
private readonly List> _stringBuilderResources;
private string _current;
internal Enumerator(Trie trie)
{
_trie = trie;
_stackResource = Buffers<(int, PooledResource, int)>.Stack.Get(
out Stack<(int, PooledResource, int)> stack
);
_stack = stack;
_listResource = Buffers>.List.Get(
out List> stringBuilderResources
);
_stringBuilderResources = stringBuilderResources;
_current = null;
// Initialize with root node
PooledResource sbResource = Buffers.StringBuilder.Get();
_stringBuilderResources.Add(sbResource);
_stack.Push((0, sbResource, 0));
}
public string Current => _current;
public bool MoveNext()
{
while (
_stack.TryPop(
out (int node, PooledResource sbResource, int sbLength) item
)
)
{
(int node, PooledResource sbResource, int sbLength) = item;
StringBuilder sb = sbResource.resource;
sb.Length = sbLength;
// Check if this node represents a word
if (_trie._isWord[node])
{
_current = sb.ToString();
// Push siblings and children for next iteration
PushChildrenAndContinue(node, sbResource, sbLength);
return true;
}
// Push all children onto the stack
for (
int child = _trie._firstChild[node];
child != Poison;
child = _trie._nextSibling[child]
)
{
PooledResource childResource = Buffers.StringBuilder.Get(
out StringBuilder childSb
);
_stringBuilderResources.Add(childResource);
childSb.Append(sb);
childSb.Append(_trie._chars[child]);
_stack.Push((child, childResource, childSb.Length));
}
}
return false;
}
private void PushChildrenAndContinue(
int node,
PooledResource sbResource,
int sbLength
)
{
StringBuilder sb = sbResource.resource;
// Push all children onto the stack for future iterations
for (
int child = _trie._firstChild[node];
child != Poison;
child = _trie._nextSibling[child]
)
{
PooledResource childResource = Buffers.StringBuilder.Get(
out StringBuilder childSb
);
_stringBuilderResources.Add(childResource);
childSb.Append(sb);
childSb.Append(_trie._chars[child]);
_stack.Push((child, childResource, childSb.Length));
}
}
public void Dispose()
{
// Return all pooled StringBuilders to the pool
foreach (PooledResource resource in _stringBuilderResources)
{
resource.Dispose();
}
// Return the Stack and List to their pools
_stackResource.Dispose();
_listResource.Dispose();
}
}
///
/// Reference-based enumerator for IEnumerable interface compatibility.
///
private sealed class EnumeratorObject : IEnumerator
{
private Enumerator _enumerator;
internal EnumeratorObject(Trie trie)
{
_enumerator = new Enumerator(trie);
}
public string Current => _enumerator.Current;
object IEnumerator.Current => Current;
public bool MoveNext()
{
return _enumerator.MoveNext();
}
public void Reset()
{
throw new NotSupportedException();
}
public void Dispose()
{
_enumerator.Dispose();
}
}
}
///
/// A highly optimized, array-backed generic Trie for mapping string keys to values of type T.
/// Preallocates storage based on total characters in the key set and uses integer indices for traversal,
/// minimizing memory allocations and indirections. Provides allocation-free prefix search (aside from
/// the output list allocations themselves).
///
public sealed class Trie : IEnumerable
{
private const int Poison = -1;
private readonly char[] _chars;
private readonly int[] _firstChild;
private readonly int[] _nextSibling;
private readonly bool[] _hasValue;
private readonly T[] _values;
private int _nodeCount;
///
/// Constructs the Trie from the provided dictionary of keys to values.
///
/// Mapping from unique string keys to values of type T.
public Trie(IReadOnlyDictionary items)
{
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
using PooledResource>> pooledListResource = Buffers<
KeyValuePair
>.List.Get(out List> itemList);
int capacity = 1;
foreach (KeyValuePair entry in items)
{
capacity += entry.Key.Length;
itemList.Add(entry);
}
_chars = new char[capacity];
_firstChild = new int[capacity];
_nextSibling = new int[capacity];
_hasValue = new bool[capacity];
_values = new T[capacity];
Array.Fill(_firstChild, Poison);
Array.Fill(_nextSibling, Poison);
_nodeCount = 1;
for (int i = 0; i < itemList.Count; ++i)
{
KeyValuePair kv = itemList[i];
Insert(kv.Key, kv.Value);
}
}
// Inserts a single key-value pair into the Trie
private void Insert(string key, T value)
{
int node = 0;
foreach (char c in key)
{
int prev = Poison;
int child = _firstChild[node];
while (child != Poison && _chars[child] != c)
{
prev = child;
child = _nextSibling[child];
}
if (child == Poison)
{
child = _nodeCount++;
_chars[child] = c;
_firstChild[child] = Poison;
_nextSibling[child] = Poison;
if (prev == Poison)
{
_firstChild[node] = child;
}
else
{
_nextSibling[prev] = child;
}
}
node = child;
}
_hasValue[node] = true;
_values[node] = value;
}
///
/// Attempts to retrieve the value associated with the exact key.
///
public bool TryGetValue(string key, out T value)
{
int node = 0;
foreach (char c in key)
{
int child = _firstChild[node];
while (child != Poison && _chars[child] != c)
{
child = _nextSibling[child];
}
if (child == Poison)
{
value = default;
return false;
}
node = child;
}
if (_hasValue[node])
{
value = _values[node];
return true;
}
value = default;
return false;
}
///
/// Collects up to maxResults values whose keys start with the given prefix.
/// Results are added into the provided list (which is cleared at the start).
/// Returns the number of results added.
///
public List GetValuesWithPrefix(
string prefix,
List results,
int maxResults = int.MaxValue
)
{
results.Clear();
if (maxResults <= 0)
{
return results;
}
int node = 0;
foreach (char c in prefix)
{
int child = _firstChild[node];
while (child != Poison && _chars[child] != c)
{
child = _nextSibling[child];
}
if (child == Poison)
{
return results;
}
node = child;
}
Collect(node, results, maxResults);
return results;
}
// Recursive collection without extra allocations
private void Collect(int node, List results, int maxResults)
{
if (results.Count >= maxResults)
{
return;
}
if (_hasValue[node])
{
results.Add(_values[node]);
if (results.Count >= maxResults)
{
return;
}
}
for (int child = _firstChild[node]; child != Poison; child = _nextSibling[child])
{
Collect(child, results, maxResults);
if (results.Count >= maxResults)
{
return;
}
}
}
///
/// Returns a value-based enumerator for efficient iteration without heap allocations.
///
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new EnumeratorObject(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return new EnumeratorObject(this);
}
///
/// Value-based enumerator for efficient foreach iteration without heap allocations.
/// Implements IDisposable to return pooled Stack.
///
public struct Enumerator : IDisposable
{
private readonly Trie _trie;
private readonly PooledResource> _stackResource;
private readonly Stack _stack;
private T _current;
internal Enumerator(Trie trie)
{
_trie = trie;
_stackResource = Buffers.Stack.Get(out Stack stack);
_stack = stack;
_current = default;
// Initialize with root node
if (_trie._nodeCount >= 1)
{
_stack.Push(0);
}
}
public T Current => _current;
public bool MoveNext()
{
while (_stack.TryPop(out int node))
{
// Check if this node has a value (including root for empty string keys)
if (_trie._hasValue[node])
{
_current = _trie._values[node];
// Push children for next iteration
PushChildren(node);
return true;
}
// Push all children onto the stack
for (
int child = _trie._firstChild[node];
child != Poison;
child = _trie._nextSibling[child]
)
{
_stack.Push(child);
}
}
return false;
}
private void PushChildren(int node)
{
// Push all children onto the stack for future iterations
for (
int child = _trie._firstChild[node];
child != Poison;
child = _trie._nextSibling[child]
)
{
_stack.Push(child);
}
}
public void Dispose()
{
// Return the Stack to the pool
_stackResource.Dispose();
}
}
///
/// Reference-based enumerator for IEnumerable interface compatibility.
///
private sealed class EnumeratorObject : IEnumerator
{
private Enumerator _enumerator;
internal EnumeratorObject(Trie trie)
{
_enumerator = new Enumerator(trie);
}
public T Current => _enumerator.Current;
object IEnumerator.Current => Current;
public bool MoveNext()
{
return _enumerator.MoveNext();
}
public void Reset()
{
throw new NotSupportedException();
}
public void Dispose()
{
_enumerator.Dispose();
}
}
}
}