khon98 2020. 12. 19. 15:20

해시(Hash)

- 데이터를 최대한 빠른 속도로 관리하도록 도와주는 자료 구조

- 해시를 사용하면 메모리 공간이 많이 소모되지만 매우 빠른 속도로 데이터를 처리할 수 있음

- 빠르게 데이터에 접근할 수 있다는 점에서 데이터베이스 등의 소프트웨어에서 활용

 

해시 함수 동작 과정

- 특정한 값을 찾고자 할 때는 그 값의 키로 접근할 수 있음

- 일반적으로 해시 함수는 모듈로 연산 등의 수학적 연산으로 이루어져 있으므로 O(1)만에 값에 접근할 수 있음

 

* 특정한 값이 들어왔을 때 그 값에 10을 나눈 나머지 값이 키가 될 수 있도록 했다고 가정

입력 데이터 >  해시 함수(n mod 10)  > 키(Key) | 값(Value)

78 / khon                                              2           | khon01

12 / khon01                                          4           | khon03

55 / khon02                                         5           | khon02

64 / khon03                                         8           | khon

 

해시의 충돌

- 해시 함수의 입력 값으로는 어떠한 값이나 모두 들어갈 수 있지만 해시 함수를 거쳐 생성되는 키의 경우의 수는 한정적이기 때문에 키 중복이 발생할 수 있음

- 해시 테이블에서 키가 중복되는 경우 충돌(Collision)이 발생했다고 표현

- 동일한 데이터 값이 들어갈 경우 충돌

 

생일의 역설

- 무작위의 사람 57명을 한 곳에 모아 놓으면, 2명의 생일이 같을 확률이 99%

- 이를 해싱 과정과 비교하자면 '365로 나눈 나머지'를 키로 이용하는 해시 함수의 경우 입력 데이터가 57개만 되어도 사실상 충돌이 무조건 발생한다고 판단해야 하는 것

 

해싱(Hashing)

- 해싱 알고리즘은 나눗셈 법이 가장 많이 활용됨

- 입력 값을 테이블의 크기로 나눈 나머지를 키로 이용하는 방식

- 테이블의 크기는 소수로 설정하는 것이 효율성이 높음

 

충돌을 처리하는 방법

- 아무리 잘 만들어진 해시 함수라고 해도 충돌이 발생할 수밖에 없음

- 충돌을 처리하는 방법은 다음과 같이 두 가지 유형으로 분류할 수 있음

1. 충돌을 발생시키는 항목을 해시 테이블의 다른 위치에 저장 : 선형 조사법, 이차 조사법 등

2. 해시 테이블의 하나의 버킷에 여러 개의 항목을 저장 : 체이닝 등

 

선형 조사법

- 데이터가 충돌할 경우 그다음 데이터 비어있다면 그곳에 데이터를 넣음

// 선형 조사법 구조체 정의
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define TABLE_SIZE 10007
#define INPUT_SIZE 5000

typedef struct {
    int id;
    char name[20];
} Student;

// 해시 테이블 초기화
void init(Student** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        hashTable[i] = NULL;
    }
}

// 해시 테이블 메모리 반환
void destructor(Student** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (hashTable[i] != NULL) {
            free(hashTable[i]);
        }
    }
}

// 해시 테이블 내 빈 공간을 선형 탐색으로 찾음
int findEmpty(Student** hashTable, int id) {
    int i = id % TABLE_SIZE;
    while (1) {
        if (hashTable[i % TABLE_SIZE] == NULL) {
            return i % TABLE_SIZE;
        }
        i++;
    }
}

// 특정한 ID 값에 매칭되는 데이터를 해시 테이블 내에서 찾음
int searh(Student** hashTable, int id) {
    int i = id % TABLE_SIZE;
    while (1) {
        if (hashTable[i % TABLE_SIZE] == NULL) return -1;
        else if (hashTable[i % TABLE_SIZE]->id == id) return i;
        i++;
    }
}

// 특정한 키 인덱스에 데이터를 삽입
void add(Student** hashTable, Student* input, int key) {
    hashTable[key] = input;
}

// 해시 테이블에서 특정한 키의 데이터를 반환
Student* getValue(Student** hashTable, int key) {
    return hashTable[key];
}

// 선형 조사법 데이터 출력 함수
// 해시 테이블에 존재하는 모든 데이터를 출력
void show(Student** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (hashTable[i] != NULL) {
            printf("키: [%d] 이름: [%s]\n", i, hashTable[i]->name);
        }
    }
}

int main(void) {
    Student **hashTable;
    hashTable = (Student**)malloc(sizeof(Student) * TABLE_SIZE);
    init(hashTable);
    
    for (int i = 0; i < INPUT_SIZE; i++) {
        Student* student = (Student*)malloc(sizeof(Student));
        student->id = rand() % 1000000;
        sprintf(student->name, "사람%d", student->id);
        if (searh(hashTable, student->id) == -1) { // 중복되는 ID는 존재하지 않도록 함
            int index = findEmpty(hashTable, student->id);
            add(hashTable, student, index);
        }
    }
    
    show(hashTable);
    destructor(hashTable);
    system("pause");
    return 0;
}

 

선형 조사법의 단점

- 충돌이 발생하기 시작하면 유사한 값을 가지는 데이터끼리 서로 밀집되는 집중 결함 문제가 존재

- 메모리를 많이 소모한다면 충돌이 적게 발생하므로 매우 바른 데이터 접근 속도를 가질 수 있음

- 일반적인 경우 해시 테이블의 크기는 한정적이므로 충돌이 최대한 적게 발생하도록 해야 함

 

이차 조사법

- 선형 조사법을 약간 변형한 형태로 키 값을 선택할 때 완전 제곱수를 더해 나가는 방식

- 데이터를 입력하다가 충돌이 발생할 경우 1만큼 떨어진 공간에서 확인을 하고 그 자리에 숫자가 차 있다면 2의 제곱만큼 넘어서 자리를 확인 

- 기존에 발생했던 데이터가 밀집되는 현상이 줄어들기 때문에 해시 충돌이 적게 발생한다는 특징이 있음

 

조사법의 테이블 크기 재설정

- 일반적으로 선형 조사법, 이차 조사법 등의 조사법에서 테이블이 가득 차게 되면 테이블의 크기를 확장하여 계속해서 해시 테이블을 유지할 수 있도록 함

 

체이닝(Chaining)

- 연결 리스트를 활용해 특정한 키를 가지는 항목들을 연결하여 저장함

- 연결 리스트를 사용한다는 점에서 삽입과 삭제가 용이함

- 테이블 크기 재설정 문제는 동적 할당을 통해 해결할 수 있음

- 동적 할당을 위한 추가적인 메모리 공간이 소모된다는 단점이 있음

- 충돌이 발생하더라도 무시하고 키 값에 해당하는 연결 리스트에 계속해서 데이터를 넣음

// 체이닝 구조체 정의
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define TABLE_SIZE 10007
#define INPUT_SIZE 5000

typedef struct {
    int id;
    char name[20];
} Student;


typedef struct {
    Student* data;
    struct Bucket* next;
} Bucket;


// 해시 테이블 초기화
void init(Bucket** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        hashTable[i] = NULL;
    }
}

// 해시 테이블 메모리 반환
void destructor(Bucket** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (hashTable[i] != NULL) {
            free(hashTable[i]);
        }
    }
}

// 체이닝 데이터 탐색 함수
int isExist(Bucket** hashTable, int id) {
    int i = id % TABLE_SIZE;
    if (hashTable[i] == NULL) return 0;
    else {
        Bucket *cur = hashTable[i];
        while (cur != NULL) {
            if (cur->data->id == id) return 1;
            cur = cur->next;
        }
    }
    return 0;
}

// 체이닝 데이터 삽입 함수
// 특정한 키 인덱스에 데이터를 삽입
void add(Bucket** hashTable, Student* input) {
    int i = input->id % TABLE_SIZE;
    if (hashTable[i] == NULL) {
        hashTable[i] = (Bucket*)malloc(sizeof(Bucket));
        hashTable[i]->data = input;
        hashTable[i]->next = NULL;
    }
    else {
        Bucket *cur = (Bucket*)malloc(sizeof(Bucket));
        cur->data = input;
        cur->next = hashTable[i];
        hashTable[i] = cur;
    }
}

// 체이닝 데이터 출력 함수
// 해시 테이블에 존재하는 모든 데이터를 출력
void show(Bucket** hashTable) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        if (hashTable[i] != NULL) {
            Bucket *cur = hashTable[i];
            while (cur != NULL) {
                printf("키: [%d] 이름: [%s]\n", i, cur->data->name);
                cur = cur->next;
            }
        }
    }
}

int main(void) {
    Bucket **hashTable;
    hashTable = (Bucket**)malloc(sizeof(Bucket) * TABLE_SIZE);
    init(hashTable);
    
    for (int i = 0; i < INPUT_SIZE; i++) {
        Student* student = (Student*)malloc(sizeof(Student));
        student->id = rand() % 1000000;
        sprintf(student->name, "사람%d", student->id);
        if (!isExist(hashTable, student->id)) { // 중복되는 ID는 존재하지 않도록 함
            add(hashTable, student);
        }
    }
    show(hashTable);
    destructor(hashTable);
    system("pause");
    return 0;
}