TL; DR: Таблиці хешу гарантують O(1)
очікуваний найгірший час, якщо вибираєте хеш-функцію навмання рівномірно з універсального сімейства хеш-функцій. Очікуваний найгірший випадок не такий, як середній випадок.
Відмова: Я офіційно не O(1)
доводжу хеш-таблиці , тому що перегляньте це відео з курсу [ 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|
дуже великими.
Порівняйте цей результат з попереднім алгоритмом, коли супротивник завжди вигравав виклик. Handwaving тут трохи, але так як більшість раз противник зазнає невдачі, і це вірно для всіх можливих стратегій противник може спробувати, то хоча найгірший випадок O(n)
, то очікується , в гіршому випадку , насправді O(1)
.
Знову ж таки, це не формальне підтвердження. Гарантія, яку ми отримуємо від цього очікуваного найгіршого аналізу, полягає в тому, що наш час запуску не залежить від конкретного вкладу . Це справді випадкова гарантія, на відміну від середнього аналізу випадків, коли ми показали, що мотивований противник може легко створити погані дані.