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).
Знову ж таки, це не формальне підтвердження. Гарантія, яку ми отримуємо від цього очікуваного найгіршого аналізу, полягає в тому, що наш час запуску не залежить від конкретного вкладу . Це справді випадкова гарантія, на відміну від середнього аналізу випадків, коли ми показали, що мотивований противник може легко створити погані дані.