Почему хэш-функции должны использовать модуль простых чисел? - PullRequest
318 голосов
/ 17 июля 2009

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

Что вы ожидаете от книги за 1,25 доллара?

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

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

Ответы [ 13 ]

232 голосов
/ 18 июля 2009

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

(first char) + k * (second char) + k^2 * (third char) + ...

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

[Например, строковый хэш-код Java очень похож на этот - он выполняет обратный порядок символов с k = 31. Таким образом, вы получаете поразительные отношения по модулю 31 между строками, которые заканчиваются одинаково, и поразительные отношения по модулю 2 ^ 32 между строками, которые одинаковы, за исключением конца. Это серьезно не портит хеш-таблицу поведения.]

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

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

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

Оказывается, что "из-за природы математики", если константа, используемая в хэше, и число сегментов равны взаимно просты , то столкновения минимизируются в некоторых распространенных случаях. Если они не являются взаимно простыми , то существуют некоторые довольно простые отношения между входами, для которых коллизии не минимизируются. Все хэши получаются равными по модулю общего множителя, что означает, что все они попадут в 1 / n-ую ячейку, которая имеет это значение по модулю общего множителя. Вы получаете в n раз больше столкновений, где n является общим фактором. Поскольку n равно по крайней мере 2, я бы сказал, что для довольно простого варианта использования неприемлемо генерировать как минимум вдвое больше коллизий, чем обычно. Если какой-то пользователь собирается разбить наш дистрибутив на сегменты, мы хотим, чтобы это был странный случай, а не простое предсказуемое использование.

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

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

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

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

[Редактировать: есть еще одна, более специализированная причина использовать простое число сегментов, то есть если вы обрабатываете столкновения с линейным зондированием. Затем вы вычисляете шаг по хеш-коду, и если этот шаг становится фактором подсчета сегментов, вы можете только выполнить (bucket_count / stride) зонды, прежде чем вернетесь к тому, с чего начали. Конечно, вам больше всего нужно избегать: stride = 0, что, конечно, должно быть в специальном регистре, но чтобы избежать также специального случая, когда bucket_count / stride равен маленькому целому числу, вы можете просто сделать простое число bucket_count и не заботиться о том, что при условии, что это не 0.]

28 голосов
/ 23 сентября 2009

Первое, что вы делаете при вставке / извлечении из хеш-таблицы, это вычисление hashCode для данного ключа, а затем поиск правильного сегмента путем обрезки hashCode до размера hashTable с помощью hashCode% table_length. Вот 2 «утверждения», которые вы, скорее всего, где-то читали

  1. Если вы используете степень 2 для длины таблицы, поиск (hashCode (ключ)% 2 ^ n) так же прост и быстр, как (hashCode (ключ) & (2 ^ n -1)). Но если ваша функция для вычисления hashCode для данного ключа не годится, вы определенно пострадаете от кластеризации многих ключей в несколько блоков хеша.
  2. Но если вы используете простые числа для table_length, вычисленные хэш-коды могут отображаться в различные хэш-блоки, даже если у вас есть немного глупая функция hashCode.

А вот и доказательство.

Если предположить, что ваша функция hashCode приводит к следующим хеш-кодам среди прочих {x, 2x, 3x, 4x, 5x, 6x ...}, то все они будут сгруппированы всего за m блоков, где m = table_length / GreatestCommonFactor (table_length, x). (Это тривиально проверить / вывести это). Теперь вы можете выполнить одно из следующих действий, чтобы избежать кластеризации

Убедитесь, что вы не генерируете слишком много хеш-кодов, кратных другому хеш-коду, как в {x, 2x, 3x, 4x, 5x, 6x ...}. Но это может быть довольно сложно, если ваш hashTable должен иметь миллионы записей. Или просто сделайте m равным table_length, сделав GreatestCommonFactor (table_length, x) равным 1, т. Е. Сделав table_length взаимно простым с x. И если x может быть почти любым числом, то убедитесь, что table_length является простым числом.

С - http://srinvis.blogspot.com/2006/07/hash-table-lengths-and-prime-numbers.html

10 голосов
/ 17 июля 2009

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

Довольно четкое объяснение, тоже с картинками.

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

Лучший вопрос был бы: почему именно число 31?

9 голосов
/ 06 ноября 2012

ТЛ; др

index[hash(input)%2] приведет к коллизии для половины всех возможных хешей и диапазона значений. index[hash(input)%prime] приводит к коллизии <2 всех возможных хешей. Прикрепление делителя к размеру таблицы также гарантирует, что число не может быть больше таблицы. </p>

8 голосов
/ 26 ноября 2013

Простые числа используются, потому что у вас есть хорошие шансы получить уникальное значение для типичной хеш-функции, которая использует полиномы по модулю P. Скажем, вы используете такую ​​хеш-функцию для строк длиной <= N, и у вас есть коллизия. Это означает, что 2 разных многочлена производят одно и то же значение по модулю P. Разница этих многочленов снова является многочленом одинаковой степени N (или меньше). Он имеет не более N корней (именно здесь проявляется математика, поскольку это утверждение верно только для полинома над полем => простое число). Так что, если N намного меньше, чем P, вы, скорее всего, не столкнетесь. После этого эксперимент, вероятно, может показать, что значение 37 достаточно велико, чтобы избежать коллизий для хеш-таблицы строк длиной 5–10, и достаточно мало для использования в вычислениях.

5 голосов
/ 17 июля 2009

Просто чтобы предоставить альтернативную точку зрения, есть этот сайт:

http://www.codexon.com/posts/hash-functions-the-modulo-prime-myth

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

3 голосов
/ 18 июля 2009

Зависит от выбора хеш-функции.

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

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

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

3 голосов
/ 17 июля 2009

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

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

Однако использование простых чисел является старым техника. Ключ здесь, чтобы понять что до тех пор, пока вы можете генерировать достаточно уникальный ключ, который вы можете переместить к другим методам хеширования тоже. Идти здесь больше на эту тему о http://www.azillionmonkeys.com/qed/hash.html

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

2 голосов
/ 30 марта 2017

Копирование из моего другого ответа https://stackoverflow.com/a/43126969/917428. См. Подробности и примеры.

Я считаю, что это связано с тем, что компьютеры работают с базой 2. Просто подумайте, как работает то же самое с базой 10:

  • 8% 10 = 8
  • 18% 10 = 8
  • 87865378% 10 = 8

Неважно, что это за число: до тех пор, пока оно оканчивается на 8, его модуль 10 будет равен 8.

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

2 голосов
/ 06 сентября 2016

Предположим, ваш размер таблицы (или число по модулю) равен T = (B * C). Теперь, если хэш для вашего ввода подобен (N * A * B), где N может быть любым целым числом, то ваш вывод не будет хорошо распределен. Поскольку каждый раз, когда n становится C, 2C, 3C и т. Д., Ваши выходные данные будут повторяться. т. е. ваш вывод будет распространяться только по позициям C. Обратите внимание, что здесь C (T / HCF (размер таблицы, хэш)).

Эту проблему можно устранить, сделав HCF 1. Простые числа очень хороши для этого.

Еще одна интересная вещь, когда Т 2 ^ N. Они дадут вывод точно так же, как и все младшие N битов входного хэша. Поскольку каждое число может быть представлено степенью 2, когда мы возьмем по модулю любое число с T, мы вычтем все степени числа 2 из числа, которые являются> = N, следовательно, всегда выделяя номер конкретного шаблона, в зависимости от ввода , Это тоже плохой выбор.

Точно так же T как 10 ^ N также является плохим из-за похожих причин (шаблон в десятичной записи чисел вместо двоичного).

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

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