TL; DR: хеш-таблицы гарантируют O(1)
ожидаемое время наихудшего случая, если вы выбираете хеш-функцию равномерно случайным образом из универсального семейства хеш-функций. Ожидаемый худший случай - это не то же самое, что средний случай.
Отказ от ответственности: я официально не доказываю, что хеш-таблицы таковыми являются O(1)
, для этого посмотрите это видео с coursera [ 1 ]. Я также не обсуждаю амортизированные аспекты хеш-таблиц. Это ортогонально обсуждению хеширования и коллизий.
Я вижу на удивление много путаницы по этой теме в других ответах и комментариях и постараюсь исправить некоторые из них в этом длинном ответе.
Рассуждения о худшем случае
Существуют разные типы анализа наихудшего случая. Анализ, который до сих пор дается здесь большинством ответов, является не наихудшим, а скорее средним случаем [ 2 ]. Анализ среднего случая, как правило, более практичен. Возможно, у вашего алгоритма есть один плохой вход для худшего случая, но на самом деле он хорошо работает для всех других возможных входов. Суть в том, что ваша среда выполнения зависит от набора данных, на котором вы работаете.
Рассмотрим следующий псевдокод get
метода хеш-таблицы. Здесь я предполагаю, что мы обрабатываем столкновение путем объединения в цепочку, поэтому каждая запись таблицы представляет собой связанный список (key,value)
пар. Мы также предполагаем, что количество сегментов m
фиксировано, но есть O(n)
, где n
- количество элементов во входных данных.
function get(a: Table with m buckets, k: Key being looked up)
bucket <- compute hash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
Как указывали другие ответы, это работает в среднем O(1)
и худшем случае O(n)
. Здесь мы можем сделать небольшой набросок доказательства по вызову. Задача заключается в следующем:
(1) Вы передаете злоумышленнику алгоритм своей хеш-таблицы.
(2) Противник может изучать его и готовиться сколько угодно долго.
(3) Наконец, злоумышленник дает вам размер, который n
вы можете вставить в свою таблицу.
Вопрос в том, насколько быстро ваша хеш-таблица реагирует на входные данные злоумышленника?
На шаге (1) злоумышленник знает вашу хеш-функцию; на этапе (2) злоумышленник может составить список n
элементов из них hash modulo m
, например, путем случайного вычисления хеш-функции группы элементов; а затем в (3) они могут дать вам этот список. Но о чудо, поскольку все n
элементы хешируются в одну корзину, вашему алгоритму потребуется O(n)
время, чтобы пройти по связанному списку в этой корзине. Независимо от того, сколько раз мы пытаемся выполнить вызов, противник всегда побеждает, и в худшем случае именно так плох ваш алгоритм O(n)
.
Почему хеширование O (1)?
Что отбросило нас в предыдущем испытании, так это то, что злоумышленник очень хорошо знал нашу хеш-функцию и мог использовать эти знания для создания наихудшего из возможных входных данных. Что, если бы вместо того, чтобы всегда использовать одну фиксированную хеш-функцию, у нас действительно был бы набор хеш-функций, H
из которых алгоритм мог бы произвольно выбирать во время выполнения? Если вам интересно, H
это называется универсальным семейством хеш-функций [ 3 ]. Хорошо, давайте попробуем добавить к этому немного случайности .
Сначала предположим, что наша хеш-таблица также включает начальное число r
и ей r
присваивается случайное число во время построения. Мы назначаем его один раз, а затем фиксируем для этого экземпляра хеш-таблицы. Теперь вернемся к нашему псевдокоду.
function get(a: Table with m buckets and seed r, k: Key being looked up)
rHash <- H[r]
bucket <- compute rHash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
Если мы попробуем выполнить задачу еще раз: с шага (1) злоумышленник может узнать все хэш-функции, которые у нас есть H
, но теперь конкретная хеш-функция, которую мы используем, зависит от r
. Значение r
является частным для нашей структуры, злоумышленник не может проверить его во время выполнения или предсказать его заранее, поэтому он не может составить список, который всегда плохо для нас. Предположим, что на шаге (2) злоумышленник выбирает одну функцию hash
в H
случайном порядке, затем он составляет список n
конфликтов hash modulo m
и отправляет его для шага (3), скрещивая пальцы, которые во время выполнения H[r]
будут такими же, как hash
они выбрали.
Это серьезная ставка для противника, список, который он создал, противоречит hash
, но будет просто случайным вводом для любой другой хеш-функции H
. Если он выиграет эту ставку, наше время выполнения будет наихудшим, O(n)
как и раньше, но если он проиграет, тогда нам просто дают случайный ввод, который занимает среднее O(1)
время. И действительно, в большинстве случаев противник проигрывает, он побеждает только один раз в каждом |H|
испытании, и мы можем сделать |H|
его очень большим.
Сравните этот результат с предыдущим алгоритмом, в котором противник всегда побеждал. Здесь немного размахивают руками, но поскольку в большинстве случаев злоумышленник терпит неудачу, и это верно для всех возможных стратегий, которые он может попробовать, из этого следует, что хотя наихудший случай таков O(n)
, ожидаемый наихудший случай на самом деле таков O(1)
.
Опять же, это не формальное доказательство. Гарантия, которую мы получаем из этого ожидаемого анализа наихудшего случая, заключается в том, что время выполнения теперь не зависит от каких-либо конкретных входных данных . Это действительно случайная гарантия, в отличие от анализа среднего случая, когда мы показали, что мотивированный противник может легко создать неверные данные.