HashMap отримати / поставити складність


131

Ми звикли говорити, що HashMap get/putоперації O (1). Однак це залежить від реалізації хешу. Типовий хеш об'єкта - це фактично внутрішня адреса в купі JVM. Ми впевнені, що це досить добре, щоб стверджувати, що get/putє O (1)?

Наявна пам'ять - ще одне питання. Як я розумію з javadocs, показник HashMap load factorповинен бути 0,75. Що робити, якщо нам не вистачає пам'яті в JVM і load factorперевищує межу?

Отже, схоже, що O (1) не гарантується. Це має сенс чи я щось пропускаю?


1
Можливо, ви захочете знайти концепцію амортизованої складності. Дивіться, наприклад, тут: stackoverflow.com/questions/3949217/time-complexity-of-hash-table Найгірша складність випадку не є найважливішим заходом для хеш-таблиці
Dr G

3
Правильно - це амортизований O (1) - ніколи не забувайте про цю першу частину, і таких питань не будете мати :)
Інженер

Найгірший випадок у часі - це O (logN), оскільки Java 1.8, якщо я не помиляюся.
Тарун Колла

Відповіді:


216

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

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

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

І так, якщо у вас не вистачає пам'яті для хеш-карти, ви потрапите в біду ... але це буде правдою незалежно від структури даних, яку ви використовуєте.


@marcog: Ви припускаєте O (n log n) для одного пошуку ? Мені це здається хитромудрим. Це, звичайно, залежатиме від складності хеш-функцій та рівності, але це навряд чи залежатиме від розміру карти.
Джон Скіт

1
@marcog: Отже, що ви вважаєте за O (n log n)? Вставка n предметів?
Джон Скіт

1
+1 за гарну відповідь. Будь ласка, надайте у своїй відповіді посилання, подібні до цієї статті у вікіпедії для хеш-таблиці ? Таким чином, більш зацікавлений читач міг би дійти до нестримного зернистості розуміння того, чому ви дали свою відповідь.
Девід Вейзер

2
@SleimanJneidi: Це все ще є, якщо ключ не реалізує Порівняний <T> `- але я оновлю відповідь, коли матиму більше часу.
Джон Скіт

1
@ ip696: Так, putце "амортизований O (1)" - зазвичай O (1), іноді O (n) - але досить рідко, щоб збалансувати.
Джон Скіт

9

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

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

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


6
Чорт. Я вважаю, що якби мені не довелося набирати це на перегортаючому сенсорному екрані мобільного телефону, я міг би побити Джона Листа на удар. Для цього є значок, правда?
Том Андерсон

8

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

Тепер, переходячи до другої частини питання про пам’ять, тоді так обмеження пам’яті буде опікуватися JVM.


8

Вже згадувалося, що хешмапи є O(n/m)в середньому, якщо nце кількість предметів і mрозмір. Також було відзначено, що в принципі вся справа може розпастись в окремо пов'язаний список із O(n)часом запитів. (Це все передбачає, що обчислення хешу є постійним часом).

Однак те, що не часто згадується, це те, що принаймні з ймовірністю 1-1/n(так що для 1000 предметів це 99,9% шансу) найбільше відро не заповниться більше O(logn)! Отже, відповідність середньої складності дерев бінарного пошуку. (І константа хороша, жорсткіша межа (log n)*(m/n) + O(1)).

Все, що потрібно для цієї теоретичної межі, - це те, що ви використовуєте досить хорошу хеш-функцію (див. Вікіпедія: Універсальний хешинг . Це може бути так само просто a*x>>m). І звичайно, що людина, яка дає вам значення хешу, не знає, як ви обрали випадкові константи.

TL; DR: З дуже високою ймовірністю найгірший випадок складності хешмапу - це отримати / поставити O(logn).


(І зауважте, що нічого з цього не передбачає випадкових даних. Ймовірність випливає суто з вибору хеш-функції)
Thomas Ahle

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

4

Я погоджуюся з:

  • загальна амортизована складність O (1)
  • погана hashCode()реалізація може призвести до декількох зіткнень, а це означає, що в гіршому випадку кожен об'єкт переходить до того ж відра, таким чином O ( N ), якщо кожне відро підтримується символом a List.
  • оскільки Java 8 HashMapдинамічно замінює Вузли (зв'язаний список), що використовуються у кожному відрі, TreeNodes (червоно-чорне дерево, коли список набирає більше 8 елементів), що призводить до найгіршої продуктивності O ( logN ).

Але це НЕ в повній істині, якщо ми хочемо бути на 100% точними. Реалізація hashCode()та тип ключа Object(незмінний / кешований або колекційний) також можуть суворо впливати на реальну складність.

Припустимо наступні три випадки:

  1. HashMap<Integer, V>
  2. HashMap<String, V>
  3. HashMap<List<E>, V>

Чи мають вони однакову складність? Ну а амортизована складність 1-го, як очікується, O (1). Але, для решти, нам також потрібно обчислити hashCode()елемент пошуку, а це означає, що нам, можливо, доведеться пересувати масиви та списки в нашому алгоритмі.

Припустимо, що розмір усіх вищезазначених масивів / списків дорівнює k . Тоді HashMap<String, V>і HashMap<List<E>, V>буде мати O (k) амортизовану складність і аналогічно, O ( k + logN ) в гіршому випадку у Java8.

* Зауважте, що використання Stringключа є більш складним випадком, оскільки він незмінний, а Java кешує результат hashCode()у приватній змінній hash, тому він обчислюється лише один раз.

/** Cache the hash code for the string */
    private int hash; // Default to 0

Але вищезазначене має і свій найгірший випадок, тому що String.hashCode()реалізація Java перевіряє, чи потрібно hash == 0перед обчисленням hashCode. Але ей, є не порожні рядки, які виводять hashcodeнуль, наприклад "f5a5a608", дивіться тут , і в цьому випадку запам'ятовування може бути не корисним.


2

На практиці це O (1), але це насправді жахливе і математично нерозуміле спрощення. Позначення O () говорить про те, як поводиться алгоритм, коли розмір проблеми має тенденцію до нескінченності. Hashmap get / put працює як алгоритм O (1) для обмеженого розміру. Обмеження досить велике з пам'яті комп'ютера та з точки зору адресації, але далеко не нескінченне.

Коли можна сказати, що get / put hashmap - це O (1), то дійсно слід сказати, що час, необхідний для get / put, є більш-менш постійним і не залежить від кількості елементів у хешмапі, наскільки може бути хешмап представлені на власне обчислювальній системі. Якщо проблема виходить за рамки цього розміру і нам потрібні більші хешмапи, через деякий час, безсумнівно, кількість бітів, що описують один елемент, також збільшиться, коли у нас вичерпаються можливі описані різні елементи. Наприклад, якщо ми використовували хеш-карту для зберігання 32-бітових чисел і пізніше збільшуємо розмір проблеми, щоб у нас було більше 2 ^ 32 бітових елементів у хешмапі, то окремі елементи будуть описані з більш ніж 32 бітами.

Кількість бітів, необхідних для опису окремих елементів, це log (N), де N - максимальна кількість елементів, тому get і put є дійсно O (log N).

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

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


O(log(n) * log(max(n)))Тоді чи не повинна бути складність для набору дерев ? Хоча порівняння на кожному вузлі може бути розумнішим, в гіршому випадку потрібно перевірити всі O(log(max(n))біти, правда?
maaartinus
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.