Как бы вы реализовали хеш-таблицу на C++?

Ответ

Я бы реализовал хеш-таблицу с разрешением коллизий методом цепочек (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 сгодится, а если нет — свой напишешь, дело житейское.

В общем, вот такая манда с ушами получается. Работает, проверено. Главное — не забывай про рехеш, а то будет тебе хиросима и нигерсраки с производительностью.