# теория · шаг 4 из 7
Коэффициент заполнения: почему словарь всё-таки не константа
Коротко
Коэффициент заполнения α = n/m - это отношение числа ключей n к числу ячеек m,
мера «тесноты» таблицы. От него напрямую зависит скорость: чем плотнее таблица, тем длиннее
цепочки коллизий и тем дольше поиск. При равномерной хеш-функции среднее число проверок на
поиск - величина порядка 1 + α, поэтому таблицу держат НЕДОЗАПОЛНЕННОЙ (α в районе 0.5–0.75),
а при переполнении увеличивают m и перекладывают всё заново (рехеширование). Именно поэтому
словарь - O(1) в среднем, но не бесплатно и не всегда: за константой стоит условие «функция
равномерна И α под контролем». Нарушь любое - и O(1) превратится в O(n).
Если ты знаешь, что α = n/m, что среднее время поиска растёт с α, и почему таблицу держат недозаполненной с рехешированием, листай до «Теперь сам».
Как думаешь, от чего зависит скорость хеш-таблицы
Мы знаем: доступ по ключу - O(1) в среднем, но коллизии неизбежны и их надо разрешать.
Возникает вопрос: а СКОЛЬКО именно работы добавляют коллизии? Ясно, что это зависит от
тесноты: в почти пустой таблице ключи редко сталкиваются, поиск короткий; в набитой под
завязку - каждая ячейка забита, поиск бредёт по длинной цепочке столкнувшихся ключей. Значит
у скорости есть один главный «регулятор» - насколько плотно набита таблица. Назовём его и
поймём, как он управляет временем: тогда станет ясно, почему словарь надо держать
полупустым и почему иногда он вдруг «тормозит» на одной операции.
На что это похоже
Парковка у торгового центра. Пока занято 30% мест, ты въезжаешь и сразу видишь свободное -
почти не ищешь. Когда занято 95%, ты кружишь по этажам, проверяя ряд за рядом, - поиск
места растягивается. Заполненность парковки - это и есть α, а время поиска места растёт
вместе с ней. Разумный ТЦ не ждёт, пока забьётся под завязку: при 80% открывает новый этаж
(увеличивает m). Хеш-таблица делает ровно это - следит за α и, когда становится тесно,
расширяется, чтобы поиск снова стал коротким.
Механизм: α = n/m и его влияние на время
Коэффициент заполнения - простое отношение:
α = n / m # n - сколько ключей уже в таблице, m - сколько всего ячеек
α = 0.5 - таблица заполнена наполовину; α = 1 - ключей столько же, сколько ячеек;
α > 1 возможно только при цепочках (в открытой адресации α < 1 обязательно - следующая
глава). При РАВНОМЕРНОЙ хеш-функции ключи размазаны по ячейкам ровно, и в среднем на ячейку
приходится α ключей. Отсюда среднее число проверок на поиск - порядка 1 + α: одна на
саму ячейку плюс проход по столкнувшимся в ней. Пока α - небольшая константа (скажем,
0.75), это O(1). Но если дать α расти (ключей всё больше, m не меняется), время
поиска растёт линейно по α, и O(1) теряется.
Поэтому реальные таблицы держат порог. При превышении (например, α > 0.75) делают
рехеширование:
1. завести новый массив вдвое больше (m → 2m, снова простое рядом)
2. пересчитать h(ключ) для КАЖДОГО ключа под новое m и переложить
3. старый массив выбросить
Правило
Коэффициент заполнения α = n/m управляет скоростью хеш-таблицы: при равномерной функции
среднее число проверок на поиск - порядка 1 + α. Пока α держат небольшой константой
(0.5–0.75), поиск - O(1) в среднем; если дать α расти, время растёт линейно. Поэтому
таблицу держат НЕДОЗАПОЛНЕННОЙ, а при превышении порога РЕХЕШИРУЮТ - заводят массив вдвое
больше и перекладывают все ключи. Отсюда честная формулировка: словарь - O(1) в среднем
при условии «равномерная функция И α под контролем».
Разбор: почему рехеширование - это амортизированно O(1)
Рехеширование пугает: переложить ВСЕ n ключей - это O(n), дорогая операция посреди
быстрых. Не рушит ли она O(1)? Нет - и здесь работает та же амортизация, что у растущего
массива из раздела про списки. Удваиваем m при заполнении, значит между двумя
рехешированиями успеваем добавить ≈ m новых ключей (пока α дойдёт от 0.375 снова до
0.75). Дорогая переборка O(m) «размазывается» по этим m дешёвым вставкам - по константе
на каждую. Итог: одна вставка иногда стоит O(n), но СРЕДНЯЯ (амортизированная) по длинной
серии - O(1). Ключевое условие - удвоение (умножение на константу), а не добавление
фиксированного числа ячеек: прибавляй мы по +1000, рехеширования шли бы всё чаще и
амортизация сломалась бы (O(n) на вставку). Это ровно тот же расчёт, что и у динамического
массива, - хеш-таблица наследует его целиком.
Разбор: почему из-за рехеширования словарь иногда «дёргается»
Практическое следствие амортизации: отдельная вставка в словарь МОЖЕТ занять O(n), когда
на ней сработало рехеширование. В среднем всё быстро, но конкретная операция «не повезло»
затягивается - таблица в этот миг перекладывает миллионы ключей. Для большинства программ
это незаметно, но там, где важна ПРЕДСКАЗУЕМАЯ задержка каждой операции (системы реального
времени, игровые кадры, торговые системы), внезапный O(n)-скачок - проблема. Поэтому в
таких местах либо заранее задают размер таблицы с запасом (чтобы рехеширования не случилось),
либо берут структуры с гарантированным худшим случаем (сбалансированные деревья, O(log n)
ВСЕГДА - следующий раздел). Понимать это - значит не удивляться, почему «константный» словарь
иногда даёт всплеск времени, и знать, когда его нельзя ставить в критичный по задержке путь.
Что ломается без этого
Первая ошибка - не следить за α и дать таблице переполниться: цепочки растут, поиск из
O(1) становится O(n), словарь тормозит непонятно почему. Второе - расширять таблицу
добавлением ФИКСИРОВАННОГО числа ячеек (m += 1000), а не удвоением: амортизация ломается,
вставки в среднем становятся O(n). Третье - рехешируя, забыть ПЕРЕСЧИТАТЬ h(ключ) под
новое m: h(k) = k mod m при другом m даёт другой номер, старые индексы недействительны,
и просто скопировать ячейки нельзя - надо переразложить каждый ключ. Четвёртое - обещать
O(1) на КАЖДУЮ операцию там, где важен худший случай: из-за рехеширования отдельная вставка
бывает O(n).
Зачем это в работе
Коэффициент заполнения - то, чем настраивают хеш-таблицу под задачу. Реальные словари
(dict в Python, HashMap в Java) сами держат α ниже порога и рехешируют, но знание
этого механизма даёт рычаги: если знаешь заранее, сколько будет ключей, - задай начальный
размер (dict через нужную ёмкость, HashMap(capacity)), и рехешировок вообще не будет,
код станет заметно быстрее на больших вставках. Понимание амортизации объясняет, почему
dict в среднем константа, но иногда «дёргается», и подсказывает, где словарь не годится
(жёсткие требования к задержке каждой операции). А формула 1 + α объясняет практический
совет «не набивай хеш-таблицу под завязку»: разница между α = 0.5 и α = 0.95 - это
разница между быстрым и медленным словарём при том же числе ключей. Настройка α - дешёвый
способ ускорить код, о котором многие не знают.
Теперь сам
Хеш-таблица с цепочками, m = 1000 ячеек, в неё положили n = 3000 ключей, α = 3.
Сколько в среднем проверок займёт поиск, и что делать, чтобы вернуть поиск к O(1)?
Ответ. α = n/m = 3000/1000 = 3. При равномерной функции ключи размазаны по ячейкам
ровно, значит в среднем на ячейку приходится α = 3 ключа, и поиск - это проход по цепочке
из трёх плюс сама ячейка: порядка 1 + α = 4 проверок. Это уже не «одна проверка», а вчетверо
дольше - таблица переполнена (α = 3 означает втрое больше ключей, чем ячеек). Чтобы вернуть
O(1), надо РЕХЕШИРОВАТЬ: завести массив побольше - примерно вдвое, но ПРОСТОГО размера
(например, m = 4001 - простое рядом с 4000, не степень двойки, как предупреждал урок про
хеш-функцию), пересчитать h(ключ) = ключ mod новое_m для всех 3000 ключей и переложить.
Тогда α = 3000/4001 ≈ 0.75 - у порога, цепочки коротки, поиск
снова около одной-двух проверок. Ключ - удваивать m (амортизированно O(1)), а не добавлять
понемногу.
Главное
Коэффициент заполнения α = n/m - главный регулятор скорости хеш-таблицы: при равномерной
функции среднее число проверок на поиск порядка 1 + α. Пока α - небольшая константа
(0.5–0.75), поиск O(1) в среднем; дашь α расти - время растёт линейно. Поэтому таблицу
держат недозаполненной и при превышении порога РЕХЕШИРУЮТ: заводят массив ВДВОЕ больше
(удвоение, не +const - иначе амортизация ломается) и перекладывают все ключи, пересчитав
h под новое m. Рехеширование - O(n) разово, но амортизированно O(1) (как у растущего
массива), из-за чего отдельная вставка иногда «дёргается» на O(n). Итог: словарь O(1) в
среднем при условии «равномерная функция И α под контролем», но не в худшем случае.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий