2.3.1.3 — простейший способ разрешить коллизию: не разрешать её вообще2.3.1.3 — the simplest way to resolve a collision: don't resolve it at all
Хеш-функция неизбежно порождает коллизии — два разных ключа, отображённых в одну и ту же позицию. Простейший ответ: не пытаться найти для второго ключа другое место, а просто хранить в каждой позиции ("корзине") не один элемент, а связный список (2.3.1.1) всех ключей, которые в неё попали.A hash function inevitably produces collisions — two different keys mapped to the same position. The simplest answer: don't try to find the second key a different spot, just store in each position (a "bucket") not one element but a linked list (2.3.1.1) of every key that landed there.
Таблица — это массив из cap "корзин", каждая изначально пустой список. Чтобы вставить пару (key, value), вычисляется idx = hash(key) % cap, и пара добавляется в список buckets[idx] (если ключ там уже есть — значение обновляется, а не дублируется). Чтобы найти значение по ключу, вычисляется тот же idx, и внутри buckets[idx] выполняется линейный поиск нужного ключа — короткий, пока корзина не переполнена. Удаление работает так же: вычислить idx, найти ключ в списке корзины, удалить узел — та же операция O(1) из 2.3.1.1, если ключ уже найден.The table is an array of cap "buckets," each initially an empty list. To insert a (key, value) pair, idx = hash(key) % cap is computed, and the pair is added to the list buckets[idx] (if the key is already there, the value is updated, not duplicated). To find a value by key, the same idx is computed, and a linear search for the key runs inside buckets[idx] — short, as long as the bucket isn't overloaded. Deletion works the same way: compute idx, find the key in the bucket's list, remove the node — the same O(1) operation from 2.3.1.1, once the key is located.
Инвариант, который держит эту схему быстрой: средняя длина цепочки в одной корзине равна load factor = n / cap (n элементов, cap корзин). Пока load factor остаётся константой (обычно ≤ 0.75, для чего таблица периодически увеличивается — rehashing), средняя длина цепочки — тоже константа, и поиск/вставка/удаление остаются O(1) в среднем, несмотря на то что внутри каждой корзины поиск линеен.The invariant keeping this scheme fast: the average chain length in one bucket equals the load factor = n / cap (n elements, cap buckets). As long as the load factor stays a constant (usually ≤ 0.75, which is why the table periodically grows via rehashing), the average chain length is also constant, and lookup/insert/delete stay O(1) on average, even though the search inside each bucket is linear.
коллизия не устраняется, а просто помещается рядом — цена платится только внутри конкретной перегруженной корзиныthe collision isn't eliminated, just placed alongside — the cost is paid only inside that specific overloaded bucket
template<typename K, typename V>
class HashTable {
std::vector<std::list<std::pair<K, V>>> buckets;
public:
explicit HashTable(size_t cap) : buckets(cap) {}
void put(const K& key, const V& val) {
auto& bucket = buckets[std::hash<K>{}(key) % buckets.size()];
for (auto& [k, v] : bucket)
if (k == key) { v = val; return; } // обновить, не дублировать// update, don't duplicate
bucket.emplace_back(key, val);
}
std::optional<V> get(const K& key) const {
const auto& bucket = buckets[std::hash<K>{}(key) % buckets.size()];
for (const auto& [k, v] : bucket)
if (k == key) return v;
return std::nullopt;
}
};
Ключи — целые числа, хеш-функция — просто key % cap, специально выбрана маленькая, чтобы коллизии возникали часто и были наглядны. Каждая корзина — цепочка узлов, растущая вправо. Кнопки put/get/delete выполняют операцию сразу.Keys are integers, the hash function is simply key % cap, deliberately kept small so collisions happen often and are easy to see. Each bucket is a chain of nodes growing to the right. The put/get/delete buttons execute immediately.
выберите операцию, чтобы выполнить её сразуpick an operation to execute it immediately
Если хеш-функция плохая (или злоумышленник целенаправленно подбирает ключи, дающие одинаковый хеш — атака на хеш-таблицу через coordinated collision), все n элементов могут оказаться в одной корзине, и поиск в ней деградирует до O(n) — таблица превращается в связный список с лишним слоем косвенности. Это не гипотетический риск: несколько веб-фреймворков в 2011-2012 годах пострадали от практических DoS-атак именно через предсказуемые хеш-функции для параметров форм. Защита — использовать криптографически устойчивые к предсказанию хеш-функции с случайным seed (SipHash и подобные), делающие целенаправленный подбор коллизий вычислительно неосуществимым.If the hash function is poor (or an attacker deliberately picks keys yielding the same hash — a hash-table collision attack), all n elements can land in one bucket, and lookup there degrades to O(n) — the table becomes a linked list with an extra layer of indirection. This isn't a hypothetical risk: several web frameworks suffered practical DoS attacks in 2011-2012 through exactly this — predictable hash functions for form parameters. The defense is using hash functions resistant to prediction, with a random seed (SipHash and similar), making deliberate collision crafting computationally infeasible.
Java's HashMap исторически (и до сих пор в основе) использует цепочки — с оптимизацией: при большом количестве коллизий в одной корзине (начиная с Java 8) список превращается в красно-чёрное дерево (2.3.2), гарантируя O(log n) вместо O(n) в худшем случае внутри одной корзины.Java's HashMap historically (and still at its core) uses chaining — with an optimization: when a bucket has many collisions (since Java 8), the list converts into a red-black tree (2.3.2), guaranteeing O(log n) instead of O(n) worst case within one bucket.
Простота реализации (не нужно думать про переполнение таблицы или сложные схемы вытеснения) делает цепочки хорошей отправной точкой для понимания того, зачем вообще нужны более сложные подходы вроде открытой адресации (2.3.1.4) и Robin Hood hashing (2.3.1.5).Implementation simplicity (no need to worry about table overflow or complex eviction schemes) makes chaining a good starting point for understanding why more elaborate approaches like open addressing (2.3.1.4) and Robin Hood hashing (2.3.1.5) are needed at all.