Приведи пример хеш-функции для целого числа (int) в C++.

Ответ

Для целых чисел хорошая хеш-функция должна быстро вычисляться и обеспечивать равномерное распределение. Вот пример комбинированной функции, подходящей для использования в std::unordered_map или std::unordered_set.

Пример: функция, смешивающая биты (bit-mixing hash)

#include <cstdint>
#include <functional> // для std::hash

// 1. Простая и эффективная функция (подходит для вставки в стандартные контейнеры)
struct IntHash {
    std::size_t operator()(int key) const noexcept {
        // Используем умножение на большое простое число и сдвиг
        std::uint64_t x = static_cast<std::uint64_t>(key);
        x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
        x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
        x = x ^ (x >> 31);
        return static_cast<std::size_t>(x);
    }
};

// 2. Использование с std::unordered_set
#include <unordered_set>
#include <iostream>

int main() {
    std::unordered_set<int, IntHash> unique_numbers;

    for (int i = 0; i < 100; ++i) {
        unique_numbers.insert(i * 12345); // Вставка чисел с плохим распределением
    }

    // Проверяем распределение по корзинам (bucket)
    std::cout << "Bucket count: " << unique_numbers.bucket_count() << 'n';
    std::cout << "Load factor: " << unique_numbers.load_factor() << 'n';

    return 0;
}

Почему эта функция эффективна:

  • Быстрая: Использует только целочисленные операции (XOR, сдвиг, умножение).
  • Хорошее распределение: Многораундовое перемешивание бит минимизирует коллизии даже для последовательных чисел.
  • Детерминированная: Для одного входного значения всегда возвращает одинаковый хеш.

Альтернатива для простых случаев: Если производительность критична, а данные достаточно случайны, можно использовать тривиальную функцию, но это может привести к кластеризации.

// Тривиальная функция (может плохо распределять последовательные числа)
std::size_t trivial_hash(int key) {
    return static_cast<std::size_t>(key);
}

Ответ 18+ 🔞

Давай разберёмся с этой хеш-функцией, ёпта. Ты знаешь, что такое хеш? Это как паспорт для числа, чтобы его быстро найти в бардаке, который зовётся unordered_map. Главное, чтобы этот паспорт выдавался мгновенно и чтобы у всех чисел были разные, иначе начнётся пиздец и коллизии.

Вот смотри, классическая проблема: если взять тривиальную функцию, которая просто возвращает само число, то последовательные значения 1, 2, 3, 4... все попадут в соседние корзины. Это пизда рулю, распределение будет, как говно в проруби — всё в одну кучу. Нагрузка на одну корзину будет овердохуища, и поиск превратится в линейный обход списка. Терпения ебать ноль, пока найдёшь нужный элемент.

Поэтому умные дядьки придумали bit-mixing. Суть в том, чтобы взять исходное число и так перемешать его биты, чтобы на выходе получилась каша, не имеющая ничего общего с исходным порядком. Даже если на входе идут подряд 1, 2, 3, на выходе хеши будут разбросаны по всему диапазону, как ошмётки после хорошего взрыва.

Смотри на этот код, ядрёна вошь:

struct IntHash {
    std::size_t operator()(int key) const noexcept {
        // Превращаем int в uint64_t, чтобы не было знакового расширения, которое может всё испортить
        std::uint64_t x = static_cast<std::uint64_t>(key);
        // Первый раунд: XOR со сдвигом и умножение на магическое простое число
        x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
        // Второй раунд: ещё раз перемешиваем
        x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
        // Финальный аккорд: ещё один XOR для пущей равномерности
        x = x ^ (x >> 31);
        // Возвращаем как size_t
        return static_cast<std::size_t>(x);
    }
};

Почему это работает? Ну, умножение на большое нечётное число — это как взболтать коктейль, биты начинают взаимодействовать друг с другом. Операция XOR со сдвигом (x ^ (x >> N)) — это классический приём, чтобы младшие биты узнали о существовании старших. Всё вместе это даёт тот самый эффект лавины: изменение даже одного бита на входе кардинально меняет весь выходной хеш. Коллизии, конечно, возможны, но их будет дохуя меньше, чем при тривиальном подходе.

А теперь главное, чувак: не выёбывайся без нужды. Если твои данные — это, например, ID пользователей, которые уже являются случайными 64-битными числами, то тебе эта навороченная функция нахуй не сдалась. Можно просто кастовать uint64_t к size_t и всё. Зачем городить огород? Ты только добавишь лишних тактов процессору. Но если ты засовываев в мапу ключи типа (x, y) координат чанков в игре, которые идут подряд, то тут без перемешивания — просто манда с ушами получится, производительность просядет в сраку.

Вот смотри, как это вставляется в unordered_set:

int main() {
    // Указываем нашу кастомную функцию хеширования вторым шаблонным параметром
    std::unordered_set<int, IntHash> unique_numbers;

    for (int i = 0; i < 100; ++i) {
        unique_numbers.insert(i * 12345); // Вставка чисел с плохим распределением
    }

    // Смотрим, не обосрались ли мы с распределением
    std::cout << "Bucket count: " << unique_numbers.bucket_count() << 'n';
    std::cout << "Load factor: " << unique_numbers.load_factor() << 'n';

    return 0;
}

Если load_factor близок к 1, а bucket_count маленький — это плохой знак, контейнер будет постоянно рехешироваться. Наша же функция должна размазать ключи по корзинам равномерно, что и требуется.

Итог, бля: Не изобретай велосипед, если данные уже случайны. Но если есть подозрение ебать, что ключи будут иметь паттерны (последовательные числа, только чётные и т.д.), то бери готовую bit-mixing функцию, как выше. Она проверена, быстра и делает из упорядоченной хуйни — равномерную кашу. И да, код в блоках не трогай, там всё правильно.