Як працює хеш-таблиця?


494

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

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

Хтось міг би прояснити процес?

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


4
Нещодавно я написав цю ( en.algoritmy.net/article/50101/Hash-table ) статтю, в якій описую декілька способів зберігання та пошуку даних, наголошуючи на хеш-таблицях та їх стратегіях (окремі ланцюжки, лінійне зондування, подвійне хешування )
malejpavouk

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

Відповіді:


913

Ось пояснення в умовах мирян.

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

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

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

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

Але як це можна зробити? Що ж, з трохи роздумів, коли ви заповнюєте бібліотеку, і багато роботи, коли ви заповнюєте бібліотеку.

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

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

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

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

Краса такого алгоритму полягає в тому, що якщо ви будете подавати один і той же вхід знову і знову, він буде постійно виплювати одне і те ж число кожного разу.

Ок, так це в основному, як працює хеш-таблиця.

Далі йде технічний матеріал.

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

Отже, що ми робимо? Ми використовуємо щось, що називається модулем обчислення, яке в основному говорить про те, що якщо ви порахували потрібне число (тобто мільярдне число), але хочете залишитися в набагато меншому діапазоні, кожен раз, коли ви досягаєте межі цього меншого діапазону, на якому ви починали назад 0, але ви повинні слідкувати за тим, наскільки далеко у великій послідовності ви прийшли.

Скажіть, що вихід алгоритму хешу знаходиться в діапазоні від 0 до 20, і ви отримуєте значення 17 з певного заголовка. Якщо розмір бібліотеки становить лише 7 книг, ви рахуєте 1, 2, 3, 4, 5, 6, а коли добираєтесь до 7, ви починаєте з 0. Оскільки нам потрібно порахувати 17 разів, у нас є 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, а кінцеве число - 3.

Звичайно, обчислення модуля не робиться так, це робиться з поділом і залишком. Залишок ділення 17 на 7 - 3 (7 переходить в 2 рази на 17 в 14, а різниця між 17 і 14 - 3).

Таким чином, ви помістите книгу в слот №3.

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

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

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

Я сподіваюся, що це пояснення було трохи більше на землю, ніж відра та функції :)


Дякую за таке чудове пояснення. Чи знаєте ви, де я можу знайти більше технічних деталей щодо того, як це реалізовано в 4.x .Net Framework?
Johnny_D

Ні, це просто число. Ви б просто нумерували кожну полицю та слот, починаючи з 0 або 1 і збільшуючи на 1 для кожного слота на цій полиці, а потім продовжуйте нумерацію на наступній полиці.
Лассе В. Карлсен

2
"Існують різні методи обробки зіткнень, включаючи запуск даних у ще один розрахунок, щоб отримати ще одне місце в таблиці" - що ви маєте на увазі під іншим розрахунком? Це просто ще один алгоритм? Добре, тож припустимо, що ми використовуємо інший алгоритм, який виводить інше число на основі назви книги. Потім, якби я знайшов цю книгу, як би я дізнався, який алгоритм використовувати? Я б використовував перший алгоритм, другий алгоритм і так далі, поки не знайду книгу, назва якої є такою, яку я шукаю?
користувач107986

1
@KyleDelaney: Ні для закритого хешування (коли зіткнення обробляються шляхом пошуку альтернативного відра, що означає використання пам'яті фіксовано, але ви витрачаєте більше часу на пошук по відрах). Для відкритого хешування ака-ланцюжка в патологічному випадку (жахлива хеш-функція або входи, навмисно створені для зіткнення якогось противника / хакера), ви можете закінчити, що більшість хеш-відра порожні, але загальне використання пам'яті не гірше - просто більше покажчиків NULL замість індексація до даних корисно.
Тоні Делрой

3
@KyleDelaney: потрібна річ "@Tony", щоб отримувати повідомлення про ваші коментарі. Здається, вам здається, що ви цікавитесь ланцюжком: скажімо, у нас є три вузли значення A{ptrA, valueA}, B{ptrB, valueB}, C{ptrC, valueC}та хеш-таблиця з трьома відрами [ptr1, ptr2, ptr3]. Незалежно від того, чи є зіткнення при вставці, використання пам'яті фіксується. У вас можуть не бути зіткнень: A{NULL, valueA} B{NULL, valueB} C{NULL, valueC}і [&A, &B, &C], або всі зіткнення A{&B, valueA} B{&C, valueB}, C{NULL, valueC}і [NULL, &A, NULL]: чи відмінені відра "NULL"? Щось, щось ні. Загальна використана пам'ять.
Тоні Делрой

104

Використання та лінго:

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

Приклад реального світу:

Компанія Hash & Co. , заснована в 1803 р. І не мала будь-яких комп'ютерних технологій, мала 300 кабінетів для зберігання детальної інформації (записів) для своїх приблизно 30 000 клієнтів. Кожна папка файлів була чітко ототожнена з її клієнтським номером, унікальним номером від 0 до 29 999.

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

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

Щоб отримати клієнтський запис, діловодам, що подають документи, надається номер клієнта на аркуші паперу. Використовуючи цей унікальний номер клієнта ( хеш-ключ ), вони модифікують його на 300, щоб визначити, в якій шафі подання була папка клієнтів. Коли вони відкрили шафу, вони виявили, що в ній багато папок, упорядкованих за номером клієнта. Шукаючи записи, вони швидко знайдуть папку клієнта та відновлять її.

У прикладі реального світу наші відра - це шафи для подачі документів, а наші записи - папки файлів .


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

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

Так, у наведеному вище прикладі 30000 можливих клієнтів або близько того відображаються на меншому просторі.


Основна ідея в цьому - розділити весь набір даних на сегменти, щоб прискорити фактичний пошук, який зазвичай займає багато часу. У нашому прикладі вище, кожен із 300 кабінетів подання даних (статистично) міститиме близько 100 записів. Пошук (незалежно від порядку) через 100 записів набагато швидший, ніж мати справу з 30 000.

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

Сподіваюся, це допомагає,

Джих!


2
Ви описуєте конкретний тип стратегії уникнення зіткнень хеш-таблиць, що називається варіативно "відкритою адресацією" або "закритою адресацією" (так, сумно, але правда) або "ланцюжком". Існує ще один тип, який не використовує відрізки списку, але замість цього зберігає елементи "inline".
Конрад Рудольф

2
відмінний опис. за винятком того, що кожна шафа подає в середньому приблизно 100записи (30 к. записів / 300 кабінетів = 100). Можливо, варто відредагувати.
Райан Тук

@TonyD, перейдіть на цей веб - сайт sha-1 в Інтернеті та створіть хеш SHA-1 для TonyDцього введення в текстовому полі. У вас з’явиться сформоване значення чогось схожого e5dc41578f88877b333c8b31634cf77e4911ed8c. Це не більше, ніж велика шістнадцяткова кількість 160 біт (20 байт). Потім ви можете використовувати це, щоб визначити, яке відро (обмежена кількість) буде використовуватися для зберігання вашої записи.
Джич

@TonyD, я не впевнений, де в конфліктній справі позначається термін "хеш-ключ"? Якщо так, то вкажіть дві або більше локацій. Або ви говорите, що "ми" використовуємо термін "хеш-ключ", а інші сайти, такі як Вікіпедія, використовують "хеш-значення, хеш-коди, хеш-суми або просто хеші"? Якщо це так, кого цікавить, доки термін, що використовується, є узгодженим у групі чи організації. Програмісти часто використовують "ключовий" термін. Я особисто заперечував би, що ще одним хорошим варіантом буде «хеш-цінність». Але я би виключав використання "хеш-коду, хеш-суми або просто хешей". Зосередьтеся на алгоритмі, а не на словах!
Jeach

2
@TonyD, я змінив текст на "вони модулюють хеш-ключ на 300", сподіваючись, що він буде чистішим і зрозумілішим для всіх. Дякую!
Jeach

64

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

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

Якщо у вас є лише невеликий простір для хешування, ви можете уникнути просто інтерпретації цих речей як цілих чисел, і ви закінчите (наприклад, 4 байт-рядки)

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

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

Врівноваження цих двох властивостей (і кількох інших) заважало багатьох людей!

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

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

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

Звичайно, на практиці ти зазвичай цього не можеш зробити, це витрачає занадто багато пам'яті. Таким чином, ви робите все на основі розрідженого масиву (де єдині записи є тими, якими ви насправді використовуєте, а все інше неявно недійсне).

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


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

@herbrandson: ні ... розріджений масив просто означає, що відносно мало індексів заповнене значеннями - ви все одно можете індексувати безпосередньо до конкретного елемента масиву для хеш-значення, яке ви обчислили зі свого ключа; все-таки розріджена реалізація масиву, яку описує Саймон, є розумною лише в дуже обмежених обставинах: коли розміри відра мають розмір сторінки пам'яті (порівняно з intклавішами скажіть на 1-1000 розрідженості та 4k сторінок = більшість торкаються сторінок), і коли ОС працює ефективно на всіх 0 сторінках (тому всі сторінки, які не використовуються, не потребують резервної пам’яті), коли адресного простору багато…
Тоні Делрой

@TonyDelroy - це правда, це надмірне спрощення, але ідея полягала в тому, щоб дати огляд того, що вони є і чому, а не практичне втілення. Деталі останнього більш нюансовані, коли ви киваєте на розширення.
Сімон

48

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

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

bucket#  bucket content / linked list

[0]      --> "sue"(780) --> null
[1]      null
[2]      --> "fred"(42) --> "bill"(9282) --> "jane"(42) --> null
[3]      --> "mary"(73) --> null
[4]      null
[5]      --> "masayuki"(75) --> "sarwar"(105) --> null
[6]      --> "margaret"(2626) --> null
[7]      null
[8]      --> "bob"(308) --> null
[9]      null

Кілька пунктів:

  • кожен із записів масиву (індекси [0], [1]...) відомий як відро і починає - можливо, порожній - пов'язаний список значень (ака елементів , у цьому прикладі - імена людей )
  • кожне значення (наприклад, "fred"з хешем 42) пов'язане з відра, [hash % number_of_buckets]наприклад 42 % 10 == [2]; %- це оператор модуля - решта, що ділиться на кількість відра
  • декілька значень даних можуть зіштовхуватися у та пов’язані з одного і того ж відра, найчастіше тому, що їх хеш-значення стикаються після операції модуля (наприклад 42 % 10 == [2], та 9282 % 10 == [2]), але іноді тому, що значення хешу однакові (наприклад, "fred"і "jane"обидва показані хешем 42вище)
    • більшість хеш-таблиць обробляють зіткнення - з дещо зниженою продуктивністю, але без функціональної плутанини - порівнюючи повне значення (тут текст) значення, яке шукається або вставляється, до кожного значення, яке вже є у зв'язаному списку, у відрізці хеш-до

Довжина пов'язаних списків стосується коефіцієнта навантаження, а не кількості значень

Якщо розмір таблиці збільшується, хеш-таблиці, реалізовані, як зазначено вище, мають тенденцію змінювати розмір (тобто створювати більший масив відер, створювати нові / оновлені зв'язані списки звідти, видаляти старий масив), щоб зберегти відношення значень до відра (aka load) фактор ) десь у діапазоні 0,5 - 1,0.

Ханс наводить фактичну формулу для інших коефіцієнтів навантаження в коментарі нижче, але для орієнтовних значень: з коефіцієнтом навантаження 1 та функцією хешування криптографічної сили, 1 / е (~ 36,8%) відра буде, як правило, порожнім, ще 1 / е (~ 36,8%) мають один елемент, 1 / (2e) або ~ 18,4% два елементи, 1 / (3! E) приблизно 6,1% три елементи, 1 / (4! E) або ~ 1,5% чотири елементи, 1 / (5! E) ~ .3% мають п’ять і т. Д. - середня довжина ланцюга з не порожніх відер становить ~ 1,58 незалежно від того, скільки елементів міститься в таблиці (тобто чи є 100 елементів і 100 відра, або 100 мільйонів елементів і 100 мільйонів відро), тому ми кажемо, що пошук / вставка / стирання є O (1) операціями постійного часу.

Як хеш-таблиця може асоціювати ключі зі значеннями

З огляду на реалізацію хеш-таблиці, як описано вище, ми можемо уявити, як створити такий тип значення, як struct Value { string name; int age; };, порівняння рівності та хеш-функції, які дивляться лише на nameполе (ігноруючи вік), і тоді трапиться щось чудове: ми можемо зберігати Valueзаписи, як {"sue", 63}у таблиці , потім пізніше шукайте "позов", не знаючи її віку, знайдіть збережене значення та відновіть або навіть оновіть її вік
- з днем ​​народження Сью - що цікаво не змінює хеш-значення, тому не вимагає перенесення запису Сью на інший відро.

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

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

Є й інші способи реалізації хеш-таблиці

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


Кілька слів про хеш-функції

Сильний хеш ...

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

Це, як правило, оркестровано з математикою, надто складною для мене. Я згадаю один простий для розуміння спосіб - не самий масштабований або кеш-приємний, але за своєю суттю елегантний (як шифрування одноразовим майданчиком!) - так як я думаю, що це допомагає домогтися додому бажаних якостей, згаданих вище. Скажіть, що ви хешували 64-бітні doubles - ви можете створити 8 таблиць у кожному з 256 випадкових чисел (код нижче), а потім використовувати кожен 8-бітний / 1-байтовий фрагмент doubleпредставлення пам'яті для індексації в іншій таблиці, XORing the випадкові числа, які ви шукаєте. При такому підході легко помітити, що трохи (у значенні двійкової цифри), що змінюється в будь-якому місці doubleрезультатів, в одній із таблиць шукається інше випадкове число, і цілком некорельоване кінцеве значення.

// note caveats above: cache unfriendly (SLOW) but strong hashing...
size_t random[8][256] = { ...random data... };
const char* p = (const char*)&my_double;
size_t hash = random[0][p[0]] ^ random[1][p[1]] ^ ... ^ random[7][p[7]];

Слабкий, але часто швидкий хеш ...

Функції хешування Багато бібліотек проходять через цілі числа без змін (відомий як тривіальної або ідентичність хеш - функції); це інша крайність із сильного хешування, описаного вище. Хеш ідентичності є надзвичайнозіткнення в найгірших випадках, але сподіваємось, що в досить поширеному випадку цілих клавіш, які мають тенденцію до збільшення (можливо, з деякими прогалинами), вони будуть перетворюватися на послідовні відра, залишаючи менше порожніх, ніж випадкові хеширующие листя (наші ~ 36,8 % при коефіцієнті навантаження 1, згаданому раніше), тим самим маючи менше зіткнень і менше довше пов'язаних списків стикаються елементів, ніж це досягається випадковими відображеннями. Також чудово економити час, необхідний для створення сильного хешу, і якщо ключі шукати, щоб їх знайти у відро поблизу пам’яті, покращуючи хіти кешу. Коли клавіші незбільшуються добре, сподіваємось, що вони будуть достатньо випадковими, їм не знадобиться сильна хеш-функція, щоб повністю рандомизувати їх розміщення у відра.


6
Дозвольте мені просто сказати: фантастична відповідь.
CRThaze

@Tony Delroy Дякую за дивовижну відповідь. Я все ще маю одну відкриту точку в своєму розумі, хоча. Ви говорите, що навіть якщо є 100 мільйонів відер, час пошуку буде O (1) з коефіцієнтом навантаження 1 та хеш-функцією криптографічної сили. А як же знайти правильне відро на 100 мільйонів? Навіть якщо ми відсортували всі відра, чи не O (log100.000.000)? Як знайти відро для O (1)?
selman

@selman: ваше запитання не містить багато деталей, щоб пояснити, чому ви думаєте, що це може бути O (log100,000,000), але ви говорите "навіть якщо у нас усі відра відсортовані" - майте на увазі, що значення в ковшах хеш-таблиць ніколи і НЕ «упорядковано» в звичайному сенсі цього слова: який з'являється значення , в якому ківш визначається шляхом застосування хеш - функції до ключу. Якщо вважати складність O (log100,000,000), ви маєте на увазі, що ви представляєте двійковий пошук через відсортовані відра, але це не так, як працює хешування. Можливо, прочитайте кілька інших відповідей і подивіться, чи це не має сенсу.
Тоні Делрой

@TonyDelroy Дійсно, "відсортовані відра" - найкращий сценарій, який я уявляю. Звідси O (log100,000,000). Але якщо це не так, як додаток може знайти відповідне відро серед мільйонів? Чи створює хеш-функція якось розташування пам'яті?
selman

1
@selman: тому що пам'ять комп'ютера дозволяє постійно "випадковий доступ": якщо ви можете обчислити адресу пам'яті, ви можете отримати вміст пам'яті, не маючи доступу до пам'яті в інших частинах масиву. Отже, незалежно від того, чи отримуєте ви доступ до першого, останнього відра чи відра десь посередині, він матиме однакові характеристики продуктивності (вільно, забирайте однакову кількість часу, хоча і залежно від впливу кешування пам'яті процесора L1 / L2 / L3, але вони працюють лише, щоб допомогти вам швидко повторно отримати доступ до нещодавно доступних або випадково розташованих відра, і їх можна проігнорувати для аналізу big-O).
Тоні Делрой

24

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

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

uint slotIndex = hashValue % hashTableSize;

Це значення є слотом, в яке буде входити значення. У відкритій адресації, якщо слот вже заповнений іншим значенням і / або іншими даними, операція з модулем буде запущена ще раз, щоб знайти наступний слот:

slotIndex = (remainder + 1) % hashTableSize;

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

Якщо у вас є метод модуля, якщо у вас є таблиця розміру 1000, будь-яке значення, що має значення від 1 до 1000, перейде у відповідний слот. Будь-які негативні значення та будь-які значення, що перевищують 1000, потенційно зможуть зіткнутися зі значеннями слота. Шанси цього трапляються залежать як від вашого методу хешування, так і від кількості загальних елементів, які ви додасте до хеш-таблиці. Як правило, найкраще робити розмір хештеля таким, щоб загальна кількість доданих до нього значень становила лише приблизно 70% від його розміру. Якщо ваша хеш-функція добре справляється з рівномірним розподілом, ви, як правило, стикаєтеся з дуже малою кількістю зіткнень між ковшами та слотами, і вона буде виконуватись дуже швидко як для операцій пошуку, так і для запису. Якщо загальна кількість значень для додавання не відома заздалегідь, зробіть хорошу оцінку, використовуючи будь-які засоби,

Я сподіваюся, що це допомогло.

PS - У C # GetHashCode()метод досить повільний і призводить до фактичних зіткнень за величиною при багатьох умовах, які я перевірив. Для справжньої розваги побудуйте свою власну хеш-функцію і спробуйте дозволити її НІКОЛИ не стикатися з конкретними даними, які ви хешуєте, бігайте швидше, ніж GetHashCode, і отримайте досить рівномірний розподіл. Я робив це, використовуючи довгі замість значень хеш-коду розміру int, і він досить добре працював на до 32 мільйонів ентіш-хеш-значень у хештейлі з 0 зіткненнями. На жаль, я не можу поділитися кодом, оскільки він належить моєму роботодавцю ... але можу виявити, що це можливо для певних доменів даних. Коли ви можете досягти цього, хештейт ДУЖЕ швидко. :)


я знаю, що повідомлення досить стара, але чи може хтось пояснити, що тут (залишок + 1) означає
Харі,

3
@Hari remainderпосилається на результат початкового обчислення модуля, і ми додаємо до нього 1, щоб знайти наступний доступний слот.
x4nd3r

"Сам масив буде містити щось у кожному слоті. Як мінімум, ви зберігатимете хевалю або значення у цьому слоті." - "слоти" (відра) зазвичай не зберігають значення; Реалізації відкритої адреси часто зберігають або NULL, або вказівник на перший вузол у пов'язаному списку - без значення безпосередньо в слоті / відрі. "зацікавив би інших" - "+1", який ви ілюструєте, називається лінійним зондуванням , часто більш ефективним: квадратичним зондуванням . "загалом стикаються дуже мало, коли жодних зіткнень між ковшами та гніздами немає" - @ 70% ємність, ~ 12% слотів з 2/3 значеннями, ~ 3% 3 ....
Тоні Делрой

"Я робив це, використовуючи довгі замість значень хеш-коду розміру int, і він досить добре працював на до 32 мільйонів ентіш-хеш-значень у хештейлі з 0 зіткненнями." - це просто неможливо в загальному випадку, коли значення ключів фактично випадкові в набагато більшому діапазоні, ніж кількість відра. Зауважте, що мати чіткі хеш-значення часто досить просто (а ваша розмова про longхеш-значення означає, що ви цього досягли), але переконайтесь, що вони не стикаються. в хеш-таблиці після того, як мод /% не працює (у загальному випадку ).
Тоні Делрой

(Уникнення всіх зіткнень називається ідеальним хешуванням . Загалом, це практично для кількох сотень чи тисяч ключів, які відомі заздалегідь - gperf - приклад інструменту для обчислення такої хеш-функції. Ви також можете написати свій власний дуже обмежений обставини - наприклад, якщо ваші ключі - покажчики на об’єкти з вашого власного пулу пам'яті, який зберігається досить заповненим, і кожен вказівник фіксується на відстань, ви можете розділити покажчики на цю відстань і ефективно мати покажчик на злегка розрізнений масив, уникаючи зіткнення.)
Тоні Делрой

17

Ось як це працює в моєму розумінні:

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

Скажімо, у вас є 200 об’єктів, але лише 15 з них мають хеш-коди, що починаються з літери "B." Хеш-таблицю потрібно буде лише шукати і шукати через 15 об’єктів у відрі "B", а не всі 200 об'єктів.

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


13

Коротке і солодке:

Хеш-таблиця завершує масив, дозволяє називати його internalArray. Елементи вставляються в масив таким чином:

let insert key value =
    internalArray[hash(key) % internalArray.Length] <- (key, value)
    //oversimplified for educational purposes

Іноді дві клавіші хешують один і той же індекс у масиві, і ви хочете зберегти обидва значення. Мені подобається зберігати обидва значення в одному індексі, який легко кодувати, створюючи internalArrayмасив пов'язаних списків:

let insert key value =
    internalArray[hash(key) % internalArray.Length].AddLast(key, value)

Отже, якби я хотів отримати предмет із своєї хеш-таблиці, я міг би написати:

let get key =
    let linkedList = internalArray[hash(key) % internalArray.Length]
    for (testKey, value) in linkedList
        if (testKey = key) then return value
    return null

Операції з видалення так само просто написати. Як ви можете сказати, вставок, пошуку та видалення з нашого масиву зв'язаних списків майже O (1).

Коли наш внутрішній масив стає занадто заповненим, можливо, на рівні близько 85%, ми можемо змінити розмір внутрішнього масиву і перемістити всі елементи зі старого масиву в новий масив.


11

Це навіть простіше цього.

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

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

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

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


10

Ви берете купу речей і масив.

Для кожної речі ви складаєте для нього індекс, який називається хеш. Важливим у хеші є те, що він сильно «розсипається»; ви не хочете, щоб дві схожі речі мали подібні хеші.

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

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

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

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


1
дякую за останню точку, яку всі інші пропустили згадати
Sandeep Raju Prabhakar

4

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

Як пояснив Сімон, хеш-функція використовується для відображення великого простору в малий простір. Проста, наївна реалізація хеш-функції для нашого прикладу може взяти першу букву рядка і відобразити її на ціле число, тому "алігатор" має хеш-код 0, "бджола" має хеш-код 1 " зебра "було б 25 і т.д.

Далі ми маємо масив з 26 відрів (може бути ArrayLists на Java), і ми поміщаємо елемент у відро, яке відповідає хеш-коду нашого ключа. Якщо у нас є більше одного елемента, у якого є ключ, який починається з тієї самої літери, у них буде той самий хеш-код, тож усі вони йдуть у відро для цього хеш-коду, тому лінійний пошук повинен бути здійснений у відрі для знайти конкретний предмет.

У нашому прикладі, якби у нас було лише кілька десятків елементів із клавішами, що охоплюють алфавіт, це працювало б дуже добре. Однак якби у нас було мільйон елементів або всі ключі почалися з 'a' або 'b', то наша хеш-таблиця не була б ідеальною. Для кращої продуктивності нам знадобиться інша хеш-функція та / або більше відра.


3

Ось ще один спосіб поглянути на це.

Я припускаю, що ви розумієте поняття масиву A. Це те, що підтримує операцію індексації, де ви можете дістатися до I-го елемента, A [I], за один крок, незалежно від того, наскільки великий A.

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

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

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


2

Те, як обчислюється хеш, зазвичай залежить не від хешбела, а від доданих до нього елементів. У бібліотеках фреймворків / базових класів, таких як .net і Java, кожен об'єкт має метод GetHashCode () (або подібний), який повертає хеш-код для цього об'єкта. Ідеальний алгоритм хеш-коду та точна реалізація залежить від даних, представлених в об'єкті.


2

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

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

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

Тепер великий набір, відображений на невеликому наборі, відображення має бути багато-на-один. Отже, різним клавішам буде виділено однаковий простір (?? не справедливо). Є кілька способів вирішити це, я просто знаю два популярні з них:

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

Вступ до алгоритмів CLRS дає дуже хороший погляд на цю тему.


0

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

(void) addValue : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   if (bucket) 
   {
       //do nothing, just overwrite
   }
   else   //create bucket
   {
      create_extra_space_for_bucket();
   }
   put_value_into_bucket(bucket,value);
}

(bool) exists : (object) value
{
   int bucket = calculate_bucket_from_val(value);
   return bucket;
}

де calculate_bucket_from_val()функція хешування, де повинна відбуватися вся унікальність магії.

Правило великого пальця: Для того, щоб задане значення було вставлено, відро повинно бути УНІКАЛЬНИМ І ПЕРЕДБАЧЕНОМИ ЗНАЧЕННЯ, яке воно має зберігати.

Bucket - це будь-який простір, де зберігаються значення - адже тут я зберег його int як індекс масиву, але це, можливо, і місце пам'яті.


1
"правило: для того, щоб задане значення було вставлено, відро повинно бути УНІКАЛЬНИМ І ПОВЕРХНЕНОМИ З ЦІННОСТІ, яке воно має зберігати." - це описує ідеальну хеш-функцію , яка, як правило, можлива лише для кількох сотень чи тисяч значень, відомих під час компіляції. Більшість хеш-таблиць мають вирішувати зіткнення . Крім того, хеш-таблиці мають тенденцію виділяти простір для всіх відер, незалежно від того, чи вони порожні чи ні, тоді як ваш псевдо-код документує create_extra_space_for_bucket()крок під час вставлення нових ключів. Відра можуть бути покажчиками.
Тоні Делрой
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.