хеш-функция для строки


124

Я работаю над хеш-таблицей на языке C и тестирую хеш-функцию для строки.

Первая функция, которую я пробовал, - это добавить код ascii и использовать по модулю (% 100), но у меня плохие результаты с первым тестом данных: 40 столкновений для 130 слов.

Итоговые входные данные будут содержать 8 000 слов (словарь хранится в файле). Хеш-таблица объявлена ​​как int table [10000] и содержит позицию слова в текстовом файле.

Первый вопрос: какой алгоритм хеширования строки лучше? а как определить размер хеш-таблицы?

заранее спасибо !

:-)


11
Если в вашей хеш-таблице 10К записей, зачем использовать по модулю 100? Получение 40 коллизий из 130 слов неудивительно с таким маленьким модулем.
Кэри Грегори

13
См. Burtleburtle.net/bob/hash/evahash.html и partow.net/programming/hashfunctions, где есть ресурсы о различных хешированиях (от общего до строкового и криптографического).

4
Чтобы прояснить @CareyGregory: вы понимаете, что, как основная математическая истина, 130 элементов в 100 сегментах (то есть мод 100) должны вызывать 30 столкновений (где столкновение считается как каждый раз, когда вставляется второй, третий и т. Д. Элемент ведро), правильно? Так что вы только немного выше этого.
derobert 05

4
@lilawood: Хорошо, я так и подумал, но для лучшего теста вы должны использовать 80 слов с хеш-таблицей из 100 записей. Это даст вам те же пропорции, что и ваши живые данные, и не вызовет коллизий.
Кэри Грегори

Ответы:


186

У меня были хорошие результаты с djb2Дэном Бернштейном.

unsigned long
hash(unsigned char *str)
{
    unsigned long hash = 5381;
    int c;

    while (c = *str++)
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

    return hash;
}

37
страница, на которую ссылается ответ, очень интересна.
Адриан Плиссон

2
как программа выходит из цикла while ?? = S
Дэниел Н.

1
@ danfly09 Когда c равно нулю. Эквивалент while (c = * str ++) будет (0! = (C = * str ++))
rxantos

5
@Josepas, хеш-функция в идеале должна возвращать size_tбеззнаковое или другое подобное значение (например, беззнаковое длинное число в этом коде). Абонент несет ответственность за принятие по модулю результата , чтобы вписать его в хэш - таблице. Вызывающий контролирует слот таблицы, в который хешируется; не функция. Он просто возвращает какое-то беззнаковое число.
WhozCraig

6
удивительный. этот алгоритм выбил из хэшей Murmur, варианты FNV и многие другие! +1
Дэвид Хаим

24

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

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

Например, предполагая общий случай 8-битных байтов, вы можете повернуть на 5 бит:

int hash(char const *input) { 
    int result = 0x55555555;

    while (*input) { 
        result ^= *input++;
        result = rol(result, 5);
    }
}

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


Это не c, но мне было бы интересно узнать, что вы думаете по этому
поводу

1
@Suragch: С тех пор, как я написал это, довольно много процессоров начали включать специальное оборудование для ускорения вычислений SHA, что сделало его гораздо более конкурентоспособным. Тем не менее, я сомневаюсь, что ваш код так же безопасен, как вы думаете - например, числа с плавающей запятой IEEE имеют два разных битовых шаблона (0 и -0), которые должны давать одинаковые хэши (они будут сравниваться как равные друг другу ).
Джерри Коффин,

@Jerry Coffin, какая библиотека мне нужна для функции rol ()?
thanos.a

@ thanos.a: Я не знаю, есть ли он в библиотеке, но для раскрутки собственного кода требуется всего пара строк. Сдвиньте один кусок влево, другой вправо и / или их вместе.
Джерри Гроб,

8

Википедия показывает красивую строковую хеш-функцию под названием Jenkins One At A Time Hash. Он также цитирует улучшенные версии этого хеша.

uint32_t jenkins_one_at_a_time_hash(char *key, size_t len)
{
    uint32_t hash, i;
    for(hash = i = 0; i < len; ++i)
    {
        hash += key[i];
        hash += (hash << 10);
        hash ^= (hash >> 6);
    }
    hash += (hash << 3);
    hash ^= (hash >> 11);
    hash += (hash << 15);
    return hash;
}

8

Существует ряд реализаций хэш-таблиц для C, от стандартной библиотеки C hcreate / hdestroy / hsearch до тех, что находятся в APR и glib , которые также предоставляют предварительно созданные хэш-функции. Я настоятельно рекомендую использовать их, а не изобретать свою собственную хеш-таблицу или хеш-функцию; они были сильно оптимизированы для обычных случаев использования.

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


hsearch ищет, сравнивая строки или адрес строки ptr? Думаю, это просто проверка адреса птр? Я пробовал использовать разные указатели, но одну и ту же строку. hsearch не сообщает, что элементы не найдены
mk ..

3

djb2 ​​имеет 317 коллизий для этого 466k английского словаря, в то время как MurmurHash не имеет ни одного для 64-битных хэшей и 21 для 32-битных хэшей (около 25 следует ожидать для 466k случайных 32-битных хэшей). Я рекомендую использовать MurmurHash, если он доступен, это очень быстро, потому что занимает несколько байтов за раз. Но если вам нужна простая и короткая хеш-функция для копирования и вставки в ваш проект, я бы рекомендовал использовать пошаговую версию по одному байту:

uint32_t inline MurmurOAAT32 ( const char * key)
{
  uint32_t h(3323198485ul);
  for (;*key;++key) {
    h ^= *key;
    h *= 0x5bd1e995;
    h ^= h >> 15;
  }
  return h;
}

uint64_t inline MurmurOAAT64 ( const char * key)
{
  uint64_t h(525201411107845655ull);
  for (;*key;++key) {
    h ^= *key;
    h *= 0x5bd1e9955bd1e995;
    h ^= h >> 47;
  }
  return h;
}

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


2

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

Хеш-функция с хорошей репутацией - MurmurHash3 .

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


1
Ожидаемое количество хеш-коллизий составляет n - m * (1 - ((m-1)/m)^n) = 57.075.... 40 столкновений лучше, чем можно было ожидать случайно (от 46 до 70 при p-балле 0,999). Рассматриваемая хеш-функция более однородна, чем если бы она была случайной или мы наблюдаем очень редкое событие.
Вольфганг Брем,

2

Хотя то djb2, что представлено на stackoverflow от cnicutar , почти наверняка лучше, я думаю, стоит также показать хеши K&R :

1) По-видимому, ужасный алгоритм хеширования, представленный в 1-м издании K&R ( источник )

unsigned long hash(unsigned char *str)
{
    unsigned int hash = 0;
    int c;

    while (c = *str++)
        hash += c;

    return hash;
}

2) Вероятно, довольно приличный алгоритм хеширования, представленный в версии 2 K&R (проверено мной на стр. 144 книги); NB: не забудьте удалить % HASHSIZEиз оператора return, если вы планируете выполнять изменение размера модуля до длины вашего массива вне алгоритма хеширования. Также я рекомендую вам использовать тип return и hashval unsigned longвместо простого unsigned(int).

unsigned hash(char *s)
{
    unsigned hashval;

    for (hashval = 0; *s != '\0'; s++)
        hashval = *s + 31*hashval;
    return hashval % HASHSIZE;
}

Обратите внимание, что из двух алгоритмов ясно, что одна из причин, по которой хеш 1-го издания настолько ужасен, заключается в том, что он НЕ принимает во внимание порядок строковых символов , hash("ab")поэтому возвращает то же значение, что и hash("ba"). Однако это не так с хешем 2-го издания, который (намного лучше!) Возвращает два разных значения для этих строк.

Функции хеширования GCC C ++ 11, используемые для unordered_map(шаблона хеш-таблицы) и unordered_set(шаблона хеш-набора), выглядят следующим образом.

  • Это частичный ответ на вопрос о том, какие хэш-функции GCC C ++ 11 используются , в котором говорится, что GCC использует реализацию "MurmurHashUnaligned2" Остина Эпплби ( http://murmurhash.googlepages.com/ ).
  • В файле "gcc / libstdc ++ - v3 / libsupc ++ / hash_bytes.cc", здесь ( https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/libsupc++/hash_bytes.cc ), я обнаружил реализации. Вот, например, для возвращаемого значения "32-bit size_t" (извлечено 11 августа 2017 г.):

Код:

// Implementation of Murmur hash for 32-bit size_t.
size_t _Hash_bytes(const void* ptr, size_t len, size_t seed)
{
  const size_t m = 0x5bd1e995;
  size_t hash = seed ^ len;
  const char* buf = static_cast<const char*>(ptr);

  // Mix 4 bytes at a time into the hash.
  while (len >= 4)
  {
    size_t k = unaligned_load(buf);
    k *= m;
    k ^= k >> 24;
    k *= m;
    hash *= m;
    hash ^= k;
    buf += 4;
    len -= 4;
  }

  // Handle the last few bytes of the input array.
  switch (len)
  {
    case 3:
      hash ^= static_cast<unsigned char>(buf[2]) << 16;
      [[gnu::fallthrough]];
    case 2:
      hash ^= static_cast<unsigned char>(buf[1]) << 8;
      [[gnu::fallthrough]];
    case 1:
      hash ^= static_cast<unsigned char>(buf[0]);
      hash *= m;
  };

  // Do a few final mixes of the hash.
  hash ^= hash >> 13;
  hash *= m;
  hash ^= hash >> 15;
  return hash;
}

2

Я пробовал эти хеш-функции и получил следующий результат. У меня около 960 ^ 3 записей, каждая длиной 64 байта, 64 символа в разном порядке, хэш-значение 32 бит. Коды отсюда .

Hash function    | collision rate | how many minutes to finish
==============================================================
MurmurHash3      |           6.?% |                      4m15s
Jenkins One..    |           6.1% |                      6m54s   
Bob, 1st in link |          6.16% |                      5m34s
SuperFastHash    |            10% |                      4m58s
bernstein        |            20% |       14s only finish 1/20
one_at_a_time    |          6.16% |                       7m5s
crc              |          6.16% |                      7m56s

Странно то, что почти все хеш-функции имеют 6% -ную частоту конфликтов для моих данных.


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

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

Ожидаемое количество коллизий должно быть 9,112499989700318E + 7 или 0,103 * 960³, если хэши действительно случайные, поэтому я не удивился бы, если бы они были примерно равны этому значению, но 0,0616 * 960³ кажется немного неуместным, почти как если бы хэши распределяются более равномерно, чем можно было бы ожидать случайно, и при длине 64 байта этот предел обязательно должен быть достигнут. Можете ли вы поделиться набором хешированных строк, чтобы я мог попытаться воспроизвести его?
Вольфганг Брем

0

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

Вы предварительно вычисляете таблицу T со случайным числом для каждого символа в алфавите вашего ключа [0,255]. Вы хешируете свой ключ 'k0 k1 k2 ... kN', взяв T [k0] xor T [k1] xor ... xor T [kN]. Вы можете легко показать, что это так же случайно, как и ваш генератор случайных чисел, и его вычислительно очень выполнимо, и если вы действительно столкнетесь с очень плохим экземпляром с большим количеством столкновений, вы можете просто повторить все это, используя новую партию случайных чисел.


Если я не ошибаюсь, здесь та же проблема, что и у K&R 1st в ответе Габриэля; т.е. «ab» и «ba» будут иметь одно и то же значение.
Йоханн Оскарссон
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.