2.3.1.6 — назван в честь птицы, выталкивающей чужие яйца из гнезда2.3.1.6 — named after the bird that pushes other eggs out of the nest
Все предыдущие схемы (2.3.1.3-2.3.1.5) ищут свободное место для нового ключа. Cuckoo hashing поступает иначе: у каждого ключа есть ровно две возможные позиции (по двум разным хеш-функциям), и если обе заняты, новый ключ вытесняет ("выталкивает из гнезда") текущего обитателя одной из них, а тот в свою очередь переселяется на свою вторую возможную позицию, вытесняя следующего — цепочка продолжается, пока кто-то не найдёт свободное место.Every earlier scheme (2.3.1.3-2.3.1.5) searches for a free spot for a new key. Cuckoo hashing does the opposite: every key has exactly two possible positions (via two different hash functions), and if both are taken, a new key evicts ("kicks out of the nest") the current occupant of one of them, who in turn relocates to their own second possible position, evicting the next one — a chain continues until someone finds a free spot.
Используются две таблицы (или одна таблица с двумя хеш-функциями) — T1 и T2, с хеш-функциями h1 и h2. Каждый ключ k может находиться либо в T1[h1(k)], либо в T2[h2(k)] — никогда больше нигде. Это и есть инвариант, который делает поиск гарантированно O(1) в худшем случае: проверить нужно ровно два места, не последовательность произвольной длины, как в открытой адресации.Two tables are used (or one table with two hash functions) — T1 and T2, with hash functions h1 and h2. Every key k can live either in T1[h1(k)] or T2[h2(k)] — never anywhere else. This is precisely the invariant that makes lookup guaranteed O(1) worst case: exactly two places need checking, not a sequence of arbitrary length as in open addressing.
Вставка ключа k: если T1[h1(k)] свободен — готово. Если занят другим ключом k' — k занимает эту позицию, а k' вытесняется и пытается занять T2[h2(k')]; если и там занято другим ключом k'' — тот вытесняется в T1[h1(k'')], и так далее. Если цепочка вытеснений превышает разумный лимит (обычно O(log n)), это означает, что текущий набор хеш-функций создал цикл — таблица целиком перехешируется с новыми хеш-функциями (полный rehash, амортизированно редкий при достаточном запасе по load factor).Inserting key k: if T1[h1(k)] is free, done. If occupied by another key k', k takes the spot, and k' is evicted and tries to occupy T2[h2(k')]; if that's also occupied by k'', it's evicted to T1[h1(k'')], and so on. If the eviction chain exceeds a reasonable limit (usually O(log n)), it means the current hash functions created a cycle — the whole table is rehashed with new hash functions (a full rehash, amortized rare given enough load-factor headroom).
каждый вытесненный ключ идёт ровно в свою "запасную" позицию — цепочка предсказуема и конечна, если не образует циклevery evicted key goes exactly to its "backup" position — the chain is predictable and finite, unless it forms a cycle
bool put(int key, int val) {
for (int kicks = 0; kicks < MAX_KICKS; ++kicks) {
size_t i1 = h1(key) % cap;
if (!t1[i1].occupied) { t1[i1] = {key, val, true}; return true; }
std::swap(key, t1[i1].key); std::swap(val, t1[i1].val); // выселить в T1// evict into T1
size_t i2 = h2(key) % cap;
if (!t2[i2].occupied) { t2[i2] = {key, val, true}; return true; }
std::swap(key, t2[i2].key); std::swap(val, t2[i2].val); // выселить в T2// evict into T2
}
rehash_with_new_hash_functions(); // обнаружен цикл — перехешировать всё// cycle detected — rehash everything
return put(key, val);
}
Две таблицы показаны рядом. Красная вспышка — момент вытеснения. Наблюдайте, как одна вставка может запустить целую цепочку перемещений между таблицами.The two tables are shown side by side. A red flash marks an eviction moment. Watch how a single insertion can trigger a whole chain of moves between the tables.
введите ключ и нажмите «put»enter a key and press "put"
Поиск асимметричен по отношению к вставке: инвариант "ключ находится в одной из ровно двух позиций" держится всегда, независимо от истории вставок — поэтому поиск всегда проверяет ровно 2 места, O(1) без всяких "в среднем" или "с высокой вероятностью". Вставка же может потребовать сколь угодно длинной цепочки вытеснений — в редких случаях (обычно при load factor, близком к пределу для двух хеш-функций, теоретически ~50%) цепочка может зациклиться, требуя полного rehash со свежими хеш-функциями. Амортизированная стоимость вставки остаётся O(1), потому что rehash случается редко (экспоненциально редко при растущем n относительно порога), но конкретная отдельная вставка иногда может быть дорогой — тот же тип компромисса, что у амортизированного анализа расширения массива (2.1.1).Lookup is asymmetric relative to insertion: the invariant "the key sits in one of exactly two positions" always holds, regardless of insertion history — so lookup always checks exactly 2 spots, O(1) with no "on average" or "with high probability" caveats. Insertion, though, can require an arbitrarily long eviction chain — in rare cases (usually at a load factor close to the limit for two hash functions, theoretically ~50%) the chain can cycle, requiring a full rehash with fresh hash functions. Insertion's amortized cost stays O(1), because a rehash is rare (exponentially rare as n grows relative to the threshold), but any single specific insertion can occasionally be expensive — the same kind of trade-off as the amortized analysis of array growth (2.1.1).
Гарантированный O(1) поиск (ровно 2 обращения к памяти) критичен для аппаратных коммутаторов и маршрутизаторов, работающих на скорости канала — непредсказуемая длина цепочки пробирования обычной открытой адресации неприемлема, когда каждый пакет должен обрабатываться за фиксированное число тактов.Guaranteed O(1) lookup (exactly 2 memory accesses) is critical for hardware switches and routers running at line rate — the unpredictable probe-chain length of plain open addressing is unacceptable when every packet must be processed in a fixed number of cycles.
Для систем, где чтение должно укладываться в жёсткий бюджет времени (например, key-value хранилища с гарантией p99 latency), детерминированные 2 обращения cuckoo hashing предпочтительнее асимптотически такого же в среднем, но непредсказуемого по худшему случаю поиска в открытой адресации.For systems where reads must fit a hard time budget (e.g. key-value stores with a p99 latency guarantee), cuckoo hashing's deterministic 2 accesses are preferable to open addressing's search, which is asymptotically the same on average but unpredictable in the worst case.