# теория · шаг 3 из 6
Цепочки против открытой адресации: что выбрать
Коротко
Два способа разрешать коллизии живут по-разному. Цепочки: поиск ≈ 1 + α проб, α
может быть любым, удаление простое, но память тратится на указатели и элементы разбросаны по
куче. Открытая адресация: поиск ≈ 1/(1 − α) проб (для неудачного), α < 1 обязателен,
удаление через надгробия, зато всё в одном массиве - отличная локальность памяти. При низком
α (0.5–0.7) оба быстры, и открытая адресация обычно ВЫИГРЫВАЕТ по скорости за счёт кеша.
При высоком α открытая адресация резко деградирует (1/(1−α) взрывается у единицы), а
цепочки лишь плавно замедляются. Отсюда правило: держи α низким и бери открытую адресацию
ради кеша; нужна устойчивость к переполнению или частые удаления - бери цепочки.
Если ты можешь сравнить цепочки (≈1+α) и открытую адресацию (≈1/(1−α)) по числу проб при заданном α и знаешь, когда какой способ выбрать, листай до «Теперь сам».
Как думаешь, какой способ быстрее
Мы разобрали цепочки и открытую адресацию - оба решают коллизии, но устроены
противоположно: одни выносят лишние ключи в списки, другие теснят их в тот же массив. Значит
у них разная цена, и она по-разному зависит от заполнения α. Наивный ответ «открытая
адресация быстрее, потому что без указателей» верен не всегда: при высоком заполнении она
резко проседает. Чтобы выбирать осознанно, надо СРАВНИТЬ их по главной метрике - числу проб
на поиск - при разных α, и увидеть, где проходит граница. Тогда «что выбрать» перестанет
быть догадкой.
На что это похоже
Два способа рассадить гостей на переполненном банкете. Цепочки - поставить к каждому столу
приставные стулья: сколько гостей ни придёт, всех усадим (стульев подтащим), но зал
загромождён, и официант бегает между приставками. Открытая адресация - жёстко N стульев,
занял свой - ищи ближайший свободный: пока зал полупуст, садишься мгновенно и всё компактно;
но когда занято 95%, ты обходишь полстолов в поисках места, а при 100% сесть НЕКУДА. Пока
свободно - тесная рассадка быстрее; под завязку - приставные стулья спасают.
Механизм: две формулы числа проб
Главная метрика - среднее число ячеек, которые надо проверить на поиск, как функция от α.
Цепочки: поиск проходит список ячейки, средняя длина которого α, плюс сама ячейка:
проб ≈ 1 + α # линейно и мягко; α может быть любым (даже > 1)
Открытая адресация (при равномерном пробировании), неудачный поиск:
проб ≈ 1 / (1 − α) # взрывается у α = 1; требует α < 1
Подставим значения и сравним:
α |
цепочки 1 + α |
откр. адресация 1/(1−α) |
|---|---|---|
| 0.5 | 1.5 | 2.0 |
| 0.7 | 1.7 | 3.3 |
| 0.9 | 1.9 | 10.0 |
| 0.95 | 1.95 | 20.0 |
| 2.0 | 3.0 | — (невозможно) |
Видно: до α ≈ 0.7 оба дёшевы и близки, после - открытая адресация улетает вверх, а цепочки
растут еле-еле.
Правило
Цепочки: поиск ≈ 1 + α проб (растёт мягко, α любой, удаление простое, память на
указатели, плохая локальность). Открытая адресация: поиск ≈ 1/(1 − α) проб (взрывается у
α = 1, α < 1 обязателен, удаление через надгробия, отличная локальность памяти). До α ≈
0.7 оба быстры и открытая адресация обычно выигрывает по КЕШУ; выше - она резко деградирует,
а цепочки лишь плавно замедляются. Правило: держи α низким и бери открытую адресацию ради
локальности; нужна устойчивость к переполнению или много удалений - бери цепочки.
Разбор: почему 1/(1−α) взрывается, а 1+α - нет
Разница в формулах отражает разную природу. В цепочках лишние ключи уходят В СПИСОК, вне
массива: сколько бы ни было коллизий, они не мешают ЧУЖИМ ячейкам. Средняя длина списка α
растёт линейно с числом ключей - отсюда мягкое 1 + α. В открытой адресации лишний ключ
занимает ЧУЖУЮ ячейку, удлиняя пробы для всех, кто в неё метил, - коллизии
взаимодействуют, эффект нелинеен. Когда свободных ячеек почти не осталось (α → 1), каждая
проба редко попадает в пустую, и их число 1/(1−α) устремляется в бесконечность: при α =
0.99 это 100 проб, при α = 0.999 - 1000. Знаменатель 1 − α - это доля свободных
ячеек, и деление на неё показывает: чем меньше свободного места, тем дольше его искать.
Поэтому открытую адресацию НЕЛЬЗЯ подпускать к полному заполнению, а цепочки терпят его
спокойно.
Разбор: почему на практике часто выбирают открытую адресацию, хоть формула хуже
Парадокс: формула проб у открытой адресации хуже (1/(1−α) против 1+α), но быстрые
современные библиотеки выбирают именно её. Причина - КОНСТАНТА за одной пробой, которую
асимптотика не видит. Проба в открытой адресации - это чтение СОСЕДНЕЙ ячейки того же
массива, а он лежит в памяти сплошняком: процессор подтягивает его в кеш пачками, и десять
проб подряд - это десять чтений из быстрого кеша. Шаг по цепочке - это прыжок по УКАЗАТЕЛЮ на
узел, разбросанный по куче: почти каждый такой шаг - промах мимо кеша и поход в медленную
основную память, а он в разы дороже. Поэтому при низком α (где проб и там, и там мало)
открытая адресация с её кеш-дружелюбием выигрывает по РЕАЛЬНОМУ времени, хотя «проб» у неё
формально больше. Это тот же урок, что в разделе про массивы: асимптотика считает операции, а
железо ещё и любит локальность - и на практике второе часто решает.
Что ломается без этого
Первая ошибка - выбирать способ по одной асимптотике, игнорируя кеш: «у цепочек 1+α меньше,
значит они быстрее» - неверно на практике при низком α, где локальность открытой адресации
перевешивает. Второе - гнать открытую адресацию к высокому α (0.9+): 1/(1−α) взрывается,
и она становится драматически медленной; там либо рехешь раньше, либо цепочки. Третье -
брать цепочки, когда важны память и скорость, а удалений и переполнения нет: заплатишь
указателями и промахами кеша зря. Четвёртое - забыть, что обе формулы предполагают РАВНОМЕРНУЮ
функцию: на плохой функции обе деградируют к O(n) независимо от α.
Зачем это в работе
Выбор между цепочками и открытой адресацией - реальное инженерное решение, которое авторы
библиотек делают за тебя, но понимать его нужно. Оно объясняет, почему современные dict
(Python, Rust, быстрые C++) - открытая адресация (кеш решает при низком α), а HashMap в
Java - цепочки (с превращением длинных цепочек в деревья ради защиты худшего случая). Для
своего кода вывод практический: если делаешь хеш-таблицу сам или настраиваешь чужую - держи
α низким (≤ 0.7) при открытой адресации, не жди чуда от неё под завязку, а при массовых
удалениях помни про надгробия (или бери цепочки). А главная мысль - что асимптотика проб не
единственный критерий: локальность памяти на реальном железе часто важнее, и именно поэтому
«худшая по формуле» открытая адресация побеждает в бенчмарках. Это зрелый взгляд: считать не
только операции, но и то, как они ложатся на память.
Теперь сам
При α = 0.9 посчитай среднее число проб для цепочек и для открытой адресации. Во сколько раз
разница, и что она говорит о выборе способа при высоком заполнении?
Ответ. Цепочки: 1 + α = 1 + 0.9 = 1.9 пробы. Открытая адресация: 1/(1 − α) = 1/(1 −
0.9) = 1/0.1 = 10 проб. Разница - примерно в 10/1.9 ≈ 5.3 раза, открытая адресация впятеро
медленнее. Это говорит однозначно: при высоком заполнении (α = 0.9) открытую адресацию брать
НЕЛЬЗЯ - её 1/(1−α) уже взлетело (а при α = 0.95 было бы 20, при 0.99 - 100), тогда
как цепочки почти не заметили роста (1.9 против 1.5 при α = 0.5). Практический вывод: если
таблица должна работать плотно заполненной - бери цепочки; если хочешь скорость открытой
адресации (ради кеша) - обязан держать α низким и рехешировать заранее, не подпуская к
единице. Формула 1/(1−α) - это встроенное предупреждение «не заполняй под завязку».
Главное
Цепочки: поиск ≈ 1 + α (мягкий рост, α любой, простое удаление, но память на указатели и
плохая локальность). Открытая адресация: поиск ≈ 1/(1 − α) (взрыв у α = 1, α < 1
обязателен, удаление через надгробия, зато отличная локальность памяти). До α ≈ 0.7 оба
быстры и открытая адресация обычно выигрывает по РЕАЛЬНОМУ времени за счёт кеша (проба -
чтение соседней ячейки, а не прыжок по указателю); выше - она резко деградирует (1/(1−α)
улетает), цепочки лишь плавно замедляются. Правило: держи α низким и бери открытую адресацию
ради локальности; нужна работа под завязку или частые удаления - бери цепочки. Обе формулы
верны лишь при РАВНОМЕРНОЙ функции.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий