해시 테이블의 평균 검색, 삽입, 삭제시의 시간복잡도는 O(1)이다.
이게 가능하려면, 어떤 메모리 주소에 어떤 데이터가 저장되어 있는지 한번에 알 수 있어야 한다.
이번 포스팅에서는 아래 순서대로 hash table
을 파헤쳐 보려고 한다.
- 무작위로 뽑은 10개의 수를 어떻게 하면 O(1)의 시간복잡도로 추가하고 삭제할 수 있는지에 대한 답을 찾아가는 방식으로 해시를 이해
- 해시를 이해하기 위해 필요한 용어와 개념
- 해시 테이블에 문자열을 저장하는 방법
코드 예제
에서는 C언어
로 작성하였다.
239, 94, 1, 131, 99, 853, 42, 56, 922, 3
O(1)
으로 알 수 있는 방법은 무엇일까?
가장 단순 무식하면서 주먹구구식 방법이다.
즉, 이 경우에는 입력값이 배열의 색인
numbers[3] = 1; /* 입력값: 3 */
numbers[99] = 1; /* 입력값: 99 */
해시 테이블 초기화
#include <limits.h>
#define TRUE (1)
#define FALSE (0)
#define BUCKET_SIZE (23)
int s_numbers[BUCKET_SIZE];
/* INT_MIN으로 해시 테이블을 초기화 */
void init_hashtable(void)
{
size_t i;
for (i = 0; i < BUCKET_SIZE; ++i) {
s_numbers[i] = INT_MIN;
}
}
검색
int has_value(int value)
{
int i;
int start_index;
start_index = value % BUCKET_SIZE;
/* 음수가 들어왔을 때 버켓 사이즈 보다 작게 만드려고 아래 코드를 추가 */
if (start_index < 0) {
start_index += BUCKET_SIZE;
}
i = start_index;
do {
if (s_numbers[i] == value) {
return TRUE;
} else if (s_numbers[i] == INT_MIN) {
return FALSE;
}
i = (i + 1) % BUCKET_SIZE;
} while (i != start_index); /* 한 바퀴 다 돌면 값이 없다는 뜻이므로 반복문을 빠져 나옴 */
return FALSE;
}
} else if (s_numbers[i] == INT_MIN) {
추가
int add(int value)
{
int i;
int start_index;
start_index = value % BUCKET_SIZE;
/* 음수가 들어왔을 때 버켓 사이즈 보다 작게 만드려고 아래 코드를 추가 */
if (start_index < 0) {
start_index += BUCKET_SIZE;
}
i = start_index;
do {
if (s_numbers[i] == value || s_numbers[i] == INT_MIN) {
s_numbers[i] = value;
return TRUE;
}
i = (i + 1) % BUCKET_SIZE;
} while (i != start_index);
return FALSE;
}
main.c 에서 사용
int main (void)
{
init_hashtable();
add(703);
add(793);
add(724);
add(441);
add(219);
add(1);
add(81);
add(546);
add(777);
add(747);
return 0;
}
해시값은 어떤 데이터를 해시 함수에 넣어서 나온 결과값이다. 이렇게 해서 받은 값은 주민등록번호나 학번과 같이 그 데이터를 대표하는 값이여야 하고 고유한 값이여야 한다.
해시 함수는 임의의 크기 (길이의 제한x)를 가진 데이터를 고정된 길이의 해시값에 대응하게 해 주는 함수이다.
해시 함수도 함수이기 때문에, 아래 함수의 특성을 가진다.
앞서 해시 함수란 임의의 크기를 가진 데이터를 고정길이의 출력값으로 바꾸는 함수라고 했다. 임의의 크기를 가진 데이터는 어떤 것이 있을까? 정수형은 길이가 정해져 있다. 일반적으로 int형 의 경우 4byte, double 형의 경우 8byte의 고정된 크기를 가지고 있다. 문자열의 경우는? 문자열은 고정된 크기가 없고 5글자일수도 5만자일 수도 있다. 때문에 해시 테이블에 문자열을 저장해 보도록 하자.
- 해야할 일
- 문자열을 배열 색인으로 변환
- 문자열은 문자열이라 % 연산이 불가능함
- 그래서 두 단계로 나눠서 진행
- 문자열을 정수형으로 반환 (해시 함수 이용)
- 반환된 정수를 %연산해서 배열 색인으로 변환
문자열의 각 요소는 char형 = 아스키코드 = 정수형
각 요소의 아스키 코드값을 더해 주어 정수형으로 변환
해시 함수 예
int hash_function(const char*)
{
int value = 0;
const char* c = str;
while (*c != '\0') {
value += *c++;
}
return value;
}
문자열을 정수형으로 변환했으니 실제로 버킷에 저장해보자.
"John"의 해시값은 399, "Maria"의 해시값은 490
해시 값을 바탕으로 나머지 연산을 이용하여 색인을 구할 수 있음
색인을 구하면 테이블에 저장
/* add 함수에 문자열 하나와 hash함수 포인터를 전달 */
int add(const char* value, int (*hash_func)(const char*));
add("Teemo", hash_function);
add("Hana", hash_function);
하지만 위 해시함수는 O(문자열 길이)만큼 돌아야 하기 때문에 O(1)으로 돌지 않는다.
만약, 해당 문자열을 여러번 쓰는 경우 (삽입할 때 한번쓰고, 검색할 때 한번 쓰고 등등), O(1)으로 만드려면 해시 함수를 한 번만 호출하고 그 결과를 기억해 둬야 함
그리고 필요할 때 hash_id를 이용해 해시 테이블 함수 호출
이렇게 하면 hash_id % 23 연산 한 번으로 해당 문자열의 색인을 구할 수 있으니, hash_id로 부터 저장할 위치의 색인을 O(1)으로 구할 수 있다!
지금까지 본 건 엄밀히 따지면 HashSet
이다.
집합 내 어떤 데이터를 중복 없이 저장한 게 전부이다. 저장하려고 해시 함수를 사용했을 뿐.
다음 포스팅에서 HashMap
에 대해서 알아보려고 한다.