2.3.1.4 — разрешить коллизию, оставаясь внутри одного массива2.3.1.4 — resolving a collision while staying inside a single array

Открытая АдресацияOpen Addressing

Цепочки (2.3.1.3) разрешают коллизию, выходя за пределы позиции — заводя отдельный список. Открытая адресация делает противоположное: если позиция занята, ищется другая позиция в том же массиве по заранее определённому правилу (probing sequence) — без указателей, без отдельных аллокаций, все данные физически смежны в одном блоке памяти.Chaining (2.3.1.3) resolves a collision by stepping outside the position — building a separate list. Open addressing does the opposite: if a position is taken, another position within the same array is searched for by a predetermined rule (a probing sequence) — no pointers, no separate allocations, all data physically contiguous in one memory block.

avg O(1) чувствительна к load factorsensitive to load factor кэш-дружелюбнаcache-friendly

Идея и инвариантIdea & invariant

Вставка ключа k пробует позиции probe(k, 0), probe(k, 1), probe(k, 2), ... до первой свободной. Три классические схемы пробирования различаются только формулой probe: linear probing — probe(k, i) = (h(k) + i) mod cap, простой сдвиг на 1 при каждой коллизии; quadratic probing — probe(k, i) = (h(k) + i²) mod cap, шаги растут квадратично, разбивая длинные последовательные "заторы"; double hashing — probe(k, i) = (h₁(k) + i·h₂(k)) mod cap, шаг зависит от второй хеш-функции самого ключа, что делает последовательности проб разными для разных ключей даже при одинаковой начальной позиции.Inserting key k tries positions probe(k, 0), probe(k, 1), probe(k, 2), ... until the first free one. The three classic probing schemes differ only in the probe formula: linear probing — probe(k, i) = (h(k) + i) mod cap, a simple shift by 1 on each collision; quadratic probing — probe(k, i) = (h(k) + i²) mod cap, steps grow quadratically, breaking up long consecutive "clusters"; double hashing — probe(k, i) = (h₁(k) + i·h₂(k)) mod cap, the step depends on the key's own second hash function, making probe sequences different for different keys even from the same starting position.

Удаление вносит осложнение, не существующее в цепочках: слот нельзя просто пометить пустым — если он лежит на пути пробирования для какого-то другого ключа, поиск этого ключа прервётся раньше времени, ошибочно решив "ключа нет" при первой встреченной пустой ячейке. Решение — маркер-tombstone ("удалено, но не пусто"): поиск проходит сквозь него дальше, а вставка может переиспользовать эту позицию.Deletion introduces a complication that doesn't exist with chaining: a slot can't simply be marked empty — if it lies on some other key's probe path, that key's search will stop prematurely, wrongly concluding "not present" at the first empty cell encountered. The fix is a tombstone marker ("deleted, but not empty"): a search keeps going past it, while an insertion can reuse that position.

linear probing, cap=7: put(1), put(8) — h(8)%7 = h(1)%7 = 1 0: ∅ 1: key=1 2: key=8 3: ∅ слот 1 занят → пробуем слот 1+1=2 get(8): вычислить h(8)=1 → занято другим ключом → пробовать 2 → нашли

поиск повторяет ту же последовательность проб, что и вставка — оба используют одну и ту же формулу probe(k, i)search repeats the same probe sequence as insertion — both use the same probe(k, i) formula

enum class Slot { EMPTY, OCCUPIED, DELETED };

template<typename K, typename V>
class OpenAddrTable {
    std::vector<Slot> state;
    std::vector<K> keys;
    std::vector<V> vals;

    size_t probe(const K& k, size_t i) const {
        return (h1(k) + i) % state.size();   // линейное пробирование// linear probing
    }
public:
    bool put(const K& key, const V& val) {
        size_t first_deleted = SIZE_MAX;
        for (size_t i = 0; i < state.size(); ++i) {
            size_t idx = probe(key, i);
            if (state[idx] == Slot::EMPTY) {
                size_t target = (first_deleted != SIZE_MAX) ? first_deleted : idx;
                keys[target] = key; vals[target] = val; state[target] = Slot::OCCUPIED;
                return true;
            }
            if (state[idx] == Slot::DELETED && first_deleted == SIZE_MAX) first_deleted = idx;
            else if (state[idx] == Slot::OCCUPIED && keys[idx] == key) { vals[idx] = val; return true; }
        }
        return false;  // таблица полна// table full
    }
};

Демонстрация вживуюLive demonstration

Переключатель схемы меняет формулу probe. Кнопки put/get/delete запускают всю последовательность проб автоматически — оранжевая рамка показывает текущую проверяемую позицию, серая штриховка — tombstone (удалённый слот, который поиск должен пропускать, а вставка — может переиспользовать).The scheme switch changes the probe formula. The put/get/delete buttons run the whole probe sequence automatically — the orange outline shows the currently checked position, gray hatching is a tombstone (a deleted slot that search must skip past, and insertion may reuse).

выберите операцию, чтобы выполнить её сразуpick an operation to execute it immediately

Почему quadratic и double hashing существуют, если linear уже работаетWhy quadratic and double hashing exist if linear already works

Linear probing страдает от первичной кластеризации: как только образуется последовательный блок занятых слотов, любой новый ключ, хешируемый в него или рядом, удлиняет этот блок ещё больше — блоки растут непропорционально быстро относительно load factor. Quadratic probing смягчает это (растущий шаг i² быстро уводит пробирование прочь от кластера), но подвержено более мягкой версии — вторичной кластеризации: два ключа с одинаковым h(k) всегда порождают идентичную последовательность проб, даже если сами ключи разные. Double hashing устраняет и это: шаг зависит от второй хеш-функции конкретного ключа, поэтому даже при совпадении h₁ последовательности проб для разных ключей расходятся с первого же шага.Linear probing suffers from primary clustering: once a consecutive block of occupied slots forms, any new key hashing into or near it lengthens that block further — blocks grow disproportionately fast relative to the load factor. Quadratic probing eases this (the growing i² step quickly moves probing away from the cluster), but is subject to a milder version — secondary clustering: two keys with the same h(k) always produce an identical probe sequence, even though the keys themselves differ. Double hashing eliminates this too: the step depends on the specific key's second hash function, so even when h₁ matches, different keys' probe sequences diverge from the very first step.

Где встречаетсяWhere it shows up

01

Python's dict — открытая адресация с псевдослучайным пробированием— open addressing with pseudo-random probing

CPython реализует словари через открытую адресацию (не цепочки) именно ради кэш-локальности и компактности памяти — вариант probing в CPython использует часть битов хеша для создания псевдослучайной, но детерминированной последовательности, родственной double hashing.CPython implements dicts via open addressing (not chaining) precisely for cache locality and memory compactness — CPython's probing variant uses part of the hash bits to build a pseudo-random but deterministic sequence, related to double hashing.

02

Компактные хеш-таблицы во встраиваемых системах и GPUCompact hash tables in embedded systems and GPUs

Отсутствие указателей и предсказуемый непрерывный блок памяти делают открытую адресацию предпочтительной там, где важна экономия памяти и предсказуемость доступа — например, в GPU-реализациях хеш-таблиц, где случайные обращения по указателям особенно дороги.The absence of pointers and a predictable contiguous memory block make open addressing preferable where memory economy and access predictability matter — e.g. in GPU hash-table implementations, where random pointer-chasing accesses are especially expensive.