Ответ
Я бы реализовал хеш-таблицу с разрешением коллизий методом цепочек (separate chaining). Вот базовая структура и ключевые методы:
template<typename Key, typename Value, typename Hash = std::hash<Key>>
class HashTable {
private:
std::vector<std::list<std::pair<Key, Value>>> buckets;
Hash hasher;
size_t itemCount = 0;
const double maxLoadFactor = 0.75;
size_t getBucketIndex(const Key& key) const {
return hasher(key) % buckets.size();
}
public:
HashTable(size_t initialSize = 16) : buckets(initialSize) {}
void insert(const Key& key, Value value) {
// Проверка необходимости рехеширования
if (load_factor() > maxLoadFactor) {
rehash(buckets.size() * 2);
}
size_t index = getBucketIndex(key);
auto& bucket = buckets[index];
// Проверка, существует ли уже ключ
for (auto& pair : bucket) {
if (pair.first == key) {
pair.second = std::move(value); // Обновление значения
return;
}
}
// Вставка новой пары
bucket.emplace_back(key, std::move(value));
++itemCount;
}
Value* find(const Key& key) {
size_t index = getBucketIndex(key);
for (auto& pair : buckets[index]) {
if (pair.first == key) {
return &pair.second;
}
}
return nullptr;
}
void rehash(size_t newBucketCount) {
std::vector<std::list<std::pair<Key, Value>>> newBuckets(newBucketCount);
for (auto& bucket : buckets) {
for (auto& pair : bucket) {
size_t newIndex = hasher(pair.first) % newBucketCount;
newBuckets[newIndex].push_back(std::move(pair));
}
}
buckets = std::move(newBuckets);
}
double load_factor() const {
return static_cast<double>(itemCount) / buckets.size();
}
};
Объяснение выбора подхода:
- Метод цепочек проще для реализации и надежнее при плохой хеш-функции.
std::listдля бакета — классический выбор, но в высокопроизводительных сценариях можно использоватьstd::vectorили односвязный список.- Рехеширование критически важно для поддержания производительности (среднее время поиска O(1)).
- Шаблоны позволяют использовать таблицу с любыми типами, поддерживающими
std::hashили кастомный хешер.
Ответ 18+ 🔞
Блин, ну смотри, вот тебе хеш-таблица, которую я на коленке собрал, но она работает, ёпта. Основа — метод цепочек, это когда коллизии решаются не дракой на кулаках, а просто все пары с одинаковым хешом в один список пихаются. Проще пареной репы, зато надёжно, даже если хеш-функция кривая, как нога у табуретки.
template<typename Key, typename Value, typename Hash = std::hash<Key>>
class HashTable {
private:
std::vector<std::list<std::pair<Key, Value>>> buckets;
Hash hasher;
size_t itemCount = 0;
const double maxLoadFactor = 0.75;
size_t getBucketIndex(const Key& key) const {
return hasher(key) % buckets.size();
}
public:
HashTable(size_t initialSize = 16) : buckets(initialSize) {}
void insert(const Key& key, Value value) {
// Проверка необходимости рехеширования
if (load_factor() > maxLoadFactor) {
rehash(buckets.size() * 2);
}
size_t index = getBucketIndex(key);
auto& bucket = buckets[index];
// Проверка, существует ли уже ключ
for (auto& pair : bucket) {
if (pair.first == key) {
pair.second = std::move(value); // Обновление значения
return;
}
}
// Вставка новой пары
bucket.emplace_back(key, std::move(value));
++itemCount;
}
Value* find(const Key& key) {
size_t index = getBucketIndex(key);
for (auto& pair : buckets[index]) {
if (pair.first == key) {
return &pair.second;
}
}
return nullptr;
}
void rehash(size_t newBucketCount) {
std::vector<std::list<std::pair<Key, Value>>> newBuckets(newBucketCount);
for (auto& bucket : buckets) {
for (auto& pair : bucket) {
size_t newIndex = hasher(pair.first) % newBucketCount;
newBuckets[newIndex].push_back(std::move(pair));
}
}
buckets = std::move(newBuckets);
}
double load_factor() const {
return static_cast<double>(itemCount) / buckets.size();
}
};
А теперь, блядь, объясняю на пальцах, почему так, а не иначе.
Метод цепочек — это наш выбор, потому что он, во-первых, проще, чем линейное пробивание, где надо по массиву скакать, как угорелому. Во-вторых, если твоя хеш-функция внезапно окажется пиздопроебибна и будет все ключи в одно ведро пихать, то таблица просто деградирует в список, но не сломается. С пробиванием же будет полный пиздец и бесконечный цикл.
std::list для ведра — классика жанра. Можно, конечно, взять std::vector для кэш-локальности, но тогда при вставке в середину придётся всё сдвигать, а это тоже не сахар. Для начала списка хватит за глаза.
Рехеширование — это святое, ебать мои старые костыли! Без этого твоя таблица превратится в помойку, где в каждом списке будет овердохуища элементов, и поиск станет медленнее, чем у бабки в очереди за пенсией. Как только загрузка (load_factor) переваливает за 0.75 — бабах, увеличиваем массив ведер в два раза и перераспределяем всё по-новой. Красота!
Шаблоны — ну тут всё ясно, чтобы можно было пихать внутрь что угодно: и строки, и свои классы, лишь бы хеш для них был. Умолчальный std::hash сгодится, а если нет — свой напишешь, дело житейское.
В общем, вот такая манда с ушами получается. Работает, проверено. Главное — не забывай про рехеш, а то будет тебе хиросима и нигерсраки с производительностью.