Чи є хешмап Java справді O (1)?


159

Я бачив кілька цікавих претензій на хешмапи SO re Java та їх O(1)час пошуку. Хтось може пояснити, чому це так? Якщо ці хешмапи не сильно відрізняються від будь-якого алгоритму хешування, на який я був придбаний, завжди повинен існувати набір даних, який містить зіткнення.

У цьому випадку пошук буде O(n)швидше, ніж O(1).

Чи може хтось пояснити, чи є вони O (1), і якщо так, то як вони цього досягають?


1
Я знаю, що це може не відповісти, але я пам’ятаю, що у Вікіпедії є дуже гарна стаття про це. Не пропустіть розділ аналізу ефективності
victor hugo

28
Позначення Big O дає верхню межу для конкретного типу аналізу, який ви робите. Ви все ж повинні уточнити, чи вас цікавить найгірший випадок, середній випадок тощо.
Дан Гомерик,

Відповіді:


127

Особливістю HashMap є те, що на відміну, скажімо, з збалансованих дерев, її поведінка є ймовірною. У цих випадках зазвичай найбільш корисно говорити про складність з точки зору ймовірності настання гіршої події. Що стосується хеш-карти, це, звичайно, є випадком зіткнення щодо того, наскільки повною є карта. Зіткнення оцінити досить просто.

p зіткнення = n / потужність

Тож хеш-карта з навіть скромною кількістю елементів, швидше за все, випробує хоча б одне зіткнення. Велика нотація O дозволяє нам зробити щось більш переконливе. Зауважте, що для будь-якої довільної, фіксованої постійної k.

O (n) = O (k * n)

Ми можемо використовувати цю функцію для покращення продуктивності хеш-карти. Натомість ми могли б подумати про ймовірність максимум 2 зіткнень.

p зіткнення x 2 = (n / потужність) 2

Це набагато нижче. Оскільки вартість обробки одного додаткового зіткнення не має значення для продуктивності Big O, ми знайшли спосіб поліпшити продуктивність, не змінюючи алгоритм! Ми можемо зробити це загальним

p зіткнення xk = (n / потужність) k

І тепер ми можемо знехтувати деякою кількістю довільної кількості зіткнень і закінчитися суттєво крихітною ймовірністю виникнення більшої кількості зіткнень, ніж нам належить. Ви можете отримати ймовірність до довільно крихітного рівня, вибравши правильний k, і все це не змінюючи фактичну реалізацію алгоритму.

Ми говоримо про це, кажучи, що хеш-карта має доступ до O (1) з високою ймовірністю


Навіть із HTML, я все ще не дуже задоволений дробами. Почистіть їх, якщо можете придумати гарний спосіб зробити це.
SingleNegationElimination

4
Насправді, сказане вище говорить про те, що ефекти O (log N) закопуються для неекстремальних значень N фіксованими накладними.
Гарячі лизання

Технічно це число, яке ви дали, є очікуваним значенням кількості зіткнень, яке може дорівнювати ймовірності одного зіткнення.
Саймон Куанг

1
Чи схоже це на амортизований аналіз?
lostsoul29

1
@ OleV.V. хороша продуктивність HashMap завжди залежить від хорошого розподілу вашої хеш-функції. Ви можете торгувати кращою якістю хешу для швидкості хешування, використовуючи функцію криптографічного хешування на вхід.
SingleNegationElimination

38

Ви ніби змішуєте поведінку в гіршому випадку із середнім (очікуваним) часом виконання. Перший дійсно є O (n) для хеш-таблиць загалом (тобто не використовує ідеальне хешування), але це рідко актуально на практиці.

Будь-яка реалізована хеш-таблиця в поєднанні з наполовину пристойним хешем має результати пошуку O (1) з дуже малим коефіцієнтом (насправді 2) в очікуваному випадку, в межах дуже вузької межі відхилення.


6
Я завжди вважав верхню межу найгіршим випадком, але, здається, я помилявся - верхню межу можна мати середній. Отже, схоже, що люди, які претендують на O (1), повинні були дати зрозуміти, що це в середньому. Найгірший випадок - це набір даних, коли існує багато зіткнень, що робить його O (n). Це має сенс зараз.
paxdiablo

2
Вам, мабуть, слід зрозуміти, що коли ви використовуєте велику нотацію O для середнього випадку, ви говорите про верхню межу очікуваної функції виконання, яка є чітко визначеною математичною функцією. Інакше ваша відповідь не має великого сенсу.
ldog

1
gmatt: Я не впевнений, що я розумію ваше заперечення: позначення big-O - це верхня межа функції за визначенням . Що я могла мати на увазі ще?
Конрад Рудольф

3
як правило, в комп'ютерній літературі ви бачите великі позначення O, що представляють верхню межу для алгоритму виконання та складності простору простору. У цьому випадку верхній зв'язок насправді залежить від очікування, яке саме по собі не є функцією, а оператором функцій (Випадкові змінні) і насправді є інтегралом (лебесг.) Сам факт, що ви можете зв'язати таку річ, не слід сприймати як належне і не банально.
ldog

31

У Java HashMap працює, використовуючи hashCode, щоб знайти відро. Кожне відро - це список предметів, що містяться у цьому відрі. Елементи скануються, використовуючи для порівняння рівні. При додаванні елементів HashMap змінюється за розміром після досягнення певного відсотка навантаження.

Так, іноді доведеться порівнювати проти кількох предметів, але загалом це набагато ближче до O (1), ніж до O (n). Для практичних цілей це все, що вам потрібно знати.


11
Отже, оскільки передбачається, що big-O визначає межі, це не має значення, ближче до O (1) чи ні. Навіть O (n / 10 ^ 100) все ще O (n). Я розумію, що коефіцієнт корисної дії приводить потім коефіцієнт корисної дії, але це все ще ставить алгоритм на O (n).
paxdiablo

4
Аналіз хеш-карт зазвичай на середній випадок, який є O (1) (із змовами) У гіршому випадку ви можете мати O (n), але це зазвичай не так. щодо різниці - O (1) означає, що ви отримуєте однаковий час доступу незалежно від кількості елементів на діаграмі, і це зазвичай так (доки є велика частка між розміром таблиці та 'n ')
Ліран Ореві

4
Варто також зазначити, що це все-таки саме O (1), навіть якщо сканування відра займає певний час, оскільки в ньому вже є деякі елементи. Поки відра мають фіксований максимальний розмір, це лише постійний коефіцієнт, не має значення для класифікації O (). Але, звичайно, може бути додано ще більше елементів із "подібними" клавішами, так що ці відра переповнюються, і ви більше не можете гарантувати постійну роботу.
sth

@sth Навіщо колись у відра фіксований максимальний розмір !?
Навін

31

Пам'ятайте, що o (1) не означає, що кожен пошук вивчає лише один елемент - це означає, що середня кількість перевірених елементів залишається незмінною wrt, кількість елементів у контейнері. Тож якщо для пошуку предмета в контейнері зі 100 предметами потрібно в середньому 4 порівняння, то також слід взяти в середньому 4 порівняння, щоб знайти предмет у контейнері з 10000 предметами та будь-яку іншу кількість елементів (завжди є трохи варіації, особливо навколо точок, в яких хеш-таблиця переробляється, і коли є дуже мала кількість елементів).

Таким чином, зіткнення не заважають контейнеру виконувати операції o (1) до тих пір, поки середня кількість ключів на відро залишається в межах фіксованої межі.


16

Я знаю, що це старе питання, але насправді є нова відповідь на нього.

Ви праві, що хеш-карта насправді не є O(1) , строго кажучи, тому що, оскільки кількість елементів стає довільно великою, з часом ви не зможете шукати в постійному часі (і О-нотація визначається в числах, які можуть отримати довільно великі).

Але не випливає, що складність у реальному часі є O(n) тому, що немає правила, яке говорить про те, що відра повинні бути реалізовані як лінійний список.

Насправді, Java 8 реалізує відра, як TreeMapsтільки вони перевищують поріг, що робить фактичний час O(log n).


4

Якщо кількість відра (називаємо це b) утримується постійним (звичайний випадок), то пошук фактично є O (n).
Коли n стає великим, кількість елементів у кожному відрі в середньому становить n / b. Якщо дозвіл на зіткнення виконується одним із звичайних способів (наприклад, пов'язаний список), то пошук - O (n / b) = O (n).

Позначення O - це те, що відбувається, коли n стає більшим і більшим. Це може ввести в оману при застосуванні до певних алгоритмів, а хеш-таблиці - це конкретний приклад. Кількість відра ми вибираємо виходячи з того, скільки елементів ми очікуємо мати справу. Коли n приблизно такого ж розміру, як b, то пошук є приблизно постійним часом, але ми не можемо його назвати O (1), оскільки О визначається через обмеження як n → ∞.



2

Ми встановили, що стандартний опис пошуку хеш-таблиць, що є O (1), стосується очікуваного середнього випадку, а не суворої якості в гіршому випадку. Для хеш-таблиці, що вирішує зіткнення з ланцюжком (як хешмап Java), це технічно O (1 + α) з хорошою хеш-функцією , де α - коефіцієнт навантаження таблиці. Все ще незмінно, поки кількість об'єктів, які ви зберігаєте, не більше ніж постійний коефіцієнт, більший за розмір таблиці.

Також було пояснено, що строго кажучи, можна побудувати вхід, який вимагає O ( n ) пошуку для будь-якої детермінованої хеш-функції. Але також цікаво врахувати найгірший очікуваний час, який відрізняється від середнього часу пошуку. Використовуючи ланцюжок, це O (1 + довжина найдовшої ланцюга), наприклад Θ (log n / log log n ), коли α = 1.

Якщо вас цікавлять теоретичні способи досягнення постійного очікуваного найгіршого пошуку, ви можете прочитати про динамічне ідеальне хешування, яке вирішує зіткнення рекурсивно з іншою хеш-таблицею!


2

Це O (1), тільки якщо ваша хеш-функція дуже хороша. Реалізація таблиці хешів Java не захищає від поганих хеш-функцій.

Чи потрібно вам розробити таблицю, коли ви додаєте елементи чи ні, це не стосується питання, оскільки йдеться про час пошуку.


2

Елементи всередині HashMap зберігаються у вигляді масиву пов'язаного списку (вузла), кожен пов'язаний список у масиві являє собою відро для унікального хеш-значення одного або декількох ключів.
Додаючи запис у HashMap, хеш-код ключа використовується для визначення місця розташування відра в масиві, наприклад:

location = (arraylength - 1) & keyhashcode

Тут & представляє побітовий І оператор.

Наприклад: 100 & "ABC".hashCode() = 64 (location of the bucket for the key "ABC")

Під час операції get використовує той самий спосіб визначення місця розташування відра для ключа. У кращому випадку кожен ключ має унікальний хеш-код і приводить до унікального відра для кожного ключа, у цьому випадку метод get витрачає час лише на визначення місця розташування та отримання значення, яке є постійним O (1).

У найгіршому випадку всі ключі мають однаковий хеш-код і зберігаються в одному відрі, це призводить до проходження всього списку, що призводить до O (n).

У випадку з java 8 відро зв'язаного списку замінюється на TreeMap, якщо розмір збільшується до більш ніж 8, це знижує найгірший випадок ефективності пошуку до O (log n).


1

Це в основному стосується більшості реалізацій хеш-таблиць у більшості мов програмування, оскільки сам алгоритм насправді не змінюється.

Якщо в таблиці немає зіткнень, вам потрібно зробити лише один огляд, тому час роботи - O (1). Якщо є зіткнення, вам доведеться виконати більше одного огляду, який призводить до зниження продуктивності у напрямку до O (n).


1
Це передбачає, що час запуску обмежений часом пошуку. На практиці ви знайдете багато ситуацій, коли хеш-функція забезпечує межу (String)
Stephan Eggermont

1

Це залежить від алгоритму, який ви вибрали, щоб уникнути зіткнень. Якщо ваша реалізація використовує окремий ланцюжок, тоді найгірший сценарій трапляється, коли кожен елемент даних хешируется з однаковим значенням (наприклад, поганий вибір хеш-функції). У цьому випадку пошук даних не відрізняється від лінійного пошуку у зв'язаному списку, тобто O (n). Однак ймовірність того, що станеться, незначна, і найкращі випадки пошуку і середні випадки залишаються постійними, тобто O (1).


1

З точки зору вчених, з практичної точки зору, HashMaps слід сприймати як такий, що має несуттєвий вплив на продуктивність (якщо тільки ваш профілер не скаже вам про інше).


4
Не в практичних додатках. Як тільки ви використовуєте рядок як ключ, ви помітите, що не всі хеш-функції ідеальні, а деякі справді повільні.
Стефан Еггермонт

1

Тільки в теоретичному випадку, коли хеш-коди завжди різні, а відро для кожного хеш-коду також різне, O (1) буде існувати. В іншому випадку він є постійним порядком, тобто при збільшенні хешмапу, його порядок пошуку залишається постійним.


0

Звичайно, продуктивність хешмапу буде залежати від якості функції хеш-коду () для даного об'єкта. Однак якщо функція реалізована таким чином, що можливість зіткнень дуже низька, вона матиме дуже гарну продуктивність (це не є строго O (1) у кожному можливому випадку, але це в більшості випадків).

Наприклад, реалізація за замовчуванням в Oracle JRE полягає у використанні випадкового числа (яке зберігається в екземплярі об'єкта, щоб воно не змінювалося, але воно також вимикає упереджене блокування, але це інша дискусія), тому шанс зіткнень дуже низький.


"це в більшості випадків". Більш конкретно, загальний час буде прагнути до K разів N (де K є постійним), оскільки N прагне до нескінченності.
ChrisW

7
Це неправильно. Індекс у хеш-таблиці визначатиметься, через hashCode % tableSizeщо, безумовно, можуть бути зіткнення. Ви не отримуєте повного використання 32-бітових. Ось така точка хеш-таблиць ... Ви зменшуєте великий простір індексації до малого.
FogleBird

1
"вам гарантовано, що не буде зіткнень" Ні, ви не тому, що розмір карти менший за розмір хеша: наприклад, якщо розмір карти два, то зіткнення гарантується (неважливо що хеш), якщо / коли я намагаюся вставити три елементи.
ChrisW

Але як перетворити з ключа в адресу пам'яті в O (1)? Я маю на увазі як x = array ["ключ"]. Ключ не є адресою пам'яті, тому все одно повинен бути пошук O (n).
paxdiablo

1
"Я вважаю, що якщо ви не реалізуєте hashCode, він буде використовувати адресу пам'яті об'єкта". Це могло б використовувати це, але хеш-код за замовчуванням для стандартного Java Oracle насправді є 25-бітним випадковим номером, що зберігається в заголовку об'єкта, тому 64/32-бітний не викликає жодних наслідків.
Боан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.