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