일반적으로 어떤 키
에 대응하는 어떤 값
을 저장
하는 것을 해시 테이블(엄밀히는 해시 맵)의 형태라고 한다. 대표적인 예로 C#의 Dictionary, Java의 HashMap 등이 있다.
데이터의 위치
를 의미하는 데이터실제
로 저장하는 데이터
{John: 89, Maria: 100}
만약 해시 충돌을 방지할 수 있다면 어떤 이점이 있을까?
결론적으로 말하면 메모리 낭비를 줄일 수 있고, 실행속도도 빨라진다.
위 [키와 값을 사용해서 저장하는 예] 에서 보면 데이터를 추가할 때, key배열
에 문자열
을 저장하고 있다. 해시 충돌이 빈번하게 일어난다면 이처럼 문자열을 저장하지 않고서는 해시맵을 사용할 수 없을 것이다. 문자열의 길이는 정해져 있지 않고, 그렇다면 저장 할 때마다 동적 할당
을 해야 하는데, 메모리 낭비에 실행시간이 느려질 수 밖에 없다.
하지만, 해시 충돌이 전혀 일어나지 않는다면? key 배열
에 해시 값인 int형
만 저장해도 된다. 이렇게 되면 동적할당을 할 필요가 없어진다.
좋은 해시 함수를 쓰면 가능한 이야기이다.
좋은 해시 함수란 어떤 경우에도 고정된 크기의 값으로 변환이 가능해야 하고(필수), 해시 충돌이 거의 없어야 한다.
충돌이 전혀 안나는 해시함수는 없지만 아주 덜 나는 함수는 있다. 예를 들어 32비트 해시에서 이상의 데이터를 넣으면 충돌이 발생할 수 밖에 없다.
stackExchange라는 사이트에서 어떤 사람이 해시 함수 테스트 결과를 공유해 주었다. 관심이 있으면 참고해 보면 좋을 듯 하다.
해당 테스트에 사용한 키는 소문자 영단어
216,553
개, 정수
1 ~ 216,533
, 랜덤
으로 뽑은 216,533
개의 GUID
이다.
/* 65599를 곱해나가는 해시 함수 */
size_t hash_65599(const char* string, size_t len)
{
size_t i;
size_t hash;
hash = 0;
for (i = 0; i < len; ++i) {
hash = 65599 * hash + string[i];
}
return hash ^ (hash >> 16);
}
int add(const char* key, int value, size_t (*hash_func)(const char*, size_t))
{
size_t i;
size_t start_index;
size_t hash_id;
hash_id = hash_func(key, strlen(key));
start_index = hash_id % BUCKET_SIZE;
i = start_index;
do {
if (s_keys[i] == NULL) {
/* 새 키-값을 설정 */
return TRUE;
}
if (strcmp(s_keys[i], key) == 0) {
return TRUE;
}
i = (i + 1) % BUCKET_SIZE;
} while (i != start_index);
return FALSE;
}
int add_fast(size_t hash_key, const char* value)
{
size_t i;
size_t start_index;
start_index = hash_key % BUCKET_SIZE;
i = start_index;
do {
if (s_keys[i] == INT_MIN) {
/* 새 해시-값 삽입 */
return TRUE;
}
if (s_keys[i] == hash_key) {
return TRUE;
}
i = (i + 1) % BUCKET_SIZE;
} while (i != start_index);
return FALSE;
}