데이터를 효율적으로 저장하고 검색할 수 있는 자료구조이다
KEY 값과 VALUE값을 사용하여 값을 저장한다
- Key : 데이터 고유의 식별자 (Key값을 통해 value를 알아낸다)
- Value : 실제 저장될 데이터

template를 사용하여 메인에서 자료형을 지정하고 Key값과 Value를 받을 것이다
우리가 필요한 것은 노드들을 저장해둘 버킷, 키값과 벨류값을 가진 노드 구조를 만들어야한다
Node 구조체와 Bucket 구조체를 생성
생성자에서 bucket의 배열안의 내용을 초기화
#include <iostream>
#define SIZE 6
using namespace std;
template <typename KEY, typename VALUE>
// 배열로 구성
// 체이닝 기법은 단일연결리스트로 찾기
class HashTable
{
private:
struct Node
{
KEY key;
VALUE value;
Node* next;
};
struct Bucket
{
int count;
Node* head;
};
Bucket bucket[SIZE];
public:
HashTable()
{
for (int i = 0; i < SIZE; i++)
{
bucket[i].count = 0;
bucket[i].head = nullptr;
}
}
}
키값을 넣으면 해쉬인덱스를 0 ~ 5 사이의 값으로 반환되게 하고싶다!
// 0 ~ 5 사이의 값 반환 (인덱스)
int HashFunction(T key)
{
// 강제 형변환
int hashIndex = (int)key % SIZE;
return hashIndex;
}
// 템플릿 특수화
template<>
// 문자열 받기
int HashFunction(const char* key)
{
int value = 0;
for (int i = 0; i < strlen(key); i++)
{
value += key[i];
}
int hashIndex = value % SIZE;
return hashIndex;
}
// 새 노드 만들기
Node* CreateNode(KEY key, VALUE value)
{
Node* newNode = new Node;
newNode->key = key;
newNode->value = value;
newNode->next = nullptr;
return newNode;
}
void Insert(KEY key, VALUE value)
{
// 해시 함수를 통해서 값을 받는 임시 변수
int hashIndex = HashFunction(key);
// 새로운 노드를 생성합니다.
Node* newNode = CreateNode(key, value);
// 노드가 1개라도 존재하지 않는다면
if (bucket[hashIndex].head == nullptr)
{
// bucket[hashIndex]의 head 포인터가 newNode를 가리키게 합니다.
bucket[hashIndex].head = newNode;
}
else
{
// 새 노드의 next는 bucket의 head만 주소를 알고있음
newNode->next = bucket[hashIndex].head;
// 새 노드로 head 옮겨주기
bucket[hashIndex].head = newNode;
}
// bucket[hashIndex]의 count를 증가시킵니다.
bucket[hashIndex].count++;
}



서로 다른 키가 같은 해시인덱스 값을 가질때 충돌이 일어난다!
이를 해결하기 위해 체이닝(chaining) 기법을 사용
즉, 버킷을 연결리스트로 만들어서, 충돌이 발생할 때 동일한 해시 버킷에 여러 키값을 저장하는 것이다

void Remove(KEY key)
{
// 1. 해시 함수를 통해서 값을 받는 임시 변수
int hashIndex = HashFunction(key);
// 2. Node를 탐색할 수 있는 포인터 변수 선언
Node* currentNode = bucket[hashIndex].head;
// 3. 이전 Node를 저장할 수 있는 포인터 변수 선언
Node* previousNode = nullptr;
// 4. currentNode가 nullptr과 같다면 함수를 종료합니다.
if (currentNode == nullptr)
{
cout << "Not Key Found" << endl;
return;
}
else
{
// 5. currentNode를 이용해서 내가 찾고자 하는 Key값을 찾습니다.
while (currentNode != nullptr)
{
if (currentNode->key == key)
{
// head가 찾고하자하는 Key값일때
if (currentNode == bucket[hashIndex].head)
{
bucket[hashIndex].head = currentNode->next;
}
else
{
previousNode->next = currentNode->next; // nullptr
}
bucket[hashIndex].count--;
delete currentNode;
return;
}
else
{
previousNode = currentNode;
currentNode = currentNode->next;
}
}
cout << "Not Key Found" << endl;
}
}
노드를 탐색할 수 있는 currentNode 생성
currentNode의 이전노드를 알려주는 previousNode 생성
원하는 값을 currentNode가 찾을 때 까지 previousNode 와 currentNode를 갱신 시켜줘야 한다
-> previousNode가 필요 한 이유 !!
-> 내가 지우고자 하는 값이 head가 가리키는 곳이면 상관이 없지만, 만약 내가 지우고 싶은 값이 노드들의 중간쯤에 있다고 하면, currentNode로 내가 지우고 싶은 값에 접근하여서 지우면 currentNode의 이전노드가 가리킬 수 있는 곳이 없다(정보가 없기 때문)
만약 currentNode가 가리키는 값이 head이고, 내가 지우고자 하는 값일때
head를 currentNode의 next로 옮겨주고 delete


만약, 내가 지우고자 하는 값이 currentNode에 왔고, head가 아닐경우
previousNode->next를 nullptr로 가리킨다
while문이 currentNode != nullptr조건이기 때문에 currentNode의 next는 nullptr이다


void Show()
{
// 1. currentNode 생성 0~ 5까지 bucket.head == nullptr이면 노드 출력
for (int i = 0; i < SIZE; i++)
{
Node* currentNode = bucket[i].head;
while (currentNode != nullptr)
{
cout << "[" << i << "]" << " " << "{KEY : " << currentNode->key
<< "VALUE : " << currentNode->value << "} ";
currentNode = currentNode->next;
}
cout << endl;
}
}



// 소멸자
~HashTable()
{
for (int i = 0; i < SIZE; i++)
{
Node* deleteNode = bucket[i].head;
Node* nextNode = bucket[i].head;
if (bucket[i].head == nullptr)
{
continue;
}
else
{
while (nextNode != nullptr)
{
nextNode = deleteNode->next;
delete deleteNode;
deleteNode = nextNode;
}
}
}
}
코드를 보고 그림을 그려봤을때, nextNode가 이동하고 deleteNode가 가리키는 곳의 데이터를 해제하였을때, head가 데이터를 해제한 부분을 가리키고 있어서 문제가 될 거 라고 생각했다
-> 소멸자에서 구현하는 거기 때문에 추후 다른 걸 사용하는 게 아니어서 이대로도 상관이없긴 하다.
-> 걱정되면 head 를 nullptr로 가리키게 해주자
int main()
{
HashTable<const char*, int> hashTable;
hashTable.Insert("Sword", 10000);
hashTable.Insert("Armor", 30000);
hashTable.Insert("Potion", 70000);
hashTable.Remove("Sword");
hashTable.Remove("Gloves");
hashTable.Show();
return 0;
}
출력값: Gloves는 없는 키 값
해시 함수가 충분히 괜찮은 성능을 보일 때, 임의의 키값에 대한 해시값이 어떤 특정한 값이 될 확률은 1/M 이다 (해시충돌이 일어날 확률이 1/M 이라는 것)
-> 단순 균일 해시 가정을 만족한다면 O(1) 시간복잡도가 지켜진다
단순 균일 해시 가정: 해시 함수가 키를 균등하게 분포시킨다고 가정하는 것
입력값을 알 때는 출력값을 쉽게 구할 수 있지만, 출력값만 가지고는 입력값을 되찾는 게 계산적으로 불가능해야 한다.
입력 공간에 비해 출력 공간(고정된 해시 값 크기)은 훨씬 작기 때문에 여러 입력이 같은 해시 값으로 충돌하기 때문이다.
해시 충돌이 발생하면 해결방법으로 위에서 사용한 체이닝 기법도 있지만, 개방주소법도 사용할 수 있다
개방주소법 (Open Addressing) : 해시 충돌이 발생하면 충돌이 일어난 버킷을 피해서 다른 버킷에 데이터를 저장하는 방식