хеш-функція для рядка


124

Я працюю над хеш-таблицею мовою C і тестую хеш-функцію для рядка.

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

Кінцеві вхідні дані будуть містити 8 000 слів (це довідкові сховища у файлі). Хеш-таблиця оголошена як таблиця int [10000] і містить положення слова у файлі txt.

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

Спасибі заздалегідь !

:-)


11
Якщо у вашій хеш-таблиці є записи 10К, чому б ви використовували модуль 100? Отримати 40 зіткнень із 130 слів не дивно, що такий невеликий модуль.
Carey Gregory

13
Дивіться burtleburtle.net/bob/hash/evahash.html та partow.net/programming/hashfunctions, які є ресурсами про різні хешування (від загального до рядкового до криптовалютного).

3
Для уточнення @CareyGregory: Ви розумієте, що, як основна математична істина, 130 предметів у 100 відрах (тобто мод 100) повинні спричинити 30 зіткнень (де зіткнення рахується як кожен раз, коли вводиться другий, третій і т. Д. Елемент) відро), правильно? Тож ти лише трохи вище цього.
derobert

4
@lilawood: Гаразд, це я зрозумів, але для кращого тестування слід використовувати 80 слів із хеш-таблицею з 100 записів. Це дасть вам ті ж пропорції, що і ваші живі дані, і не спричинить зіткнення.
Carey Gregory

4
Можливий дублікат функції хорошого хешу для струнних
MJ Rayburn

Відповіді:


185

У мене були хороші результати з 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
дивовижний. цей алгоритм переміг хеш з мурмурського хешу, FNV варіантів хешей та багатьох інших! +1
Девід Хаїм

24

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

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

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

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

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

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


Це не з, але я був би зацікавлений в ваших думках до цього відповідного відповіді: stackoverflow.com/a/31440118/3681880
Suragch

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 рядка? Я думаю, це просто перевірка адреси 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 квт (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) Мабуть, жахливий алгоритм хешування, як це представлено у першому виданні 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 книги); Примітка: не забудьте видалити % 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"). Однак це не так з хешем другого видання, який би (набагато краще!) Повертав два різні значення для цих рядків.

Функції хешування 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-бітний розмір_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% швидкості зіткнення.


Хоча це посилання може відповісти на питання, краще включити сюди суттєві частини відповіді та надати посилання для довідки. Відповіді лише на посилання можуть стати недійсними, якщо пов’язана сторінка зміниться.
thewaywewere

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

Очікувана кількість зіткнень повинна становити 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.