// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Core.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Platform/Platform.h"
#include "HashFunctions.h"
#include "Config.h"
///
/// Template for unordered set of values (without duplicates with O(1) lookup access).
///
/// The type of elements in the set.
template
API_CLASS(InBuild) class HashSet
{
friend HashSet;
public:
///
/// Describes single portion of space for the item in a hash map
///
struct Bucket
{
friend HashSet;
public:
enum State : byte
{
Empty,
Deleted,
Occupied,
};
public:
T Item;
private:
State _state;
public:
Bucket()
: _state(Empty)
{
}
~Bucket()
{
}
public:
void Free()
{
_state = Empty;
}
void Delete()
{
_state = Deleted;
}
void Occupy(const T& item)
{
Item = item;
_state = Occupied;
}
public:
FORCE_INLINE bool IsEmpty() const
{
return _state == Empty;
}
FORCE_INLINE bool IsDeleted() const
{
return _state == Deleted;
}
FORCE_INLINE bool IsOccupied() const
{
return _state == Occupied;
}
FORCE_INLINE bool IsNotOccupied() const
{
return _state != Occupied;
}
};
private:
int32 _elementsCount = 0;
int32 _deletedCount = 0;
int32 _tableSize = 0;
Bucket* _table = nullptr;
public:
///
/// Initializes a new instance of the class.
///
HashSet()
{
}
///
/// Initializes a new instance of the class.
///
/// The initial capacity.
HashSet(int32 capacity)
{
ASSERT(capacity >= 0);
SetCapacity(capacity);
}
///
/// Initializes a new instance of the class.
///
/// Other collection to copy
HashSet(const HashSet& other)
{
Clone(other);
}
///
/// Clones the data from the other collection.
///
/// The other collection to copy.
/// The reference to this.
HashSet& operator=(const HashSet& other)
{
// Ensure we're not trying to set to itself
if (this != &other)
Clone(other);
return *this;
}
///
/// Finalizes an instance of the class.
///
~HashSet()
{
Cleanup();
}
public:
///
/// Gets the amount of the elements in the collection.
///
/// The amount of elements in the collection.
FORCE_INLINE int32 Count() const
{
return _elementsCount;
}
///
/// Gets the amount of the elements that can be contained by the collection.
///
/// The capacity of the collection.
FORCE_INLINE int32 Capacity() const
{
return _tableSize;
}
///
/// Returns true if collection is empty.
///
/// True if is empty, otherwise false.
FORCE_INLINE bool IsEmpty() const
{
return _elementsCount == 0;
}
///
/// Returns true if collection has one or more elements.
///
/// True if isn't empty, otherwise false.
FORCE_INLINE bool HasItems() const
{
return _elementsCount != 0;
}
public:
///
/// The hash set collection iterator.
///
struct Iterator
{
friend HashSet;
private:
HashSet& _collection;
int32 _index;
Iterator(HashSet& collection, const int32 index)
: _collection(collection)
, _index(index)
{
}
Iterator(HashSet const& collection, const int32 index)
: _collection((HashSet&)collection)
, _index(index)
{
}
public:
Iterator(const Iterator& i)
: _collection(i._collection)
, _index(i._index)
{
}
public:
///
/// Checks if iterator is in the end of the collection
///
/// True if is in the end, otherwise false
FORCE_INLINE bool IsEnd() const
{
return _index == _collection.Capacity();
}
///
/// Checks if iterator is not in the end of the collection
///
/// True if is not in the end, otherwise false
FORCE_INLINE bool IsNotEnd() const
{
return _index != _collection.Capacity();
}
public:
FORCE_INLINE Bucket& operator*() const
{
return _collection._table[_index];
}
FORCE_INLINE Bucket* operator->() const
{
return &_collection._table[_index];
}
FORCE_INLINE explicit operator bool() const
{
return _index >= 0 && _index < _collection._tableSize;
}
FORCE_INLINE bool operator !() const
{
return !(bool)*this;
}
FORCE_INLINE bool operator==(const Iterator& v) const
{
return _index == v._index && &_collection == &v._collection;
}
FORCE_INLINE bool operator!=(const Iterator& v) const
{
return _index != v._index || &_collection != &v._collection;
}
public:
Iterator& operator++()
{
const int32 capacity = _collection.Capacity();
if (_index != capacity)
{
do
{
_index++;
} while (_index != capacity && _collection._table[_index].IsNotOccupied());
}
return *this;
}
Iterator operator++(int)
{
Iterator i = *this;
++i;
return i;
}
Iterator& operator--()
{
if (_index > 0)
{
do
{
_index--;
} while (_index > 0 && _collection._table[_index].IsNotOccupied());
}
return *this;
}
Iterator operator--(int)
{
Iterator i = *this;
--i;
return i;
}
};
public:
///
/// Removes all elements from the collection.
///
void Clear()
{
if (_table)
{
// Free all buckets
// Note: this will not clear allocated objects space!
for (int32 i = 0; i < _tableSize; i++)
_table[i].Free();
_elementsCount = _deletedCount = 0;
}
}
///
/// Clear the collection and delete value objects.
///
void ClearDelete()
{
for (auto i = Begin(); i.IsNotEnd(); ++i)
{
if (i->Value)
::Delete(i->Value);
}
Clear();
}
///
/// Changes capacity of the collection
///
/// New capacity
/// Enable/disable preserving collection contents during resizing
void SetCapacity(int32 capacity, bool preserveContents = true)
{
// Validate input
ASSERT(capacity >= 0);
// Check if capacity won't change
if (capacity == Capacity())
return;
// Cache previous state
auto oldTable = _table;
auto oldTableSize = _tableSize;
// Clear elements counters
const auto oldElementsCount = _elementsCount;
_deletedCount = _elementsCount = 0;
// Check if need to create a new table
if (capacity > 0)
{
// Align capacity value
if (Math::IsPowerOfTwo(capacity) == false)
capacity = Math::RoundUpToPowerOf2(capacity);
// Allocate new table
_table = NewArray(capacity);
_tableSize = capacity;
// Check if preserve content
if (oldElementsCount != 0 && preserveContents)
{
// Try to preserve all values in the collection
for (int32 i = 0; i < oldTableSize; i++)
{
if (oldTable[i].IsOccupied())
Add(oldTable[i].Item);
}
}
}
else
{
// Clear data
_table = nullptr;
_tableSize = 0;
}
ASSERT(preserveContents == false || _elementsCount == oldElementsCount);
// Delete old table
if (oldTable)
{
DeleteArray(oldTable, oldTableSize);
}
}
///
/// Increases collection capacity by given extra size (content will be preserved)
///
/// Extra size to enlarge collection
FORCE_INLINE void IncreaseCapacity(int32 extraSize)
{
ASSERT(extraSize >= 0);
SetCapacity(Capacity() + extraSize);
}
///
/// Ensures that collection has given capacity
///
/// Minimum required capacity
void EnsureCapacity(int32 minCapacity)
{
if (Capacity() >= minCapacity)
return;
int32 num = Capacity() == 0 ? DICTIONARY_DEFAULT_CAPACITY : Capacity() * 2;
SetCapacity(Math::Clamp(num, minCapacity, MAX_int32 - 1410));
}
///
/// Cleanup collection data (changes size to 0 without data preserving)
///
FORCE_INLINE void Cleanup()
{
SetCapacity(0, false);
}
public:
///
/// Add element to the collection.
///
/// The element to add to the set.
/// True if element has been added to the collection, otherwise false if the element is already present.
bool Add(const T& item)
{
// Ensure to have enough memory for the next item (in case of new element insertion)
EnsureCapacity(_elementsCount + _deletedCount + 1);
// Find location of the item or place to insert it
FindPositionResult pos;
FindPosition(item, pos);
// Check if object has been already added
if (pos.ObjectIndex != INVALID_INDEX)
return false;
// Insert
ASSERT(pos.FreeSlotIndex != INVALID_INDEX);
auto bucket = &_table[pos.FreeSlotIndex];
bucket->Occupy(item);
_elementsCount++;
return true;
}
///
/// Add element at iterator to the collection
///
/// Iterator with item to add
void Add(const Iterator& i)
{
ASSERT(&i._collection != this && i);
Bucket& bucket = *i;
Add(bucket.Item);
}
///
/// Removes the specified element from the collection.
///
/// The element to remove.
/// True if cannot remove item from the collection because cannot find it, otherwise false.
bool Remove(const T& item)
{
if (IsEmpty())
return true;
FindPositionResult pos;
FindPosition(item, pos);
if (pos.ObjectIndex != INVALID_INDEX)
{
_table[pos.ObjectIndex].Delete();
_elementsCount--;
_deletedCount++;
return true;
}
return false;
}
///
/// Removes an element at specified iterator position.
///
/// The element iterator to remove.
/// True if cannot remove item from the collection because cannot find it, otherwise false.
bool Remove(Iterator& i)
{
ASSERT(&i._collection == this);
if (i)
{
ASSERT(_table[i._index].IsOccupied());
_table[i._index].Delete();
_elementsCount--;
_deletedCount++;
return true;
}
return false;
}
public:
///
/// Find element with given item in the collection
///
/// Item to find
/// Iterator for the found element or End if cannot find it
Iterator Find(const T& item) const
{
if (IsEmpty())
return End();
FindPositionResult pos;
FindPosition(item, pos);
return pos.ObjectIndex != INVALID_INDEX ? Iterator(*this, pos.ObjectIndex) : End();
}
///
/// Determines whether a collection contains the specified element.
///
/// The item to locate.
/// True if value has been found in a collection, otherwise false
bool Contains(const T& item) const
{
if (IsEmpty())
return false;
FindPositionResult pos;
FindPosition(item, pos);
return pos.ObjectIndex != INVALID_INDEX;
}
public:
///
/// Clones other collection into this
///
/// Other collection to clone
void Clone(const HashSet& other)
{
// Clear previous data
Clear();
// Update capacity
SetCapacity(other.Capacity(), false);
// Clone items
for (auto i = other.Begin(); i != other.End(); ++i)
Add(i);
// Check
ASSERT(Count() == other.Count());
ASSERT(Capacity() == other.Capacity());
}
public:
///
/// Gets iterator for beginning of the collection.
///
/// Iterator for beginning of the collection.
Iterator Begin() const
{
Iterator i(*this, INVALID_INDEX);
++i;
return i;
}
///
/// Gets iterator for ending of the collection.
///
/// Iterator for ending of the collection.
Iterator End() const
{
return Iterator(*this, _tableSize);
}
Iterator begin()
{
Iterator i(*this, -1);
++i;
return i;
}
FORCE_INLINE Iterator end()
{
return Iterator(*this, _tableSize);
}
const Iterator begin() const
{
Iterator i(*this, -1);
++i;
return i;
}
FORCE_INLINE const Iterator end() const
{
return Iterator(*this, _tableSize);
}
protected:
struct FindPositionResult
{
int32 ObjectIndex;
int32 FreeSlotIndex;
};
///
/// Returns a pair of positions: 1st where the object is, 2nd where
/// it would go if you wanted to insert it. 1st is INVALID_INDEX
/// if object is not found; 2nd is INVALID_INDEX if it is.
/// Note: because of deletions where-to-insert is not trivial: it's the
/// first deleted bucket we see, as long as we don't find the item later
///
/// The item to find
/// Pair of values: where the object is and where it would go if you wanted to insert it
void FindPosition(const T& item, FindPositionResult& result) const
{
ASSERT(_table);
const int32 tableSizeMinusOne = _tableSize - 1;
int32 bucketIndex = GetHash(item) & tableSizeMinusOne;
int32 insertPos = INVALID_INDEX;
int32 numChecks = 0;
result.FreeSlotIndex = INVALID_INDEX;
while (numChecks < _tableSize)
{
// Empty bucket
if (_table[bucketIndex].IsEmpty())
{
// Found place to insert
result.ObjectIndex = INVALID_INDEX;
result.FreeSlotIndex = insertPos == INVALID_INDEX ? bucketIndex : insertPos;
return;
}
// Deleted bucket
if (_table[bucketIndex].IsDeleted())
{
// Keep searching but mark to insert
if (insertPos == INVALID_INDEX)
insertPos = bucketIndex;
}
// Occupied bucket by target item
else if (_table[bucketIndex].Item == item)
{
// Found item
result.ObjectIndex = bucketIndex;
return;
}
numChecks++;
bucketIndex = (bucketIndex + DICTIONARY_PROB_FUNC(_tableSize, numChecks)) & tableSizeMinusOne;
}
result.ObjectIndex = INVALID_INDEX;
result.FreeSlotIndex = insertPos;
}
};