본문 바로가기
Study/알고리즘

[알고리즘] 해시 테이블(Hash Table)

by 스테디코디스트 2023. 10. 24.
반응형

1. 해시 테이블

- 완전 탐색(브루트 포스)으로 시간초과에 빠지게 되는 문제들을 풀기 위해 필요함

- Key, Value로 데이터를 저장하는 자료구조

- 데이터를 빠르게 검색할 수 있음

- 각 key에 따라 고유한 index를 생성하고 이 index 값을 활용해 값을 저장하거나 검색한다.

- 미리 key값으로 데이터들을 저장해 놓으면 데이터를 찾기 쉽다.

- 평균 시간복잡도 : O(1)

 

2. 해시 함수에서 고유 인덱스 값을 설정하는 방법

1) Division Method

- 나눗셈을 이용하는 방법

- 입력값을 테이블의 크기로 나누어 계산

- 테이블의 크기를 소수로 정하고 2의 제곱수와 먼 값을 사용해야 효과가 좋다고 알려짐

 

2) Digit Folding

- 각 key의 문자열을 ASCII 코드로 바꾸고 값을 합한 데이터를 테이블 내의 주소로 사용하는 방법

 

3) Multiplication Method

- 숫자로 된 key값 k와 0과 1사이의 실수 A, 보통 2의 제곱수인 m을 사용하여 아래와 같이 계산

- h(k) = (kAmod1) x m (mod는 moduler의 약자로 나머지를 뜻함)

 

4) Univeral Hashing

- 다수의 해시함수를 만들어 집합 H에 넣어두고, 무작위로 해시함수를 선택해 해시값을 만드는 기법

 

3. 해시 값이 충돌할 경우 해결법

1) 분리 연결법(Separate Chaining)

- 동일한 버킷(Value를 담고있는 곳)의 데이터에 대해 자료구조를 활용해 추가 메모리를 사용하여 다음 데이터 주소를 저장하는 방법

- 해시 테이블의 확장이 필요없고 간단하게 구현 가능하며 손쉽게 삭제할 수 있음

- 데이터의 수가 많아지면 동일한 버킷에 chaining되는 데이터가 많아져 캐시의 효율성이 감소

 

2) 개방 주소법(Open Addressing)

- 비어있는 해시 테이블의 공간을 활용하는 방법

[1] Linear Probing : 현재의 버킷 index로부터 고정폭 만큼씩 이동하여 차례대로 검색해 비어있는 버킷에 데이터를 저장

[2] Quadratic Probing : 해시의 저장순서 폭을 제곱으로 저장하는 방식

[3] Double Hashing Probing : 해시된 값을 한번 더 해싱하여 해시의 규칙성을 없애버리는 방식

 

4. 소스코드(C++)

#include <iostream>
#include <vector>
#include <string>

using namespace std;

static const int HASH_SIZE = 1000; // 가질 수 있는 키 값의 갯수
static const int HASH_LEN = 400; // 중복될 수 있는 키 값의 갯수
static const int HASH_VAL = 17; // 소수로 설정 -> key값을 구하기 위해 사용

static int _data[HASH_SIZE][HASH_LEN];
static int _length[HASH_SIZE];

static string s_data[HASH_SIZE][HASH_LEN];
static string str;

// 해시키를 구하는 함수
int getHashKey(string str)
{	
    int key = 0;
    
    for(int i = 0; i < str.length(); i++)
    {
    	// 해시 키 값 생성
    	key = (key * HASH_VAL) + str[i] + HASH_VAL;
    }
    
    // 키가 음수인 경우 양수로 변경
    if(key < 0) key = -key;
    
    // 키가 너무 큰 경우를 대비해 HASH_SIZE보다 작도록 HASH_SIZE로 나눈 나머지 사용
    return key % HASH_SIZE;
}

// 키가 중복되는지 확인하는 함수
int checking(int key)
{	
    // 현재 key로 찾은 value 값
    int len = _length[key];
    
    // 이미 들어온 적이 있는 경우
    if(len != 0)
    {
    	for(int i = 0; i < len; i++)
        {
        	// 이미 들어온 문자열이 해당 key 배열에 있는지 확인
        	if(str == s_data[key][i])
            {
            	// 같은 문자열이 있다면 현재 key의 index를 증가시킴
				_data[key][i]++;
                return _data[key][i];
			}
		}
        
        // 들어온 적이 없었던 경우 해당 key 배열의 문자열을 저장하고 배열의 길이를 늘림
        s_data[key][_length[key]++] = str;
        
        // 처음 들어가는 경우에는 -1을 리턴
        return -1;
    }
}

int main()
{	
    int N; // 반복 횟수
    
    for(int i = 0; i < N; i++)
    {
    	cin >> str; // 사용자의 입력 문자열을 받아옴
        
        int key = getHashKey(str); // 입력받은 문자열의 키값
        int cnt = checking(key); // 키값의 중복 체크(같은 키 값중 몇번째인지를 의미)
        
        // 처음 들어가는 경우가 아닌 경우 -> 중복 키 값이 있는 경우
        if(cnt != -1)
        {
        	// 복수 키 값인 경우
        	cout << key << " ";
            cout << cnt << endl;
		}
        else 
        {
        	// 단일 키 값인 경우
        	cout << "OK" << endl;
        }        
    }
    
    return 0;
}