programing

단순 해시 함수

stoneblock 2023. 7. 13. 20:21

단순 해시 함수

해시 테이블을 사용하여 다른 단어를 저장하는 C 프로그램을 작성하려고 하는데 도움이 필요합니다.

먼저 저장해야 할 단어 수에 가장 가까운 소수 크기의 해시 테이블을 만든 다음 해시 함수를 사용하여 각 단어의 주소를 찾습니다.저는 가장 간단한 기능으로 시작했습니다. 문자들을 더해서 88%의 충돌로 끝났습니다.그리고 나서 저는 그 기능을 실험하기 시작했고, 제가 그것을 무엇으로 바꾸든, 충돌은 35% 이하로 줄어들지 않는다는 것을 발견했습니다.지금은 사용 중입니다.

unsigned int stringToHash(char *word, unsigned int hashTableSize){
  unsigned int counter, hashAddress =0;
  for (counter =0; word[counter]!='\0'; counter++){
    hashAddress = hashAddress*word[counter] + word[counter] + counter;
  }
  return (hashAddress%hashTableSize);
}

이것은 제가 생각해 낸 임의의 기능이지만, 35% 정도의 충돌로 최고의 결과를 제공합니다.

지난 몇 시간 동안 해시 함수에 대한 기사를 읽고 djb2와 같은 몇 가지 간단한 것을 사용하려고 했지만, 모두 더 나쁜 결과를 낳았습니다. (djb2는 37%의 충돌을 일으켰고, 훨씬 더 나쁜 것은 아니지만, 더 나쁜 것보다는 더 좋은 것을 기대하고 있었습니다.) 다른 복잡한 것들도 사용할 줄 모릅니다.그들이 받아들이는 매개 변수(키, 렌, 시드)가 무엇인지 모르기 때문에 중얼거림2와 같은 것입니다.

djb2를 사용해도 35% 이상의 충돌이 발생하는 것이 정상입니까, 아니면 제가 잘못하고 있는 것입니까?키, 렌 및 시드 값은 무엇입니까?

sdbm 시도:

hashAddress = 0;
for (counter = 0; word[counter]!='\0'; counter++){
    hashAddress = word[counter] + (hashAddress << 6) + (hashAddress << 16) - hashAddress;
}

또는 djb2:

hashAddress = 5381;
for (counter = 0; word[counter]!='\0'; counter++){
    hashAddress = ((hashAddress << 5) + hashAddress) + word[counter];
}

또는 아들러32:

uint32_t adler32(const void *buf, size_t buflength) {
     const uint8_t *buffer = (const uint8_t*)buf;

     uint32_t s1 = 1;
     uint32_t s2 = 0;
 
     for (size_t n = 0; n < buflength; n++) {
        s1 = (s1 + buffer[n]) % 65521;
        s2 = (s2 + s1) % 65521;
     }     
     return (s2 << 16) | s1;
}

// ...

hashAddress = adler32(word, strlen(word));

하지만 이것들 중 어느 것도 정말 훌륭하지 않습니다.만약 당신이 정말로 좋은 해시를 원한다면, 예를 들어 lookup3, murmur3, CityHash와 같은 좀 더 복잡한 것이 필요합니다.

해시 테이블이 70-80% 이상 채워지면 충돌이 많이 발생할 것으로 예상됩니다.이것은 완벽하게 정상적이며 매우 좋은 해시 알고리즘을 사용하는 경우에도 발생할 수 있습니다.그렇기 때문에 대부분의 해시 테이블 구현은 해시 테이블의 용량을 증가시킵니다(예:capacity * 1.5아니 심지어는capacity * 2 해시테과블비이무율추는즉시하가를언가에▁)즉시는해추하▁to▁as▁and▁)table.size / capacity이미 0.7에서 0.8을 초과했습니다.용량을 늘리면 더 큰 용량으로 새 해시 테이블이 생성되고 현재 해시 테이블의 모든 값이 새 해시 테이블에 추가됩니다(대부분의 경우 새 인덱스가 다르므로 모두 다시 해시되어야 함). 새 해시 테이블 어레이가 이전 해시 테이블 어레이를 대체하고 이전 해시 테이블 어레이가 릴리스/해제됩니다.이라면, 최소 권장량, 1400 1,이 더 . 1250의 해시 테이블 용량은 다음과 같습니다. 1400의 해시 테이블 용량은 다음과 같습니다.

해시 테이블은 "채워서 가득 채워서는 안 됩니다." 적어도 빠르고 효율적이어야 하는 경우에는 그렇지 않습니다(따라서 항상 여유 용량이 있어야 함).해시입니다. . 그들은 빠릅니다.O(1)하지만 일반적으로 동일한 데이터를 다른 구조에 저장하는 데 필요한 공간보다 더 많은 공간을 낭비합니다(정렬 배열로 저장할 경우 1000단어에 대한 용량만 필요합니다. 단점은 검색 속도가 다음보다 빠를 수 없다는 것입니다.O(log n) 없는 쪽이든 .충돌 없는 해시 테이블은 대부분의 경우 어느 쪽이든 가능하지 않습니다.거의 모든 해시 테이블 구현에서는 충돌이 발생할 것으로 예상하고 일반적으로 충돌을 처리하는 방법이 있습니다(일반적으로 충돌로 인해 검색 속도가 다소 느려지지만 해시 테이블은 여전히 작동하고 많은 경우 다른 데이터 구조를 능가합니다).

꽤 함수를 , 를 사용하여 을 크롭하는 경우 요구 .%) 결국에는.많은 해시 테이블 구현이 항상 2개의 용량의 전력을 사용하는 이유는 모듈로를 사용하지 않고 AND(&AND 작업은 대부분의 CPU에서 볼 수 있는 가장 빠른 작업 중 하나이기 때문에 자르기용입니다(모듈로는 AND보다 결코 빠르지 않으며, 가장 좋은 경우에는 동일하게 빠릅니다, 대부분의 경우에는 훨씬 느립니다).해시 테이블이 두 가지 크기의 전력을 사용하는 경우 AND 작업으로 모듈을 교체할 수 있습니다.

x % 4  == x & 3
x % 8  == x & 7
x % 16 == x & 15
x % 32 == x & 31
...

하지만 이것은 두 가지 크기의 전력에서만 작동합니다.모듈로를 사용하는 경우 해시가 "비트 분포"가 매우 나쁜 매우 나쁜 해시일 경우에만 2가지 크기의 파워가 무언가를 살 수 있습니다.으로 어떤 시프트 shifting)도 에 의해 합니다.>>또는<<) 다른 또는 비트 이동과 유사한 영향을 미치는 다른 작업.

다음을 위해 분해된 Lookup3 구현을 만들었습니다.

#include <stdint.h>
#include <stdlib.h>

#define rot(x,k) (((x)<<(k)) | ((x)>>(32-(k))))

#define mix(a,b,c) \
{ \
  a -= c;  a ^= rot(c, 4);  c += b; \
  b -= a;  b ^= rot(a, 6);  a += c; \
  c -= b;  c ^= rot(b, 8);  b += a; \
  a -= c;  a ^= rot(c,16);  c += b; \
  b -= a;  b ^= rot(a,19);  a += c; \
  c -= b;  c ^= rot(b, 4);  b += a; \
}

#define final(a,b,c) \
{ \
  c ^= b; c -= rot(b,14); \
  a ^= c; a -= rot(c,11); \
  b ^= a; b -= rot(a,25); \
  c ^= b; c -= rot(b,16); \
  a ^= c; a -= rot(c,4);  \
  b ^= a; b -= rot(a,14); \
  c ^= b; c -= rot(b,24); \
}

uint32_t lookup3 (
  const void *key,
  size_t      length,
  uint32_t    initval
) {
  uint32_t  a,b,c;
  const uint8_t  *k;
  const uint32_t *data32Bit;

  data32Bit = key;
  a = b = c = 0xdeadbeef + (((uint32_t)length)<<2) + initval;

  while (length > 12) {
    a += *(data32Bit++);
    b += *(data32Bit++);
    c += *(data32Bit++);
    mix(a,b,c);
    length -= 12;
  }

  k = (const uint8_t *)data32Bit;
  switch (length) {
    case 12: c += ((uint32_t)k[11])<<24;
    case 11: c += ((uint32_t)k[10])<<16;
    case 10: c += ((uint32_t)k[9])<<8;
    case 9 : c += k[8];
    case 8 : b += ((uint32_t)k[7])<<24;
    case 7 : b += ((uint32_t)k[6])<<16;
    case 6 : b += ((uint32_t)k[5])<<8;
    case 5 : b += k[4];
    case 4 : a += ((uint32_t)k[3])<<24;
    case 3 : a += ((uint32_t)k[2])<<16;
    case 2 : a += ((uint32_t)k[1])<<8;
    case 1 : a += k[0];
             break;
    case 0 : return c;
  }
  final(a,b,c);
  return c;
}

이 코드는 원래 코드만큼 성능에 최적화되지 않았기 때문에 훨씬 간단합니다.또한 원래 코드만큼 휴대할 수는 없지만, 현재 사용 중인 모든 주요 소비자 플랫폼에 휴대할 수 있습니다.또한 CPU 엔디언을 완전히 무시하고 있지만 실제로는 문제가 되지 않으며 크고 작은 엔디언 CPU에서 작동합니다.빅 및 리틀 엔디안 CPU의 동일한 데이터에 대해 동일한 해시를 계산하지는 않지만, 이는 요구 사항이 아닙니다. 두 종류의 CPU 모두에서 양호한 해시를 계산하며, 단일 시스템에서 동일한 입력 데이터에 대해 항상 동일한 해시를 계산하는 것이 유일하게 중요합니다.

이 기능은 다음과 같이 사용할 수 있습니다.

unsigned int stringToHash(char *word, unsigned int hashTableSize){
  unsigned int initval;
  unsigned int hashAddress;

  initval = 12345;
  hashAddress = lookup3(word, strlen(word), initval);
  return (hashAddress%hashTableSize);
  // If hashtable is guaranteed to always have a size that is a power of 2,
  // replace the line above with the following more effective line:
  //     return (hashAddress & (hashTableSize - 1));
}

당신은 무엇을 궁금해 할 것입니다.initval당신이 원하는 건 무엇이든 상관없어요소금이라고 할 수 있습니다.이는 해시 값에 영향을 미치지만, 이로 인해 해시 값의 품질이 향상되거나 악화되지는 않습니다(적어도 평균적인 경우는 아니지만 매우 특정한 데이터에 대한 충돌이 많거나 적을 수 있습니다).예: 다른 방법을 사용할 수 있습니다.initval은 동일한 데이터를 두 번 해시 을 생성해야 값동데허두를번매합만값니다다야번해생을성시다해있니습같경른은다가용은우오보과없된음는려은다히장만지지시하일해이한터허▁values▁value▁if▁if▁likely▁rather▁twice▁(▁you다there▁is).initval는 다릅니다. 만약 같은 값을 생성한다면, 이것은 매우 불운한 우연일 것입니다. 당신은 그것을 일종의 충돌로 취급해야 합니다.)다른 방법을 사용하는 것은 권장되지 않습니다.initval동일한 해시 테이블에 대한 데이터를 해시할 때의 값(평균적으로 충돌이 더 많이 발생함).의 또 인데, 이가 initval이 됩니다.initval다른 데이터를 해시할 때(따라서 다른 데이터와 이전 해시 모두 해시 함수의 결과에 영향을 미칩니다).설정할 수도 있습니다.initval0해시 테이블이 생성될 때 임의의 값을 좋아하거나 선택하는 경우(그리고 항상 이 해시 테이블 인스턴스에 대해 이 임의의 값을 사용하지만 각 해시 테이블에는 고유한 임의의 값이 있음).

충돌에 대한 참고 사항:

충돌은 보통 실제로는 그렇게 큰 문제가 되지 않으며, 충돌을 피하기 위해서만 수 톤의 메모리를 낭비해도 효과가 나타나지 않습니다.문제는 오히려 당신이 그들을 어떻게 효율적으로 다룰 것인가 하는 것입니다.

당신은 현재 9000개의 단어를 다루고 있다고 말했습니다.정렬되지 않은 배열을 사용하는 경우 배열에서 단어를 찾는 데 평균 4500개의 비교가 필요합니다.시스템에서 4500 문자열 비교(단어 길이가 3~20자 사이라고 가정)에는 38마이크로초(0.000038초)가 필요합니다.그래서 이렇게 간단하고 비효율적인 알고리즘도 대부분의 목적에 충분히 빠릅니다.단어 목록을 정렬하고 이진 검색을 사용한다고 가정하면 배열에서 단어를 찾는 데 평균 13개만 비교하면 됩니다.13 비교는 시간적인 측면에서 아무것도 아닌 것에 가깝습니다. 신뢰할 수 있는 벤치마크조차 하기에는 너무 작습니다.따라서 해시 테이블에서 단어를 찾는 데 2~4개의 비교가 필요하다면 성능 문제가 큰지 여부를 묻는 데 단 1초도 허비하지 않을 것입니다.

이 경우 이진 검색을 사용하여 정렬된 목록이 해시 테이블보다 훨씬 더 높을 수 있습니다.물론, 13개의 비교는 2-4개의 비교보다 더 많은 시간이 필요하지만 해시 테이블의 경우 조회를 수행하려면 먼저 입력 데이터를 해시해야 합니다.해싱만으로는 이미 13가지 비교보다 더 오래 걸릴 수 있습니다!해시 값이 높을수록 동일한 양의 데이터를 해시하는 데 시간이 더 오래 걸립니다.따라서 해시 테이블은 매우 많은 양의 데이터를 보유하고 있거나 데이터를 자주 업데이트해야 하는 경우(예: 정렬된 목록에 비해 해시 테이블에 대한 비용이 적게 들기 때문에 테이블에서 단어를 지속적으로 추가/제거)에만 성능 면에서 이익을 얻을 수 있습니다.해쉬해슬은O(1)크기에 관계없이 룩업에는 항상 동일한 시간이 필요합니다. O(log n)단어 수에 따라 검색이 로그적으로 증가한다는 것을 의미합니다. 즉, 단어 수가 많을수록 검색 속도가 느려집니다.하지만 Big-O 표기법은 절대 속도에 대해 아무 것도 말하지 않습니다!이것은 큰 오해입니다.그렇다고는 할 수 없습니다.O(1)알고리즘은 항상 a보다 더 빨리 수행됩니다.O(log n)은 나하인 . Big-O 표기법은 당신에게 다음과 같은 경우에만 알려줍니다.O(log n)알고리즘은 특정 수의 값에 대해 더 빠르며 당신은 계속해서 값의 수를 증가시킵니다.O(1) 알리즘이확추것입다니월할실히고▁the▁will▁certainly▁overtake다▁algorithm를 추월할 것입니다.O(log n)알고리즘을 사용할 수 있지만 현재 단어 수가 해당 지점보다 훨씬 적을 수 있습니다.두 가지 접근 방식을 모두 벤치마킹하지 않고는, Big-O 표기법만 보고 어느 것이 더 빠르다고 말할 수 없습니다.

다시 충돌로 돌아갑니다.만약 당신이 충돌을 당한다면 당신은 어떻게 해야 합니까?충돌 수가 적고 여기서 나는 전체 충돌 수(해시 테이블에서 충돌하는 단어 수)가 아니라 인덱스당 1(같은 해시 테이블 인덱스에 저장된 단어 수)을 의미하는 경우 가장 간단한 방법은 연결된 목록으로 저장하는 것입니다.이 테이블 인덱스에 대해 지금까지 충돌이 없는 경우 키/값 쌍이 하나만 있습니다.충돌이 발생한 경우 키/값 쌍의 연결된 목록이 있습니다.이 경우 코드가 링크된 목록을 반복하고 각 키를 확인한 후 일치하는 경우 값을 반환해야 합니다.이 링크된 목록의 항목 수는 4개를 넘지 않으며, 4개를 비교하는 것은 성능 면에서 중요하지 않습니다. 지수를 것은 그서색인찾는것은을래▁so것은입니다.O(1) 이에 없는 을 감지하는 것)은 " " 입니다.O(n)그런데 여기서n연결된 목록 항목의 수에 불과하므로 최대 4개입니다.

수 키쌍의 크기 할 수도 . 충수 증 값 가 가 동 로 크 으 조 키 가 된 정 / 기 있 수 습 도 니 저 다 쌍 배 할 을 장 ▁of▁if/ 값,▁look다▁can있▁of▁pairs충▁array▁a▁a니▁list습돌,수,▁allows▁ofisions▁which▁may▁coll▁number키▁the▁become▁sorted도▁alsovalue▁raises저▁you 이를 통해 다음 항목을 검색할 수 있습니다.O(log n) 다시, 그고다시리,,n해시 테이블의 모든 키가 아니라 해당 배열에 있는 키의 수입니다.한 인덱스에 100개의 충돌이 있더라도 올바른 키/값 쌍을 찾는 데는 최대 7개의 비교가 필요합니다.그것은 여전히 거의 아무것도 아닙니다.실제로 한 인덱스에서 100번의 충돌이 발생하더라도 해시 알고리즘이 키 데이터에 적합하지 않거나 해시 테이블의 용량이 너무 작습니다.동적으로 크기가 조정되고 정렬된 어레이의 단점은 링크된 목록보다 키를 추가/제거하는 작업이 더 많다는 것입니다(코드 측면에서는, 반드시 성능 측면에서는 아님).따라서 일반적으로 충돌 횟수를 충분히 낮게 유지하고 이러한 연결 목록을 C에서 직접 구현하여 기존 해시 테이블 구현에 추가하는 것이 거의 사소한 경우에는 연결 목록을 사용하는 것으로 충분합니다.

제가 본 대부분의 해시 테이블 구현은 충돌을 처리하기 위해 이러한 "대체 데이터 구조로의 폴백"을 사용합니다.단점은 대체 데이터 구조를 저장하려면 메모리가 조금 더 필요하고 해당 구조에서 키를 검색하려면 코드가 조금 더 필요하다는 것입니다.해시 테이블 내부에 충돌을 저장하고 추가 메모리를 필요로 하지 않는 솔루션도 있습니다.그러나 이러한 솔루션에는 몇 가지 단점이 있습니다.첫 번째 단점은 모든 충돌이 더 많은 데이터가 추가될수록 더 많은 충돌의 가능성이 증가한다는 것입니다.두 번째 단점은 키에 대한 조회 시간이 지금까지의 충돌 횟수에 따라 선형적으로 감소하는 반면(그리고 앞에서 말했듯이, 모든 충돌은 데이터가 추가됨에 따라 훨씬 더 많은 충돌로 이어짐), 해시 테이블에 없는 키에 대한 조회 시간은 더욱 악화되고 결국에는해시 테이블에 없는 키를 조회하는 경우(조회를 수행하지 않으면 알 수 없음) 전체 해시 테이블에 대한 선형 검색(YUK!!!)만큼의 시간이 소요될 수 있습니다.따라서 여분의 메모리를 확보할 수 있다면 충돌을 처리할 수 있는 대체 구조를 선택하십시오.

먼저 저장해야 할 단어 수에 가장 가까운 소수 크기의 해시 테이블을 만든 다음 해시 함수를 사용하여 각 단어의 주소를 찾습니다.

...

return(hashAddress%hashTableSize);

서로 다른 해시의 수가 단어의 수와 비슷하므로 충돌이 훨씬 적을 것으로 예상할 수 없습니다.

랜덤 해시를 사용하여 간단한 통계 테스트를 수행한 결과, #words == #different 해시가 있는 경우 26%가 제한 충돌률이라는 것을 발견했습니다.

언급URL : https://stackoverflow.com/questions/14409466/simple-hash-functions