Как работает хеш-таблица?

Я ищу объяснение того, как работает хеш-таблица - простым английским языком для такого простака, как я!

Например, я знаю, что он берет ключ, вычисляет хэш (я ищу объяснение, как), а затем выполняет какой-то модуль, чтобы определить, где он находится в массиве, где хранится значение, но на этом мои знания останавливаются .

Может ли кто-нибудь прояснить процесс?

Изменить: я не спрашиваю конкретно о том, как рассчитываются хеш-коды, а в общих чертах смотрю, как работает хеш-таблица.


person Arec Barrwin    schedule 08.04.2009    source источник
comment
Недавно я написал это (en.algoritmy.net/article/50101/Hash-table) статья, описывающая несколько способов хранения и поиска данных, с акцентом на хеш-таблицы и их стратегии (раздельное связывание, линейное зондирование, двойное хеширование)   -  person malejpavouk    schedule 28.03.2013
comment
Вы можете рассматривать хеш-таблицу как расширенную версию массива, которая не ограничивается только последовательными целочисленными ключами.   -  person user253751    schedule 23.02.2016
comment
Вот еще один: intelligentjava.wordpress.com/2016/ 19.10 /   -  person nesvarbu    schedule 20.10.2016


Ответы (16)


Вот объяснение в условиях непрофессионала.

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

Итак, вы решаете, что если человек, который хочет прочитать книгу, знает название книги и точное название, то это все, что нужно. По названию человек с помощью библиотекаря сможет легко и быстро найти книгу.

Итак, как вы можете это сделать? Что ж, очевидно, вы можете вести какой-то список, куда вы кладете каждую книгу, но тогда у вас есть та же проблема, что и при поиске в библиотеке, вам нужно искать в списке. Конечно, список будет меньше и его будет легче искать, но все же вы не хотите выполнять поиск последовательно от одного конца библиотеки (или списка) до другого.

Вам нужно что-то, что с названием книги может сразу указать вам нужное место, поэтому все, что вам нужно сделать, это просто подойти к нужной полке и взять книгу.

Но как это сделать? Что ж, немного предусмотрительно, когда вы заполняете библиотеку, и много работы, когда вы заполняете библиотеку.

Вместо того, чтобы просто начинать заполнять библиотеку от одного конца до другого, вы придумываете небольшой хитрый метод. Вы берете название книги, запускаете его через небольшую компьютерную программу, которая выводит номер полки и номер слота на этой полке. Здесь вы кладете книгу.

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

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

Для простоты предположим, что он просто преобразует каждую букву и символ в число и суммирует их все. На самом деле, это намного сложнее, но давайте пока оставим все как есть.

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

Хорошо, так в основном и работает хеш-таблица.

Далее следует технический материал.

Во-первых, размер числа. Обычно выходные данные такого хеш-алгоритма находятся в диапазоне некоторого большого числа, обычно намного большего, чем пространство, которое у вас есть в вашей таблице. Например, предположим, что в библиотеке есть место ровно для одного миллиона книг. Результат вычисления хэша может быть в диапазоне от 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.

Это приводит к следующей проблеме. Столкновения. Поскольку алгоритм не имеет возможности распределить книги таким образом, чтобы они точно заполняли библиотеку (или хэш-таблицу, если хотите), он неизменно в конечном итоге вычислит число, которое использовалось ранее. В библиотечном смысле, когда вы подходите к полке и к номеру слота, в который хотите положить книгу, книга уже там есть.

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

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

Надеюсь, это объяснение было немного более приземленным, чем ведра и функции :)

person Lasse V. Karlsen    schedule 08.04.2009
comment
Спасибо за такое прекрасное объяснение. Вы знаете, где я могу найти более подробную техническую информацию о том, как это реализовано в платформе 4.x.Net? - person Johnny_D; 26.01.2015
comment
Нет, это просто число. Вы просто должны пронумеровать каждую полку и слот, начиная с 0 или 1 и увеличивая на 1 для каждого слота на этой полке, а затем продолжить нумерацию на следующей полке. - person Lasse V. Karlsen; 12.03.2015
comment
«Существуют различные методы обработки столкновений, в том числе передача данных в еще один расчет, чтобы получить другое место в таблице» - что вы имеете в виду под другим расчетом? Это просто еще один алгоритм? Итак, предположим, что мы используем другой алгоритм, который выводит другое число в зависимости от названия книги. Позже, если я найду эту книгу, как я узнаю, какой алгоритм использовать? Я бы использовал первый алгоритм, второй алгоритм и так далее, пока не найду книгу, название которой я ищу? - person user107986; 06.09.2015
comment
@ user107986: Я бы использовал первый алгоритм, второй алгоритм и так далее, пока не найду книгу, название которой я ищу? - да. Тем не менее, более обычным является поиск в виде последовательности смещений от исходной хешированной позиции, например, каждая последующая позиция до тех пор, пока одна из них не станет пустой (что указывает на отсутствие совпадения / место, в которое вы можете вставить новую книгу / значение) (так называемое линейное зондирование), или при +1, +4, +9, +16 и т. д. (так называемое квадратичное зондирование) .... - person Tony Delroy; 02.05.2016
comment
Отличное объяснение. Теперь я могу легко объяснить это другим. - person Sachin Aryal; 06.07.2016
comment
Я всегда думал, что это хеш-функция, которая может вызвать коллизию. Спасибо. Я тоже! :) - person SMUsamaShah; 17.11.2016
comment
Могут ли хеш-таблицы оставить много пустого потраченного впустую пространства, где ничего не хранится, заставляя данные занимать больше места, чем в противном случае? - person Kyle Delaney; 21.12.2016
comment
Муахаха, это мой секретный план по завоеванию мира, оставлять злые ответы на Stack Overflow. Ой, уже не такой секрет ... - person Lasse V. Karlsen; 26.01.2017
comment
@KyleDelaney: Нет для закрытого хеширования (где коллизии обрабатываются путем поиска альтернативной корзины, что означает использование памяти исправлено, но вы тратите больше времени на поиск по сегментам). Для открытого хеширования, также известного как цепочка в патологическом случае (ужасная хеш-функция или входные данные, намеренно созданные для столкнуться с каким-то злоумышленником / хакером) вы можете закончить тем, что большинство хеш-ведер будет пустым, но общее использование памяти не хуже - просто больше указателей NULL вместо полезной индексации данных. - person Tony Delroy; 31.08.2017
comment
Разве лишние нулевые указатели не ухудшают использование памяти? Разве хеш-таблица не будет содержать много нулей, которые в противном случае она не должна была бы содержать? - person Kyle Delaney; 01.09.2017
comment
@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]: пустые сегменты потрачены впустую? Вроде нет. Используется тот же общий объем памяти. - person Tony Delroy; 13.02.2018

Использование и жаргон:

  1. Хеш-таблицы используются для быстрого хранения и извлечения данных (или записей).
  2. Записи хранятся в сегментах с использованием хэш-ключей.
  3. Ключи хеширования вычисляются путем применения алгоритма хеширования к выбранному значению (значению ключа), содержащемуся в записи. Это выбранное значение должно быть общим значением для всех записей.
  4. В каждом сегменте может быть несколько записей, организованных в определенном порядке.

Пример из реального мира:

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

Клеркам того времени приходилось быстро получать и хранить записи о клиентах для рабочего персонала. Персонал решил, что было бы более эффективно использовать методологию хеширования для хранения и извлечения своих записей.

Чтобы подать запись о клиенте, регистраторы будут использовать уникальный номер клиента, записанный в папке. Используя этот номер клиента, они будут модулировать ключ хеширования на 300, чтобы идентифицировать картотечный шкаф, в котором он содержится. Когда они открыли картотечный шкаф, они обнаружили бы, что в нем было много папок, упорядоченных по номеру клиента. . Определив правильное место, они просто вставили его.

Чтобы получить запись о клиенте, регистраторам дадут номер клиента на листке бумаги. Используя этот уникальный номер клиента (хеш-ключ), они будут модулировать его на 300, чтобы определить, в каком картотеке находится папка клиентов. Когда они открыли картотечный шкаф, то обнаружили, что в нем много папок, отсортированных по номерам клиентов. Просматривая записи, они быстро находили клиентскую папку и извлекали ее.

В нашем реальном примере наши корзины - это картотеки, а наши записи - это папки с файлами.


Важно помнить, что компьютеры (и их алгоритмы) лучше работают с числами, чем со строками. Таким образом, доступ к большому массиву с использованием индекса значительно быстрее, чем последовательный доступ.

Как уже упоминал Саймон, который я считаю очень важным, заключается в том, что часть хеширования должна преобразовывать большое пространство (произвольной длины, обычно строки и т. д.) и отображать его на небольшое пространство (известного размера, обычно числа) для индексации. Об этом очень важно помнить!

Итак, в приведенном выше примере около 30 000 возможных клиентов сопоставлены с меньшим пространством.


Основная идея состоит в том, чтобы разделить весь набор данных на сегменты, чтобы ускорить фактический поиск, который обычно занимает много времени. В нашем примере выше каждый из 300 картотек будет (статистически) содержать около 100 записей. Искать (независимо от порядка) по 100 записям намного быстрее, чем иметь дело с 30 000.

Вы могли заметить, что некоторые на самом деле это уже делают. Но вместо разработки методологии хеширования для генерации хеш-ключа они в большинстве случаев будут просто использовать первую букву фамилии. Итак, если у вас есть 26 картотек, каждый из которых содержит буквы от A до Z, вы теоретически только что сегментировали свои данные и улучшили процесс регистрации и поиска.

Надеюсь это поможет,

Джич!

person Jeach    schedule 08.04.2009
comment
Вы описываете особый тип стратегии предотвращения конфликтов в хеш-таблицах, называемый по-разному «открытая адресация» или «закрытая адресация» (да, грустно, но верно) или «цепочка». Есть еще один тип, который не использует сегменты списка, а вместо этого хранит элементы «встроенными». - person Konrad Rudolph; 08.04.2009
comment
отличное описание. за исключением того, что каждый картотечный шкаф будет содержать в среднем около 100 записей (30 тыс. записей / 300 шкафов = 100). Возможно, стоит отредактировать. - person ryantuck; 07.11.2014
comment
@TonyD, перейдите на этот сайт sha-1 online и сгенерируйте хэш SHA-1 для TonyD, вы вводите текст в текстовое поле. В итоге вы получите сгенерированное значение чего-то похожего на e5dc41578f88877b333c8b31634cf77e4911ed8c. Это не что иное, как большое шестнадцатеричное число из 160 бит (20 байт). Затем вы можете использовать это, чтобы определить, какой сегмент (ограниченное количество) будет использоваться для хранения вашей записи. - person Jeach; 31.05.2016
comment
@TonyD, я не уверен, где в противоречивом вопросе упоминается термин хеш-ключ? Если да, укажите два или более мест. Или вы говорите, что мы используем термин «хеш-ключ», в то время как другие сайты, такие как Википедия, используют хеш-значения, хеш-коды, хеш-суммы или просто хеш-коды? Если да, то кого это волнует, если термин используется внутри группы или организации. Программисты часто используют ключевой термин. Я лично считаю, что еще одним хорошим вариантом будет хеш-значение. Но я бы исключил использование хеш-кода, хеш-суммы или просто хешей. Сосредоточьтесь на алгоритме, а не на словах! - person Jeach; 03.06.2016
comment
@TonyD, я изменил текст на модуль хеш-ключа на 300, надеясь, что он будет чище и понятнее для всех. Спасибо! - person Jeach; 03.06.2016
comment
@Jeach, это тоже очень полезно, я искал в Интернете интуитивно понятное объяснение хеширования, и этот список ответов на исходный вопрос великолепен! - person carozimm; 05.06.2020

Оказывается, это довольно глубокая область теории, но основные принципы просты.

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

Если у вас есть только небольшое пространство для хеширования, вы можете просто интерпретировать эти вещи как целые числа, и все готово (например, 4-байтовые строки)

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

Это означает, что вы не хотите, чтобы результат был таким же, и вам, вероятно, также очень хотелось бы, чтобы хеш-функция была быстрой.

Уравновешивание этих двух свойств (и некоторых других) заставило многих людей быть занятыми!

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

Теперь, чтобы заставить эту работу работать как хеш-таблица: представьте, что вас не заботит использование памяти. Затем вы можете создать массив до тех пор, пока ваш набор индексирования (например, все uint32). Когда вы добавляете что-то в таблицу, вы хешируете его ключ и смотрите на массив по этому индексу. Если там ничего нет, вы вкладываете туда свою ценность. Если там что-то уже есть, вы добавляете эту новую запись в список вещей по этому адресу вместе с достаточной информацией (ваш исходный ключ или что-то умное), чтобы определить, какая запись действительно принадлежит какому ключу.

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

Конечно, на практике вы обычно не можете этого сделать, это тратит слишком много памяти. Таким образом, вы делаете все на основе разреженного массива (где единственными записями являются те, которые вы действительно используете, все остальное неявно равно нулю).

Есть много схем и приемов, чтобы сделать эту работу лучше, но это основы.

person simon    schedule 08.04.2009
comment
Извините, я знаю, что это старый вопрос / ответ, но я пытался понять последнее, о чем вы говорите. Хеш-таблица имеет временную сложность O (1). Однако после того, как вы используете разреженный массив, разве вам не придется выполнять двоичный поиск, чтобы найти свое значение? В этот момент не становится ли временная сложность O (log n)? - person herbrandson; 28.04.2016
comment
@herbrandson: нет ... разреженный массив просто означает, что относительно небольшое количество индексов было заполнено значениями - вы все равно можете напрямую индексировать конкретный элемент массива для хеш-значения, которое вы рассчитали по вашему ключу; тем не менее, реализация разреженного массива, которую описывает Саймон, является разумной только в очень ограниченных обстоятельствах: когда размеры сегментов имеют порядок размеров страниц памяти (по сравнению, скажем, int ключей при разреженности 1 из 1000 и страницах 4k = большинство затронутых страниц), и когда ОС обрабатывает все нулевые страницы эффективно (поэтому все неиспользуемые страницы корзины не нуждаются в резервной памяти), когда адресного пространства много .... - person Tony Delroy; 02.05.2016
comment
@TonyDelroy - это правда, это чрезмерное упрощение, но идея заключалась в том, чтобы дать обзор того, что это такое и почему, а не практической реализации. Детали последнего более тонкие, если вы киваете в своем расширении. - person simon; 14.02.2019

Ответов много, но ни один из них не является очень наглядным, а хеш-таблицы могут легко щелкнуть при визуализации.

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

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], _4 _...) известна как корзина и запускает - возможно, пустой - связанный список значений (он же элементы, в этом примере - имена людей)
  • каждое значение (например, "fred" с хешем 42) связано с сегментом [hash % number_of_buckets], например 42 % 10 == [2]; % - это оператор по модулю - остаток при делении на количество ведра
  • multiple data values may collide at and be linked from the same bucket, most often because their hash values collide after the modulo operation (e.g. 42 % 10 == [2], and 9282 % 10 == [2]), but occasionally because the hash values are the same (e.g. "fred" and "jane" both shown with hash 42 above)
    • most hash tables handle collisions - with slightly reduced performance but no functional confusion - by comparing the full value (here text) of a value being sought or inserted to each value already in the linked list at the hashed-to bucket

Длина связанного списка зависит от коэффициента загрузки, а не от количества значений.

Если размер таблицы увеличивается, хеш-таблицы, реализованные, как указано выше, имеют тенденцию изменять размер (т. Е. Создавать больший массив сегментов, создавать новые / обновленные связанные списки на их основе, удалять старый массив), чтобы сохранить соотношение значений к сегментам (также известное как < em> коэффициент загрузки) где-то в диапазоне от 0,5 до 1,0.

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

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

Given a hash table implementation as described above, we can imagine creating a value type such as `struct Value { string name; int age; };`, and equality comparison and hash functions that only look at the `name` field (ignoring age), and then something wonderful happens: we can store `Value` records like `{"sue", 63}` in the table, then later search for "sue" without knowing her age, find the stored value and recover or even update her age - happy birthday Sue - which interestingly doesn't change the hash value so doesn't require that we move Sue's record to another bucket.

Когда мы это делаем, мы используем хеш-таблицу как ассоциативный контейнер, также известный как < em> map, и можно считать, что хранимые в нем значения состоят из ключа (имя) и одного или нескольких других полей, которые все еще называются - что сбивает с толку - значение (в моем примере только возраст). Реализация хеш-таблицы, используемая в качестве карты, известна как хеш-карта.

Это контрастирует с примером, приведенным ранее в этом ответе, где мы сохранили дискретные значения, такие как sue, которые вы могли бы рассматривать как собственный ключ: такой вид использования известен как хэш-набор.

Есть и другие способы реализовать хеш-таблицу

Не во всех хэш-таблицах используются связанные списки (известные как отдельные цепочки ), но большинство из них работают как основная альтернатива закрытое хеширование (также известное как < em> открытая адресация) - особенно с поддерживаемыми операциями стирания - имеет менее стабильные характеристики производительности с ключами / хэш-функциями, подверженными конфликтам.


Несколько слов о хэш-функциях

Сильное хеширование ...

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

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

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

Слабое, но часто быстрое хеширование ...

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

person Tony Delroy    schedule 01.06.2015
comment
Позвольте мне просто сказать: фантастический ответ. - person CRThaze; 06.01.2017
comment
@Tony Delroy Спасибо за прекрасный ответ. Однако у меня все еще есть один нерешенный вопрос. Вы говорите, что даже если существует 100 миллионов сегментов, время поиска будет O (1) с коэффициентом загрузки 1 и хеш-функцией криптографической стойкости. Но как насчет того, чтобы найти правильное ведро за 100 миллионов? Даже если мы отсортировали все сегменты, разве это не O (log100.000.000)? Как можно найти ведро за O (1)? - person selman; 21.12.2018
comment
@selman: в вашем вопросе нет подробностей, чтобы объяснить, почему вы думаете, что это может быть O (log100000000), но вы говорите, что даже если у нас есть отсортированные все сегменты - имейте в виду, что значения в сегментах хеш-таблицы < i> никогда не сортируется в обычном смысле: какое значение появляется в каком сегменте определяется путем применения хеш-функции к ключу. Думая, что сложность равна O (log100000000), вы представляете себе двоичный поиск по отсортированным корзинам, но хеширование работает не так. Возможно, прочтите несколько других ответов и посмотрите, станет ли это более понятным. - person Tony Delroy; 26.12.2018
comment
@TonyDelroy Действительно, сортированные корзины - это лучший сценарий, который я себе представляю. Следовательно, O (log100000000). Но если это не так, как приложение может найти связанную корзину среди миллионов? Хеш-функция каким-то образом генерирует место в памяти? - person selman; 27.12.2018
comment
@selman: да ... задача хеш-функции состоит в том, чтобы решить, к какому конкретному сегменту (вашей ячейке памяти) принадлежит данный ключ, а затем нужно искать только элементы, связанные с этим сегментом. В большинстве случаев можно доказать, что количество таких элементов - в среднем - небольшая константа, поэтому вставка, поиск и стирание амортизируются O (1). Хеш-функция обычно делает это, создавая число, которое может быть модулировано по модулю или AND-ed для формирования индекса в массиве сегментов. В случае коллизии используются различные вторичные методы хранения / поиска (в ответ: связанные списки). - person Tony Delroy; 27.12.2018
comment
@TonyDelroy Хорошо, но .... Вы говорите о формировании индекса в массиве корзин. Итак, если в этом массиве 100,00000 сегментов, даже с индексом, обращение к конкретному сегменту имеет стоимость, верно? Как может стоимость поиска быть O (1) в массиве из 100.00.000 сегментов? - person selman; 27.12.2018
comment
@selman: поскольку память компьютера допускает произвольный доступ в постоянное время: если вы можете вычислить адрес памяти, вы можете получить содержимое памяти, не обращаясь к памяти в других частях массива. Таким образом, независимо от того, получаете ли вы доступ к первому сегменту, последнему сегменту или сегменту где-нибудь между ними, он будет иметь одинаковые характеристики производительности (в общих чертах, это займет одинаковое количество времени, хотя и зависит от кэширования памяти ЦП L1 / L2 / L3, но они работают только для того, чтобы помочь вам быстро повторно получить доступ к недавно открывшимся или случайно соседним корзинам, и их можно игнорировать при анализе большого О). - person Tony Delroy; 28.12.2018
comment
Среднее, а также наиболее вероятное количество элементов в ведре должно быть коэффициентом загрузки. Почему вы говорите, что среднее значение равно 2 для коэффициента нагрузки 1? - person Hans; 19.01.2019
comment
Просто хочу добавить, что с коэффициентом загрузки a и n ведер каждое ведро имеет биномиальную вероятность {{an} \ choose k} 1 / n ^ k (1-1 / n) ^ {an-k} наличия k элементы. Вероятность приближается к распределению Пуассона e ^ {- a} a ^ k / k! когда n приближается к бесконечности, когда a и k остаются фиксированными. Это дает числовые процентные значения среднего количества элементов в каждом сегменте. - person Hans; 22.01.2019
comment
@Hans: когда вы ищете элементы, которые находятся в таблице, вы никогда не выполняете хеширование в пустое ведро: скорее, вы хешируете ведро, которое имеет хотя бы один связанный с ним элемент и, возможно, многие другие. Средняя длина этой цепочки больше, чем коэффициент загрузки, поскольку вы рассматриваете только непустые ведра, и если память обслуживает (я написал этот ответ давно), составляет 2 при коэффициенте загрузки 1. , независимо от количества ведер; важно то, что это позволяет повысить эффективность O (1), а не уменьшаться при увеличении N. Благодарим за упоминание фактической формулы как функции коэффициента нагрузки. - person Tony Delroy; 22.01.2019
comment
Рассуждения неверны. Среднее значение автоматически учитывает длину 0, поскольку 0 раз все равно 0 и фактически не учитывается. По определению, сумма всех длин сегментов - это общее количество элементов an, постоянное по всем возможностям. Разделите это на количество ковшей, чтобы получить среднюю длину ковша a, то есть коэффициент загрузки. Возможно, вы думаете об общем среднем времени выполнения, включая вычисление ключа, для которого требуется время O (1), и поиск в сегменте со временем, пропорциональным длине сегмента. В сумме получается O (1 + a). - person Hans; 22.01.2019
comment
Вернемся немного назад. Среднее, а также наиболее вероятное количество элементов в ковше должно быть коэффициентом загрузки - в равной степени наиболее точным является: ~ = 36,8% ковшей будут иметь длину цепи, соответствующую коэффициенту загрузки. , так как многие ведра будут пустыми. Тем не менее, это просто средние значения по всей таблице. Что гораздо важнее для стабильной производительности, отдельные цепочки столкновений в таблице по-прежнему имеют те же вероятности длины и среднюю длину при увеличении размера таблицы. - person Tony Delroy; 24.01.2019
comment
Если, скажем, более крупные таблицы (с тем же коэффициентом загрузки) будут иметь больше пустых сегментов, а большее количество цепочек столкновений будет длиннее, то свойства постоянного времени будут скомпрометированы. Мои комментарии / объяснения касаются именно этого аспекта, а не среднего значения для всей таблицы, которого недостаточно, чтобы гарантировать производительность O (1), если цепочки столкновений имеют тенденцию становиться длиннее с размером таблицы. - person Tony Delroy; 24.01.2019
comment
Тони, вы должны поставить @Hans перед своим комментарием, адресованным мне, иначе я не буду предупрежден. Я вижу только ваш комментарий, так как хочу добавить свой собственный. Я не понимаю, что в равной степени означает ваша фраза в кавычках. Вероятность длины приближается к распределению Пуассона, которое я дал, когда длина таблицы увеличивается до бесконечности. Вы согласны с тем, что средняя длина ковша равна коэффициенту загрузки? Это математический факт. Если это не то, что вы собираетесь заявить, было бы лучше отредактировать это утверждение в своем ответе. - person Hans; 27.01.2019
comment
Изначально я пришел, чтобы добавить следующий комментарий. Как я уже говорил ранее, средняя длина ковша по определению равна коэффициенту загрузки. Это количество не зависит от, следовательно, не отражает конкретного распределения хеш-функции, которая стремится приблизиться к равномерному распределению. Стандартное отклонение отражает дисперсию распределения. Это квадратный корень из коэффициента нагрузки. В этом особом случае, когда коэффициент нагрузки равен 1, 92% ковшей имеют длину 2 или меньше, 98% имеют длину 3 или меньше. Лучший способ увидеть это - построить кумулятивное распределение Пуассона. - person Hans; 27.01.2019
comment
@Hans: в равной степени - я просто говорю, что наличие количества элементов в ведре должно быть коэффициентом загрузки так же вероятно, как и наличие пустого ведра. В любом случае, я перефразировал то, что я хотел сообщить о длине цепочки в ответе, и исправил значение, которое я неправильно запомнил (~ 1,58, а не 2) - надеюсь, теперь оно однозначно. Спасибо также за дополнительные сведения о дистрибутивах stddev / Poisson - я думаю, кому-то это пригодится. Ваше здоровье - person Tony Delroy; 27.01.2019
comment
Вы можете произнести только фразу «количество элементов в сегменте, наиболее близкое к коэффициенту загрузки», поскольку коэффициент загрузки a является дробной частью, а длина сегмента - целым числом. Неверно, что количество элементов в ковше, наиболее близкое к коэффициенту нагрузки a, с равной вероятностью равно пустому, как вы можете понять из распределения Пуассона, где первое - e ^ {- a} a, а второе - e. ^ {- а}. Они равны только при a = 1. Я согласен с тем, что средняя длина ведра с учетом непустых ведер является лучшим показателем эффективности. Это a / (1-e ^ {- a}) и составляет приблизительно 1,58 для a = 1. - person Hans; 27.01.2019
comment
@Hans: Я подробно обсуждал коэффициент загрузки 1 для статистики в этом ответе, так что, похоже, мы на одной странице. - person Tony Delroy; 27.01.2019
comment
@TonyDelroy Я думал, что ключи также хранятся вместе со значениями, хешем и следующим указателем. Пожалуйста, поправьте, если я ошибаюсь. - person Prashant Shubham; 11.10.2019
comment
@PrashantShubham: хеш-таблица не обязательно должна иметь отдельные ключи и значения - см. (здесь). Хэш не нужно хранить: его можно регенерировать из ключа, и после поиска в ведре часто используются побайтовые сравнения ключей, но некоторые хеш-таблицы действительно хранят хеш, поэтому они могут: а) быстро различать хеш-значение коллизии и коллизии ведра (fred & jane vs bill в моем ответе), и б) не нужно снова вызывать хеш-функцию для каждого ключа при изменении размера хеш-таблицы. Недостатком является дополнительная память, необходимая для хранения хэшей. - person Tony Delroy; 23.02.2020

Вы, ребята, очень близки к тому, чтобы объяснить это полностью, но упускаете пару вещей. Хеш-таблица - это просто массив. Сам массив будет содержать что-то в каждом слоте. Как минимум, вы сохраните хэш-значение или само значение в этом слоте. В дополнение к этому вы также можете сохранить связанный / связанный список значений, которые столкнулись в этом слоте, или вы можете использовать метод открытой адресации. Вы также можете сохранить указатель или указатели на другие данные, которые хотите получить из этого слота.

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

uint slotIndex = hashValue % hashTableSize;

Это значение - это слот, в который будет вставлено значение. При открытой адресации, если слот уже заполнен другим хеш-значением и / или другими данными, операция модуля будет запущена еще раз, чтобы найти следующий слот:

slotIndex = (remainder + 1) % hashTableSize;

Я полагаю, что могут быть и другие более продвинутые методы определения индекса слота, но это самый распространенный метод, который я видел ... был бы заинтересован в любых других, которые работают лучше.

При использовании метода модуля, если у вас есть таблица размером, скажем, 1000, любое хеш-значение от 1 до 1000 попадет в соответствующий слот. Любые отрицательные значения и любые значения больше 1000 будут потенциально конфликтующими значениями слотов. Шансы на это зависят как от вашего метода хеширования, так и от того, сколько всего элементов вы добавляете в хеш-таблицу. Как правило, лучше всего делать размер хеш-таблицы таким, чтобы общее количество добавляемых к ней значений было равным примерно 70% от ее размера. Если ваша хеш-функция хорошо справляется с равномерным распределением, вы, как правило, будете сталкиваться с очень небольшим количеством или отсутствием коллизий между корзинами и слотами, и она будет работать очень быстро как для операций поиска, так и для операций записи. Если общее количество добавляемых значений заранее неизвестно, сделайте хорошее предположение, используя любые средства, а затем измените размер своей хеш-таблицы, как только количество добавленных к ней элементов достигнет 70% емкости.

Надеюсь, это помогло.

PS - В C # метод GetHashCode() довольно медленный и приводит к конфликтам фактических значений во многих тестируемых мной условиях. Для настоящего удовольствия создайте свою собственную хеш-функцию и постарайтесь, чтобы она НИКОГДА не сталкивалась с конкретными данными, которые вы хешируете, работала быстрее, чем GetHashCode, и имела довольно равномерное распределение. Я сделал это, используя длинные значения хэш-кода размера вместо int, и он неплохо работал с 32 миллионами хэш-значений entires в хеш-таблице с 0 коллизиями. К сожалению, я не могу поделиться кодом, поскольку он принадлежит моему работодателю ... но я могу сказать, что это возможно для определенных областей данных. Когда вы можете этого добиться, хеш-таблица будет ОЧЕНЬ быстрой. :)

person Chris    schedule 15.05.2010
comment
Я знаю, что сообщение довольно старое, но может ли кто-нибудь объяснить, что здесь означает (остаток + 1) - person Hari; 29.12.2014
comment
@Hari remainder относится к результату исходного вычисления по модулю, и мы добавляем к нему 1, чтобы найти следующий доступный слот. - person x4nd3r; 06.01.2015
comment
Сам массив будет содержать что-то в каждом слоте. Как минимум, вы сохраните хэш-значение или само значение в этом слоте. - обычно для слотов (сегментов) значение не сохраняется; реализации открытой адресации часто хранят либо NULL, либо указатель на первый узел в связанном списке - без значения непосредственно в слоте / сегменте. был бы заинтересован в любых других - показанный вами +1 называется линейным зондированием, часто более эффективным: квадратичным зондированием. как правило сталкиваются с очень небольшим количеством конфликтов сегмента / слот или их нет - @ 70% емкости, ~ 12% слотов с 2 значениями, ~ 3% 3 .... - person Tony Delroy; 02.05.2016
comment
Я сделал это, используя длинные значения хэш-кода размера вместо int, и он неплохо работал с 32 миллионами entires хэш-значений в хеш-таблице с 0 коллизиями. - это просто невозможно в общий случай, когда значения ключей фактически случайны в гораздо большем диапазоне, чем количество сегментов. Обратите внимание, что иметь различные хеш-значения часто достаточно просто (и ваш разговор о long хеш-значениях подразумевает, что вы достигли этого), но убедитесь, что они не конфликтуют в хеш-таблице после мода /% операции нет (в общем случае). - person Tony Delroy; 02.05.2016
comment
(Предотвращение всех коллизий известно как идеальное хеширование. В общем, это практично для нескольких сотен или тысяч ключей, которые известны заранее - gperf является примером инструмента для вычисления такой хеш-функции. Вы можете также напишите свои собственные в очень ограниченных обстоятельствах - например, если ваши ключи являются указателями на объекты из вашего собственного пула памяти, который поддерживается довольно заполненным, с каждым указателем на фиксированном расстоянии друг от друга, вы можете разделить указатели на это расстояние и эффективно иметь индекс на немного разреженный массив, избегающий коллизий.) - person Tony Delroy; 02.05.2016

Вот как это работает в моем понимании:

Вот пример: изобразите всю таблицу как серию корзин. Предположим, у вас есть реализация с буквенно-цифровыми хэш-кодами и одна корзина для каждой буквы алфавита. Эта реализация помещает каждый элемент, хэш-код которого начинается с определенной буквы, в соответствующий сегмент.

Допустим, у вас есть 200 объектов, но только 15 из них имеют хэш-коды, начинающиеся с буквы «B». В хэш-таблице нужно будет искать и перебирать только 15 объектов в корзине «B», а не все 200 объектов.

Что касается вычисления хеш-кода, то в этом нет ничего волшебного. Цель состоит в том, чтобы разные объекты возвращали разные коды, а для одинаковых объектов - одинаковые коды. Вы можете написать класс, который всегда будет возвращать одно и то же целое число в качестве хэш-кода для всех экземпляров, но вы по существу разрушите полезность хеш-таблицы, поскольку она просто станет одним гигантским ведром.

person AndreiM    schedule 08.04.2009

Коротко и мило:

Хеш-таблица завершает массив, давайте назовем его 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).

Когда наш internalArray становится слишком заполненным, возможно, примерно на 85% емкости, мы можем изменить размер внутреннего массива и переместить все элементы из старого массива в новый массив.

person Juliet    schedule 08.04.2009

Это даже проще, чем это.

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

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

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

Из-за этого у вас может быть несколько ключей, которые будут генерировать один и тот же хэш-код. Когда это происходит, элементы в векторе проходят итерацию, и выполняется прямое сравнение между ключом в векторе и ключом, который ищется. Если он найден, отлично и возвращается значение, связанное с ключом, в противном случае ничего не возвращается.

person casperOne    schedule 08.04.2009

Вы берете кучу вещей и массив.

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

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

Когда вы ищете что-то в хэше, вы выполняете те же шаги, вычисляя значение хэша, затем просматривая, что находится в корзине в этом месте, и проверяете, это то, что вы ищете.

Когда ваше хеширование работает хорошо и ваш массив достаточно велик, в любом конкретном индексе в массиве будет только несколько вещей, поэтому вам не придется много на что смотреть.

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

person chaos    schedule 08.04.2009
comment
спасибо за последний пункт, который все упустили из виду - person Sandeep Raju Prabhakar; 19.07.2016

Все ответы на данный момент хороши и касаются различных аспектов работы хеш-таблицы. Вот простой пример, который может оказаться полезным. Допустим, мы хотим сохранить некоторые элементы со строчными буквенными строками в качестве ключей.

Как объяснил Саймон, хеш-функция используется для преобразования большого пространства в маленькое. Простая, наивная реализация хэш-функции для нашего примера могла бы взять первую букву строки и сопоставить ее с целым числом, так что «аллигатор» имеет хэш-код 0, «пчела» имеет хеш-код 1, » зебра »будет 25 и т. д.

Затем у нас есть массив из 26 сегментов (может быть ArrayLists в Java), и мы помещаем в сегмент элемент, который соответствует хэш-коду нашего ключа. Если у нас есть несколько элементов, ключ которых начинается с одной и той же буквы, у них будет один и тот же хэш-код, поэтому все они будут помещены в корзину для этого хэш-кода, поэтому для найти конкретный предмет.

В нашем примере, если бы у нас было всего несколько десятков элементов с ключами, охватывающими алфавит, это сработало бы очень хорошо. Однако, если бы у нас был миллион элементов или все ключи начинались бы с «a» или «b», наша хеш-таблица не была бы идеальной. Чтобы повысить производительность, нам понадобится другая хеш-функция и / или больше корзин.

person Greg Graham    schedule 08.04.2009

Вот еще один способ взглянуть на это.

Я предполагаю, что вы понимаете концепцию массива A. Это то, что поддерживает операцию индексации, когда вы можете получить I-й элемент, A [I], за один шаг, независимо от того, насколько велик A.

Так, например, если вы хотите хранить информацию о группе людей разного возраста, самым простым способом было бы иметь достаточно большой массив и использовать возраст каждого человека в качестве индекса в массиве. Таким образом, у вас может быть одноэтапный доступ к информации любого человека.

Но, конечно, может быть более одного человека того же возраста, поэтому то, что вы помещаете в массив при каждой записи, представляет собой список всех людей этого возраста. Таким образом, вы можете получить информацию о конкретном человеке за один шаг плюс небольшой поиск по этому списку (так называемому «ведру»). Он замедляется, только если людей так много, что ведра становятся большими. Затем вам понадобится более крупный массив и какой-то другой способ получить больше идентифицирующей информации о человеке, например, первые несколько букв его фамилии, вместо использования возраста.

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

person Mike Dunlavey    schedule 08.04.2009

Хеш-таблица полностью работает на том факте, что практические вычисления следует модели машины с произвольным доступом, то есть к значению по любому адресу в памяти можно получить доступ за время O (1) или за постоянное время.

Итак, если у меня есть набор ключей (набор всех возможных ключей, которые я могу использовать в приложении, например, номер для студента, если это 4-значное число, то этот набор представляет собой набор чисел от 1 до 9999), и способ сопоставить их с конечным набором чисел размера, который я могу выделить в моей системе, теоретически моя хеш-таблица готова.

Как правило, в приложениях размер набора ключей очень велик, чем количество элементов, которые я хочу добавить в хеш-таблицу (я не хочу тратить 1 ГБ памяти на хэш, скажем, 10000 или 100000 целочисленных значений, потому что их 32 bit long в двоичном представлении). Итак, мы используем это хеширование. Это своего рода «математическая» операция смешивания, которая отображает мою большую вселенную на небольшой набор значений, которые я могу разместить в памяти. В практических случаях часто пространство хеш-таблицы имеет тот же «порядок» (большой-O), что и (количество элементов * размер каждого элемента). Таким образом, мы не тратим много памяти.

Теперь, когда большой набор сопоставлен с маленьким набором, сопоставление должно быть "многие к одному". Таким образом, разным ключам будет выделено одно и то же пространство (?? несправедливо). Есть несколько способов справиться с этим, я просто знаю два популярных из них:

  • Используйте пространство, которое должно было быть выделено для значения, как ссылку на связанный список. В этом связанном списке будет храниться одно или несколько значений, которые будут размещаться в одном слоте при сопоставлении "многие к одному". Связанный список также содержит ключи, которые помогут тем, кто приходит на поиски. Это как многие люди в одной квартире, когда приходит курьер, он идет в комнату и спрашивает именно этого парня.
  • Используйте двойную хеш-функцию в массиве, который каждый раз дает одну и ту же последовательность значений, а не одно значение. Когда я иду сохранить значение, я вижу, свободна или занята требуемая ячейка памяти. Если это бесплатно, я могу сохранить там свое значение, если оно занято, я беру следующее значение из последовательности и так далее, пока не найду свободное место и не сохраню там свое значение. При поиске или получении значения я возвращаюсь по тому же пути, который задан последовательностью, и в каждом месте запрашиваю значение, если оно есть, пока я не найду его или не буду искать все возможные места в массиве.

Введение в алгоритмы от CLRS дает очень хорошее представление об этой теме.

person div    schedule 12.06.2015

Как вычисляется хэш, обычно не зависит от хеш-таблицы, а от элементов, добавленных в нее. В фреймворках / библиотеках базовых классов, таких как .net и Java, каждый объект имеет метод GetHashCode () (или аналогичный), возвращающий хэш-код для этого объекта. Идеальный алгоритм хэш-кода и точная реализация зависят от данных, представленных в объекте.

person Lucero    schedule 08.04.2009

Основная мысль

Почему люди используют комоды для хранения своей одежды? Помимо того, что они выглядят модно и стильно, у них есть то преимущество, что у каждого предмета одежды есть место, где он должен быть. Если вы ищете пару носков, просто загляните в ящик для носков. Если вы ищете рубашку, проверьте ящик, в котором лежат ваши рубашки. Когда вы ищете носки, не имеет значения, сколько у вас рубашек или пар брюк, потому что вам не нужно на них смотреть. Вы просто заглядываете в ящик для носков и ожидаете найти там носки.

На высоком уровне хеш-таблица - это способ хранения вещей, который (вроде как) похож на комод для одежды. Основная идея заключается в следующем:

  • Вы получаете некоторое количество мест (ящиков), где можно хранить предметы.
  • Вы придумываете какое-то правило, которое сообщает вам, к какому месту (ящику) принадлежит каждый предмет.
  • Когда вам нужно что-то найти, вы используете это правило, чтобы определить, в какой ящик искать.

Преимущество такой системы в том, что, если ваше правило не слишком сложное и у вас есть соответствующее количество ящиков, вы можете довольно быстро найти то, что ищете, просто заглянув в нужное место.

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

Разумный способ думать о хэш-функции - это черный ящик. Вы помещаете данные в одну сторону, а число, называемое хеш-кодом, выходит из другой. Схематично это выглядит примерно так:

              +---------+
            |\|   hash  |/| --> hash code
   data --> |/| function|\|
              +---------+

Все хэш-функции являются детерминированными: если вы поместите одни и те же данные в функцию несколько раз, вы всегда получите одно и то же значение на другой стороне. И хорошая хеш-функция должна выглядеть более или менее случайной: небольшие изменения входных данных должны давать совершенно разные хеш-коды. Например, хэш-коды для строки pudu и для строки kudu, скорее всего, будут сильно отличаться друг от друга. (Опять же, возможно, что они такие же. В конце концов, если выходные данные хеш-функции должны выглядеть более или менее случайными, есть шанс, что мы получим один и тот же хеш-код дважды.)

Как именно построить хеш-функцию? А пока пойдем с порядочными людьми, не стоит об этом слишком много думать. Математики разрабатывали все лучшие и худшие способы разработки хэш-функций, но для наших целей нам действительно не нужно слишком беспокоиться о внутреннем устройстве. Достаточно хорошо думать о хэш-функции как о функции, которая

  • детерминированный (равные входы дают равные выходы), но
  • выглядит случайным (трудно предсказать один хэш-код по другому).

Когда у нас есть хеш-функция, мы можем построить очень простую хеш-таблицу. Мы сделаем ряд ведер, которые можно рассматривать как аналог ящиков в нашем комоде. Чтобы сохранить элемент в хэш-таблице, мы вычислим хэш-код объекта и будем использовать его в качестве индекса в таблице, что аналогично выбору, в какой ящик помещается этот элемент. Затем мы помещаем этот элемент данных в ведро с этим индексом. Если это ведро было пустым, отлично! Мы можем положить туда предмет. Если это ведро заполнено, у нас есть несколько вариантов того, что мы можем сделать. Простой подход (называемый цепное хеширование < / a>) заключается в том, чтобы рассматривать каждое ведро как список элементов, так же, как ваш ящик для носков может хранить несколько носков, а затем просто добавлять элемент в список по этому индексу.

Чтобы найти что-то в хеш-таблице, мы используем в основном ту же процедуру. Мы начинаем с вычисления хэш-кода для элемента, который нужно найти, который сообщает нам, в каком сегменте (ящике) искать. Если элемент находится в таблице, он должен быть в этом сегменте. Затем мы просто смотрим на все предметы в корзине и проверяем, есть ли там наш предмет.

В чем преимущество этого? Что ж, предполагая, что у нас есть большое количество корзин, мы ожидаем, что в большинстве корзин не будет слишком много вещей. В конце концов, наша хеш-функция вроде как выглядит так, будто имеет случайные выходы, поэтому элементы распределяются вроде как равномерно по всем сегментам. Фактически, если мы формализуем понятие нашей хэш-функции, которое выглядит как бы случайным, мы можем доказать, что ожидаемое количество элементов в каждом сегменте является отношением общего количества элементов к общему количеству сегментов. Таким образом, мы можем найти то, что ищем, без лишних усилий.

Детали

Объяснить, как работает хеш-таблица, немного сложно, потому что существует множество разновидностей хеш-таблиц. В следующем разделе рассказывается о некоторых общих деталях реализации, общих для всех хэш-таблиц, а также о некоторых особенностях работы различных стилей хэш-таблиц.

Первый вопрос, который возникает, - как превратить хэш-код в индекс слота таблицы. В приведенном выше обсуждении я только что сказал использовать хэш-код в качестве индекса, но на самом деле это не очень хорошая идея. В большинстве языков программирования хэш-коды работают с 32-битными или 64-битными целыми числами, и вы не сможете использовать их напрямую в качестве индексов корзины. Вместо этого общая стратегия состоит в том, чтобы создать массив сегментов некоторого размера m, вычислить (полные 32- или 64-битные) хэш-коды для ваших элементов, а затем изменить их по размеру таблицы, чтобы получить индекс от 0 до м-1 включительно. Использование модуля здесь хорошо работает, потому что оно достаточно быстрое и выполняет достойную работу, распределяя полный диапазон хэш-кодов на меньший диапазон.

(Иногда здесь используются побитовые операторы. Если размер вашей таблицы равен степени двойки, скажем, 2 k, то вычисляется побитовое И хэш-кода, а затем число 2 k - 1 эквивалентно вычислению модуля, и это значительно быстрее.)

Следующий вопрос - как правильно выбрать количество ведер. Если вы выберете слишком много ведер, то большинство ведер будет пустым или будет содержать мало элементов (хорошо для скорости - вам нужно всего лишь проверить несколько элементов на ведро), но вы будете использовать кучу места, просто храня ведра (не так отлично, хотя, может быть, вы можете себе это позволить). Верна и обратная сторона этого утверждения: если у вас слишком мало сегментов, то в среднем у вас будет больше элементов на каждый сегмент, поэтому поиск займет больше времени, но вы будете использовать меньше памяти.

Хорошим компромиссом является динамическое изменение количества сегментов в течение времени существования хеш-таблицы. Коэффициент загрузки хеш-таблицы, обычно обозначаемый α, представляет собой отношение количества элементов к количеству сегментов. Большинство хеш-таблиц выбирают некоторый максимальный коэффициент загрузки. Как только коэффициент загрузки превышает этот предел, хеш-таблица увеличивает количество слотов (скажем, удваивая), а затем перераспределяет элементы из старой таблицы в новую. Это называется перефразированием. Предполагая, что максимальный коэффициент загрузки в таблице является постоянным, это гарантирует, что при условии, что у вас есть хорошая хеш-функция, ожидаемые затраты на выполнение поиска останутся равными O (1). Теперь вставки имеют амортизированную ожидаемую стоимость O (1) из-за затрат на периодическое восстановление таблицы, как в случае с удалениями. (Удаление может аналогичным образом сжать таблицу, если коэффициент нагрузки станет слишком мал.)

Стратегии хеширования

До этого момента мы говорили о цепном хешировании, которое является одной из многих различных стратегий построения хеш-таблицы. Напоминаем, что цепное хеширование вроде как выглядит как шкафчик для одежды - каждое ведро (ящик) может содержать несколько элементов, и когда вы выполняете поиск, вы проверяете все эти элементы.

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

При открытой адресации, когда вы выполняете вставку, как и раньше, вы переходите к некоторому слоту, индекс которого зависит от вычисленного хэш-кода. Если этот слот бесплатный - отлично! Вы кладете туда предмет, и все готово. Но что, если слот уже заполнен? В этом случае вы используете некоторую вторичную стратегию, чтобы найти другой свободный слот для хранения предмета. Наиболее распространенная стратегия для этого использует подход под названием линейное зондирование < / em>. При линейном зондировании, если желаемый слот уже заполнен, вы просто переходите к следующему слоту в таблице. Если этот слот пуст - отлично! Вы можете положить туда предмет. Но если этот слот заполнен, вы затем переходите к следующему слоту в таблице и т. Д. (Если вы попали в конец таблицы, просто вернитесь к началу).

Линейное зондирование - удивительно быстрый способ построения хеш-таблицы. Кеши ЦП оптимизированы для местоположения ссылки , поэтому поиск в памяти в соседних ячейках памяти обычно выполняется намного быстрее, чем поиск в памяти в разрозненных местах. Поскольку вставка или удаление с линейным зондированием работает путем попадания в какой-либо слот массива, а затем линейного движения вперед, это приводит к небольшому количеству промахов в кеше и в конечном итоге оказывается намного быстрее, чем обычно предсказывает теория. (И бывает так, что теория предсказывает, что это будет очень быстро!)

Еще одна стратегия, которая стала популярной в последнее время, - кукушечное хеширование < / а>. Мне нравится думать о хешировании с кукушкой как о замороженных хэш-таблицах. Вместо одной хеш-таблицы и одной хеш-функции у нас есть две хеш-таблицы и две хеш-функции. Каждый элемент может находиться ровно в одном из двух мест - либо в месте в первой таблице, заданном первой хеш-функцией, либо в месте во второй таблице, заданном второй хеш-функцией. Это означает, что поиск наихудший эффективен, поскольку вам нужно проверить только два места, чтобы увидеть, есть ли что-то в таблице.

Вставки при хешировании с кукушкой используют другую стратегию, чем раньше. Мы начинаем с того, что проверяем, свободны ли какие-либо из двух слотов, в которых может быть помещен предмет. Если да, то отлично! Мы просто кладем туда предмет. Но если это не сработает, мы выбираем один из слотов, помещаем туда предмет и выкидываем предмет, который там был раньше. Этот элемент должен куда-то пойти, поэтому мы пытаемся поместить его в другую таблицу в соответствующий слот. Если это сработает - отлично! Если нет, мы выкидываем элемент из этой таблицы и пытаемся вставить его в другую таблицу. Этот процесс продолжается до тех пор, пока все не остановится или мы не окажемся в ловушке цикла. (Последний случай встречается редко, и если это произойдет, у нас есть множество вариантов, например, поместить его во вторичную хеш-таблицу или выбрать новые хеш-функции и перестроить таблицы.)

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

Тогда есть гибридные подходы. Хеширование Hopscotch представляет собой сочетание открытая адресация и цепное хеширование, которые можно представить как взятие связанной хеш-таблицы и сохранение каждого элемента в каждом сегменте в слоте рядом с тем, куда элемент хочет попасть. Эта стратегия хорошо работает с многопоточностью. Швейцарская таблица использует факт что некоторые процессоры могут выполнять несколько операций параллельно с одной инструкцией для ускорения таблицы линейного зондирования. Расширяемое хеширование предназначено для баз данных. и файловые системы и использует сочетание дерева и связанной хеш-таблицы для динамического увеличения размеров сегментов по мере загрузки отдельных сегментов. <сильный > Хеширование Робин Гуда - это вариант линейного исследования, при котором элементы можно перемещать после вставки, чтобы уменьшить разницу в том, как далеко от дома может находиться каждый элемент.

Дальнейшее чтение

Дополнительные сведения об основах хэш-таблиц см. На странице эти слайды с лекцией о цепном хешировании и эти последующие слайды о линейном зондировании и хешировании Робин Гуда . Вы можете узнать больше о здесь хеширование кукушки и примерно здесь теоретические свойства хеш-функций.

person templatetypedef    schedule 11.09.2020

Вот как это работает для всех, кто ищет язык программирования. Внутренняя реализация расширенных хэш-таблиц имеет множество тонкостей и оптимизаций для выделения / освобождения памяти и поиска, но идея верхнего уровня будет почти такой же.

(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 как индекс массива, но, возможно, это также место в памяти.

person Nirav Bhatt    schedule 07.10.2015
comment
эмпирическое правило: для вставки заданного значения сегмент должен быть УНИКАЛЬНЫМ И ПРОИЗВОДИМЫМ ИЗ ЗНАЧЕНИЯ, которое предполагается СОХРАНИТЬ. - это описывает идеальная хеш-функция, которая обычно возможна только для нескольких сотен или тысяч значений, известных во время компиляции. Большинству хэш-таблиц приходится обрабатывать коллизии. Кроме того, хеш-таблицы обычно выделяют место для всех сегментов, независимо от того, пусты они или нет, тогда как ваш псевдокод документирует шаг create_extra_space_for_bucket() во время вставки новых ключей. Однако ведра могут быть указателями. - person Tony Delroy; 31.08.2017

Таблица прямых адресов

Чтобы понять хеш-таблицу, нам следует понять таблицу прямых адресов. таблица прямых адресов

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

Однако перед реализацией таблицы прямых адресов следует учесть четыре соображения:

  1. Чтобы быть допустимым индексом массива, ключи должны быть целыми числами
  2. Вселенная ключей довольно мала, иначе нам понадобится гигантский массив.
  3. Не два разных ключа отображаются в один и тот же слот в массиве.
  4. Длина ключей юниверса равна длине массива

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

Хеш-таблица

Вместо прямого использования ключа хеш-таблица сначала применяет математическую хеш-функцию для последовательного преобразования любых произвольных данных ключа в число, а затем использует этот результат хеширования в качестве ключа.

Длина ключей юниверса может быть больше, чем длина массива, что означает, что два разных ключа могут быть хешированы по одному и тому же индексу (это называется хеш-коллизией)?

На самом деле, есть несколько разных стратегий, чтобы справиться с этим. Вот обычное решение: вместо хранения фактических значений в массиве мы сохраняем указатель на связанный список, содержащий значения для всех ключей, хеширующих этот индекс.

Если вам все еще интересно узнать, как реализовать хэш-карту с нуля, прочтите следующий пост

введите описание изображения здесь

person Jerry An    schedule 10.04.2021