테이블 자료구조의 탐색 연산은 O(1)의 시간 복잡도를 보인다.
위의 두개의 조건은 테이블의 기본 조건에 해당한다.
#include <stdio.h>
typedef struct _empInfo
{
int empNum; // 직원의 고유번호 : key
int age; // 직원의 나이 : value
}EmpInfo;
int main(void)
{
EmpInfo empInfoArr[1000]; // 직원들의 정보를 저장하기 위해 선언된 배열
EmpInfo ei;
int eNum;
printf("사번과 나이 입력: ");
scanf("%d %d", &(ei.empNum), &(ei.age));
empInfoArr[ei.empNum] = ei; // 단번에 저장!
printf("확인하고픈 직원의 사번 입력: ");
scanf("%d", &eNum);
ei = empInfoArr[eNum]; // 단번에 탐색!
printf("사번 %d, 나이 %d \n", ei.empNum, ei.age);
return 0;
}
위의 코드를 통해 “직원의 고유번호를 인덱스 값으로 하여, 그 위치에 데이터를 저장한다” 라는 사실을 알 수 있다.
예제에서 보인 테이블과 관련하여 지적한 문제점 두 가지를 정리하면 다음과 같다.
이 두 가지 문제를 동시에 해결해주는 것이 바로 “해쉬 함수”이다.
int GetHashValue(int empNum)
{
return empNum % 100;
}
다음과 같은 함수를 해쉬함수라고 부르며 길이가 100인 배열을 선언했다고 가정할때 8자리의 수로 이루어진 직원의 고유번호를 마지막 2자리로 판단하도록 만들어 주는 것이다.
좋은 해쉬 함수란 “충돌”이 발생할 확률이 낮다는 것을 의미한다. 따라서 좋은 해쉬 함수는 키의 일부분을 참조하여 해쉬 값을 만들지 않고, 키 전체를 참조하여 해쉬 값을 만들어 낸다.
선형 조사법은 충돌의 횟수가 증가함에 따라서 “클러스터 현상”, 쉬운 표현으로 특정 영역에 데이터가 집중적으로 몰리는 현상이 발생한다는 단점이 있다.
선형 조사법에서 키가 9인 데이터를 삭제할 때 테이블의 상태를 EMPTY가 아닌 DELETED로 바꾸는데 그 이유로 만약 키가 2인 데이터의 탐색을 진행하기 위해서는 %7의 해쉬 함수를 거치고, 그 결과로 얻은 2를 인덱스 값으로 하여 탐색을 진행하게 된다. 만약에 그 위치의 슬롯 상태가 EMPTY라면, 데이터가 존재하지 않는다고 판단하여 탐색을 종료하게 된다. 그 옆자리를 확인해야하기 때문에 DELETED 상태로 두는 것이다.
따라서 우리는 다음 두 가지 사실을 추가로 정리해야 한다.
이차 조사법의 문제점 : 해쉬 값이 같으면, 충돌 발생시 빈 슬롯을 찾기 위해서 접근하는 위치가 늘 동일하다.
따라서 이중 해쉬 방식을 이용해 이를 해결할 수 있다.
1차 해쉬 함수 h1(k) = k % 15
2차 해쉬 함수 h2(k) = 1 + (k % 7)
먼저 1을 더한 이유는 2차 해쉬 값이 0이 되는 것을 막기 위해서이다.
7로 나누어 주는 이유는 15보다 작은 소수중 하나를 선택한 것이다.
이중 해쉬는 이상적인 충돌 해결책으로 알려져 있다.
체이닝이랑 무슨 일이 있어도 자신의 자리에 저장을 한다는 의미로 충돌이 발생해도 자신의 자리에 들어가겠다는 의미이다.
따라서 2차원 배열 또는 연결리스트로 구현이 가능하다. 하지만 2차원 배열을 메모리 낭비가 심하고 충돌의 최대 횟수를 결정해야 하는 부담이 있기 때문에 보통 연결리스트로 구현을 많이 한다.
체이닝의 구현을 위한 두가지 방법
아래의 코드는 구분 2로 구현한 코드이다.
#include <stdio.h>
#include <stdlib.h>
#include "Table2.h"
#include "DLinkedList.h"
void TBLInit(Table * pt, HashFunc * f)
{
int i;
for(i=0; i<MAX_TBL; i++)
ListInit(&(pt->tbl[i]));
pt->hf = f;
}
void TBLInsert(Table * pt, Key k, Value v)
{
int hv = pt->hf(k);
Slot ns = {k, v};
if(TBLSearch(pt, k) != NULL) // 키가 중복되었다면
{
printf("≈∞ ¡fl∫π ø¿∑˘ πflª˝ \n");
return;
}
else
{
LInsert(&(pt->tbl[hv]), ns);
}
}
Value TBLDelete(Table * pt, Key k)
{
int hv = pt->hf(k);
Slot cSlot;
if(LFirst(&(pt->tbl[hv]), &cSlot))
{
if(cSlot.key == k)
{
LRemove(&(pt->tbl[hv]));
return cSlot.val;
}
else
{
while(LNext(&(pt->tbl[hv]), &cSlot))
{
if(cSlot.key == k)
{
LRemove(&(pt->tbl[hv]));
return cSlot.val;
}
}
}
}
return NULL;
}
Value TBLSearch(Table * pt, Key k)
{
int hv = pt->hf(k);
Slot cSlot;
if(LFirst(&(pt->tbl[hv]), &cSlot))
{
if(cSlot.key == k)
{
return cSlot.val;
}
else
{
while(LNext(&(pt->tbl[hv]), &cSlot))
{
if(cSlot.key == k)
return cSlot.val;
}
}
}
return NULL;
}