1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 2 из 5

Идеальное хеширование: O(1) в худшем случае для статичного набора

Коротко

Универсальное хеширование даёт O(1) в ОЖИДАНИИ. А можно ли O(1) в ХУДШЕМ случае - вообще без коллизий? Да, но при одном условии: набор ключей СТАТИЧЕН (известен заранее и не меняется - словарь ключевых слов языка, таблица маршрутов). Это идеальное хеширование. Строят его в ДВА уровня: верхняя хеш-функция раскидывает n ключей по n корзинам (коллизии есть), а внутри каждой корзины с k ключами заводят маленькую таблицу размером k² со своей функцией, БЕЗ коллизий. Почему k²: по парадоксу дней рождения при m = k² ячейках коллизии k ключей вероятны меньше чем наполовину, так что подходящую функцию находят за пару попыток. Итог - поиск за O(1) гарантированно и всего O(n) памяти суммарно.

Если ты понимаешь, зачем нужен статичный набор, как устроены два уровня идеального хеширования и почему второй уровень берут размером k² (парадокс дней рождения наоборот), листай до «Теперь сам».

Как думаешь, можно ли вообще избавиться от коллизий

Коллизии неизбежны, когда ключей больше, чем ячеек, - это мы доказали (Дирихле). Но что, если ячеек взять БОЛЬШЕ, чем ключей, да ещё знать все ключи ЗАРАНЕЕ? Тогда можно перебирать хеш- функции, пока не найдётся такая, что разложит именно эти ключи вообще без столкновений. Загвоздка в памяти: чтобы гарантированно избежать коллизий среди n ключей, ячеек нужно очень много (порядка n² - следствие парадокса дней рождения), а это неприемлемый расход. Кажется, выбор между «мало памяти, но коллизии» и «нет коллизий, но памяти квадрат». Идеальное хеширование находит третий путь - двухуровневую схему, где квадрат берётся только ВНУТРИ маленьких корзин, и суммарная память остаётся линейной.

На что это похоже

Библиотека с фиксированным, никогда не меняющимся собранием книг. Раз состав известен и постоянен, библиотекарь может ОДИН РАЗ продумать идеальную расстановку: у каждой книги свой единственный адрес, и находишь её мгновенно, без поиска по полке. Такую расстановку нельзя сделать для магазина, куда книги приходят и уходят каждый день (динамический набор) - при каждом изменении всё пришлось бы перекраивать. Но для АРХИВА с неизменным составом идеальный план окупается: строишь его однажды и потом всегда попадаешь в цель с первого раза. Идеальное хеширование - такой план для заранее известного, неизменного набора ключей.

Механизм: два уровня

Уровень 1. Универсальной функцией h раскидываем n ключей по n корзинам (таблица размером m = n). Коллизии будут: в какие-то корзины попадёт по несколько ключей. Пусть в корзину j попало kⱼ ключей.

Уровень 2. Для КАЖДОЙ корзины j заводим отдельную маленькую таблицу размером kⱼ² со своей универсальной функцией hⱼ, подобранной так, чтобы kⱼ ключей корзины легли в неё БЕЗ единой коллизии.

поиск(ключ):
    j = h(ключ)                    # уровень 1: номер корзины, O(1)
    i = h_j(ключ)                  # уровень 2: место внутри корзины, O(1)
    вернуть таблица[j][i]          # ровно один ключ - без перебора
# два подсчёта хеша - и ты на месте. O(1) в ХУДШЕМ случае.

Поиск - два обращения по хешу, без всякого перебора цепочек: O(1) ГАРАНТИРОВАННО. Ключевой факт про память: хотя корзины второго уровня квадратичны (kⱼ²), их СУММА при хорошей функции первого уровня - всего O(n), а не O(n²).

уровень 1 0123 2 ключа 1 ключ уровень 2 (без коллизий) AB размер k²=4 (k=2) C размер 1² = 1 поиск: корзина h(ключ), затем место hⱼ(ключ) - два хеша, ноль перебора Σ kⱼ² = O(n) при хорошей верхней функции → память O(n), поиск O(1) в худшем
Верхний уровень раскидывает ключи по корзинам (коллизии есть), нижний разворачивает каждую корзину в таблицу размера k² без коллизий. Поиск - два хеша, память суммарно O(n).

Правило

Идеальное хеширование даёт поиск O(1) в ХУДШЕМ случае и память O(n) - но только для СТАТИЧНОГО набора ключей (известен заранее, не меняется). Схема двухуровневая: верхняя функция раскидывает n ключей по n корзинам (с коллизиями), а каждая корзина из kⱼ ключей разворачивается во вторичную таблицу размером kⱼ² со своей функцией БЕЗ коллизий. Размер k² берётся потому, что при m = k² ячейках k ключей укладываются без коллизий с вероятностью > 1/2 (парадокс дней рождения наоборот), так что нужную функцию находят за пару попыток. Сумма Σ kⱼ² при хорошей верхней функции - O(n). Динамический набор так не захешируешь - при каждом изменении схему пришлось бы перестраивать.

Разбор: почему второй уровень размером k², а не k

Ключевой выбор - квадрат размера корзины. Возьми во вторичную таблицу m = k ячеек (столько же, сколько ключей) - и по парадоксу дней рождения коллизии почти неизбежны (k ключей на k ячеек - это α = 1, ожидаемых коллизий порядка k). Не годится: нам нужен НОЛЬ коллизий. Возьми m = k² ячеек - вот тут парадокс дней рождения работает НА нас: ожидаемое число коллизий ≈ k²/(2m) = k²/(2k²) = 1/2, то есть в среднем меньше одной. Значит вероятность, что СЛУЧАЙНАЯ функция уложит k ключей в k² ячеек вообще без коллизий, больше 1/2 - и, перебирая функции из семейства, мы находим бескризисную за пару попыток (в среднем меньше двух). Тот же парадокс дней рождения, что раньше пугал ранними коллизиями, здесь работает в нашу пользу: дай ячеек КВАДРАТ от числа ключей - и коллизии становятся редкостью, которую легко обойти выбором функции.

Разбор: почему суммарная память всё-таки O(n), а не O(n²)

Пугает квадрат: если каждая из n корзин имеет размер kⱼ², не выйдет ли суммарно O(n²)? Нет - и здесь важна МАТЕМАТИКА распределения. Сумма Σ kⱼ² велика, только если ключи собрались в немногих больших корзинах (одна корзина с n ключами дала бы n²). Но верхняя функция - универсальная, она раскидывает ключи РОВНО, и корзины выходят маленькими: можно доказать, что при m = n корзинах ожидаемая сумма Σ kⱼ² не превышает 2n - то есть O(n). Интуиция: квадрат наказывает большие корзины, но универсальная функция их не создаёт - все kⱼ малы, а сумма квадратов маленьких чисел мала. Если случайный выбор верхней функции дал слишком большую Σ kⱼ² (скажем, больше 4n - при ожидании до 2n так выходит не чаще чем в половине выборов), её просто ПЕРЕВЫБИРАЮТ: в среднем хватает двух попыток. Так квадрат живёт лишь ВНУТРИ маленьких корзин, а снаружи всё линейно - в этом вся хитрость двухуровневой схемы.

Что ломается без этого

Первая ошибка - применять идеальное хеширование к ДИНАМИЧЕСКОМУ набору (ключи добавляются и удаляются): вся схема построена под конкретный набор, и добавление ключа может создать коллизию во вторичной таблице, требуя перестройки корзины или всей структуры. Идеальное хеширование - только для статики. Второе - брать вторичную таблицу размером k вместо k²: коллизии вернутся, «идеальность» пропадёт. Третье - пугаться квадрата и решить, что память O(n²): при универсальной верхней функции Σ kⱼ² = O(n), надо лишь перевыбрать функцию, если не повезло. Четвёртое - путать идеальное хеширование (гарантия O(1) в худшем, но статика) с универсальным (ожидание O(1), но динамика): это разные инструменты под разные условия.

Зачем это в работе

Идеальное хеширование применяют там, где набор ключей ФИКСИРОВАН и важна гарантия скорости: таблицы зарезервированных слов компилятора (ключевые слова языка известны заранее - if, while, return, …), статические словари, таблицы маршрутизации, наборы допустимых команд. Инструменты вроде gperf генерируют идеальную хеш-функцию для заданного списка строк на этапе компиляции - и распознавание ключевого слова становится O(1) без единой коллизии. Понимать идеальное хеширование - значит знать, что бывает O(1) в ХУДШЕМ случае (не только в среднем), но за него платят условием статичности. А пара «универсальное (ожидание, динамика) + идеальное (гарантия, статика)» показывает общий принцип: чем больше знаешь о данных заранее, тем более сильную гарантию можно выжать. Это перекликается с сортировками (знаешь диапазон - сортируешь за линию) и с выбором алгоритма вообще: гарантии покупаются знанием о входе.

Теперь сам

В корзину идеального хеширования попало k = 4 ключа. Какого размера будет её вторичная таблица и почему такой размер гарантирует, что подходящую функцию (без коллизий) найдут быстро?

Ответ. Вторичная таблица будет размером k² = 4² = 16 ячеек на 4 ключа. Такой размер выбран из парадокса дней рождения: при m = k² = 16 ячейках и k = 4 ключах ожидаемое число коллизий ≈ k²/(2m) = 16/(2·16) = 1/2 - в среднем меньше одной коллизии. Значит СЛУЧАЙНАЯ функция из семейства уложит эти 4 ключа в 16 ячеек вообще без коллизий с вероятностью больше половины. Поэтому, перебирая функции, бескризисную находят в среднем меньше чем за две попытки: не вышло с первой - пробуем вторую, и почти наверняка получаем идеальную раскладку корзины. Взяли бы m = k = 4 (столько ячеек, сколько ключей) - коллизии были бы почти неизбежны (α = 1), и «идеальность» не получилась бы. Квадрат размера - вот что делает второй уровень бескризисным ценой всего 16 ячеек на корзину (а суммарно по всем корзинам - O(n)).

Главное

Идеальное хеширование даёт поиск O(1) в ХУДШЕМ случае и память O(n), но только для СТАТИЧНОГО набора (известен заранее, не меняется). Два уровня: верхняя универсальная функция раскидывает n ключей по n корзинам (коллизии есть), каждая корзина из kⱼ ключей разворачивается во вторичную таблицу размером kⱼ² со своей функцией БЕЗ коллизий. Размер k² - из парадокса дней рождения наоборот: при m = k² коллизии k ключей вероятны меньше чем наполовину, бескризисную функцию находят за пару попыток. Сумма Σ kⱼ² = O(n) при универсальной верхней функции (квадрат наказывает большие корзины, но их нет). Поиск - два хеша, ноль перебора. Для динамики не годится (перестройка при изменении); там - универсальное хеширование (ожидание O(1)). Применяют для ключевых слов языка, статических словарей (gperf).

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.