문자열에 대한 해시 함수


124

C 언어로 된 해시 테이블에서 작업 중이며 문자열에 대한 해시 함수를 테스트하고 있습니다.

내가 시도한 첫 번째 기능은 ascii 코드를 추가하고 모듈로 (% 100)를 사용하는 것이지만 첫 번째 데이터 테스트에서 좋지 않은 결과를 얻었습니다 : 130 단어에 대해 40 개의 충돌.

최종 입력 데이터에는 8 000 단어가 포함됩니다 (파일에 사전 저장 됨). 해시 테이블은 int table [10000]로 선언되며 txt 파일에서 단어의 위치를 ​​포함합니다.

첫 번째 질문은 문자열 해싱에 가장 적합한 알고리즘은 무엇입니까? 해시 테이블의 크기를 결정하는 방법은 무엇입니까?

미리 감사드립니다!

:-)


11
해시 테이블에 10K 항목이있는 경우 모듈로 100을 사용하는 이유는 무엇입니까? 130 개의 단어에서 40 개의 충돌을 얻는 것은 그렇게 작은 모듈러스로 놀라운 일이 아닙니다.
Carey Gregory

13
참조 burtleburtle.net/bob/hash/evahash.htmlpartow.net/programming/hashfunctions 다양한 해시에 대한 어떤 있습니다 자원에 대한 (일반에서 암호화에 문자열을).

3
@CareyGregory를 명확히하기 위해 : 기본 수학적 진실로 100 개의 버킷 (즉, mod 100)에있는 130 개의 항목이 30 개의 충돌을 생성해야합니다 (충돌은 1 초, 3 초 등의 항목이 들어갈 때마다 계산 됨). 양동이) 맞나요? 그래서 당신은 그보다 약간 더 높습니다.
derobert

4
@lilawood : 좋아, 그게 내가 생각한 것이지만 더 나은 테스트가 되려면 100 개 항목의 해시 테이블과 함께 80 개의 단어를 사용해야합니다. 이는 라이브 데이터와 동일한 비율을 제공하고 충돌을 강요하지 않습니다.
Carey Gregory

답변:


185

나는 djb2Dan Bernstein으로 좋은 결과를 얻었습니다 .

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
답변에 링크 된 페이지는 매우 흥미 롭습니다.
Adrien Plisson 2011 년

2
프로그램이 while 루프에서 어떻게 실행됩니까 ?? = S
Daniel N.

1
@ danfly09 c가 0 일 때. while (c = * str ++)에 해당하는 값은 (0! = (c = * str ++))
rxantos

5
@Josepas 해시 함수는 이상적으로는 size_t이러한 부호없는 값 (예 :이 코드의 부호없는 long)을 반환해야합니다 . 발신자는 해시 테이블에 맞게 결과의 모듈을 복용 할 책임이있다. 호출자는 해시되는 테이블 슬롯을 제어합니다. 기능이 아닙니다. 부호없는 숫자 만 반환합니다.
WhozCraig

6
놀랄 만한. 이 알고리즘은 Murmur 해시, FNV 변형 해시 및 기타 많은 것에서 지옥을 능가합니다! +1
David Haim

24

첫째, 일반적으로 해시 테이블에 암호화 해시를 사용하고 싶지 않습니다 . 암호화 표준에 따라 매우 빠른 알고리즘은 해시 테이블 표준에 의해 여전히 매우 느립니다.

둘째, 입력의 모든 비트가 결과에 영향을 미칠 수 있는지 확인하려고합니다. 이를 수행하는 한 가지 쉬운 방법은 현재 결과를 일부 비트 수만큼 회전시킨 다음 현재 해시 코드를 현재 바이트와 XOR하는 것입니다. 문자열 끝에 도달 할 때까지 반복합니다. 일반적으로 회전이 바이트 크기의 배수가되는 것을 원하지 않습니다 .

예를 들어 8 비트 바이트의 일반적인 경우를 가정하면 5 비트 씩 회전 할 수 있습니다.

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

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

편집 : 또한 10000 개의 슬롯이 해시 테이블 크기에 적합한 선택은 거의 없습니다. 일반적으로 두 가지 중 하나를 원합니다. 크기로 소수를 원하거나 (일부 유형의 해시 해상도로 정확성을 보장하는 데 필요함) 그렇지 않으면 2의 거듭 제곱을 원합니다 (따라서 값을 올바른 범위로 줄이는 것은 간단한 방법으로 수행 할 수 있습니다. 비트 마스크).


이것은 c가 아니지만이 관련 답변에 대한 귀하의 생각에 관심이 있습니다. stackoverflow.com/a/31440118/3681880
Suragch

1
@Suragch :이 글을 쓴 이후로 상당수의 프로세서가 SHA 계산을 가속화하기 위해 특수 하드웨어를 포함하기 시작하여 훨씬 더 경쟁력이있게되었습니다. 즉, 귀하의 코드가 생각만큼 안전하다고 생각합니다. 예를 들어 IEEE 부동 소수점 숫자에는 동일한 해시를 생성해야하는 두 개의 다른 비트 패턴 (0 및 -0)이 있습니다 (서로 동일한 것으로 비교 될 것입니다). ).
Jerry Coffin

@Jerry Coffin rol () 함수에 어떤 라이브러리가 필요합니까?
thanos.a

@ thanos.a : 라이브러리에 있다는 사실은 모르지만 직접 롤링하면 코드 한두 줄만 필요합니다. 한 청크를 왼쪽으로, 다른 청크를 오른쪽으로 이동하거나 함께 이동합니다.
Jerry Coffin

8

Wikipedia는 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 표준 라이브러리 hcreate / hdestroy / hsearch에서 APRglib 의 C에 대한 기존 해시 테이블 구현이 많이 있습니다.이 구현 은 미리 빌드 된 해시 함수도 제공합니다. 고유 한 해시 테이블이나 해시 함수를 개발하는 것보다 사용하는 것이 좋습니다. 일반적인 사용 사례에 맞게 크게 최적화되었습니다.

그러나 데이터 세트가 정적 인 경우 가장 좋은 해결책은 완벽한 해시 를 사용하는 것입니다 . gperf 는 주어진 데이터 세트에 대해 완벽한 해시를 생성합니다.


hsearch는 문자열 또는 문자열 ptr 주소를 비교하여 검색합니까? 나는 단지 ptr 주소를 확인하고 있다고 생각합니까? 다른 포인터를 사용했지만 동일한 문자열 계산을 시도했습니다. hsearch 어떤 요소가 발견되지 진술 실패
.. MK를

3

djb2에는 이 466k 영어 사전에 대해 317 개의 충돌 이있는 반면 MurmurHash에는 64 비트 해시에 대해 없음, 32 비트 해시에 대해 21 개 (466k 임의 32 비트 해시에 대해 약 25 개가 예상 됨)가 있습니다. 내 권장 사항은 가능한 경우 MurmurHash 를 사용하는 것입니다. 한 번에 몇 바이트를 차지하기 때문에 매우 빠릅니다. 그러나 프로젝트에 복사하여 붙여 넣기 위해 간단하고 짧은 해시 함수가 필요한 경우 한 번에 1 바이트 버전을 사용하는 것이 좋습니다.

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

첫째, 0..99로 해시 된 130 개의 단어에 대해 40 개의 충돌이 나쁘나요? 특별한 조치를 취하지 않으면 완벽한 해싱을 기대할 수 없습니다. 일반적인 해시 함수는 대부분의 경우 랜덤 생성기보다 충돌이 적지 않습니다.

평판이 좋은 해시 함수는 MurmurHash3 입니다.

마지막으로, 해시 테이블의 크기와 관련하여, 특히 버킷이 확장 가능한지 또는 단일 슬롯인지 여부를 염두에두고있는 해시 테이블의 종류에 따라 다릅니다. 버킷을 확장 할 수있는 경우 다시 선택할 수 있습니다. 보유한 메모리 / 속도 제약에 대한 평균 버킷 길이를 선택합니다.


1
예상되는 해시 충돌 수는 n - m * (1 - ((m-1)/m)^n) = 57.075...입니다. 40 번의 충돌은 우연히 예상되는 것보다 낫습니다 (p- 점수 0.999에서 46 ~ 70 번). 문제의 해시 함수는 무작위이거나 매우 드문 이벤트를 목격하는 것보다 더 균일합니다.
Wolfgang Brehm

2

비록 djb2로, cnicutar에 의해 유래에 관한 더 나은 거의 확실하다, 나는 보여주는 그것의 가치가 생각 K & R이 너무 해시 :

1) 분명히 끔찍한 해시 알고리즘, K & R 1 판 ( 출처 )

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

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

    return hash;
}

2) 아마도 K & R 버전 2에 제시된 꽤 괜찮은 해시 알고리즘 일 것이다 (책의 144 페이지에서 내가 확인 함); 주의 : % HASHSIZE해시 알고리즘 외부에서 배열 길이에 맞게 모듈러스 크기 조정을 수행 할 계획이라면 return 문에서 제거해야 합니다. 또한 unsigned long단순 unsigned(int) 대신 return 및 "hashval"유형을 사용 하는 것이 좋습니다 .

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 판 해시.

unordered_map(해시 테이블 템플릿) 및 unordered_set(해시 세트 템플릿)에 사용되는 GCC C ++ 11 해싱 함수 는 다음과 같습니다.

암호:

// 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

좋은 테이블에 대해 찬성 투표를 받으면 답변에 각 해시에 대한 소스 코드를 게시하는 것도 필수적입니다. 그렇지 않으면 링크가 끊어지고 운이 나지 않을 수 있습니다.
Gabriel Staples

예상되는 충돌 수는 해시가 실제로 무작위 인 경우 9.112499989700318E + 7 또는 0.103 * 960³이어야합니다. 따라서이 값이 모두 해당 값 주변에 있었다면 놀라지 않았을 것입니다.하지만 0.0616 * 960³는 마치 해시는 우연히 예상되는 것보다 더 균등하게 분산되며 64 바이트 길이에서는이 제한에 확실히 접근해야합니다. 해시 한 문자열 세트를 공유하여 재현 할 수 있습니까?
Wolfgang Brehm

0

내가 좋은 결과로 사용한 한 가지는 다음과 같습니다 (이름을 기억할 수 없기 때문에 이미 언급했는지 모르겠습니다).

키 알파벳 [0,255]의 각 문자에 대해 임의의 숫자로 테이블 T를 미리 계산합니다. T [k0] xor T [k1] xor ... xor T [kN]을 사용하여 키 'k0 k1 k2 ... kN'을 해시합니다. 이것이 난수 생성기만큼 임의적이며 계산적으로 매우 실행 가능하다는 것을 쉽게 보여줄 수 있으며, 충돌이 많은 매우 나쁜 인스턴스가 실제로 발생하면 새로운 난수 배치를 사용하여 모든 것을 반복 할 수 있습니다.


내가 착각하지 않으면 Gabriel의 대답에서 K & R 1st와 동일한 문제가 발생합니다. 즉, "ab"와 "ba"는 동일한 값으로 해시됩니다.
Johann Oskarsson
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.