공부/Data Structure

해시 테이블(Hash Table)

sudo 2021. 8. 12. 04:16
// Hash.h

#pragma once

#include <string>

class CHash
{
	
public:
	CHash()
	{

	}

	~CHash()
	{

	}

private:
	unsigned __int64 m_HashKey;
	
public:
	template <typename KEY>
	unsigned __int64 GetHash(KEY key)
	{
		m_HashKey = 0;

		size_t	Length = sizeof(key);

		// unsigned __int64 NewKey = (unsigned __int64)key;

		for (int i = 0; i < Length; ++i)
		{
			unsigned char data = key & 0xff;

			m_HashKey += data;

			key = key >> 8;
		}
		return m_HashKey;
	}

	template <>
	unsigned __int64 GetHash(const char* key)
	{
		m_HashKey = 0;

		size_t	Length = strlen(key);

		for (size_t i = 0; i < Length; ++i)
		{
			if (i % 2 == 0)
				m_HashKey += (unsigned __int64)key[i];

			else
				m_HashKey *= (unsigned __int64)key[i];
		}

		return m_HashKey;
	}

	template<>
	unsigned __int64 GetHash(std::string key)
	{
		m_HashKey = 0;

		size_t	Length = key.length();

		for (size_t i = 0; i < Length; ++i)
		{
			if (i % 2 == 0)
				m_HashKey += (unsigned __int64)key[i];

			else
				m_HashKey *= (unsigned __int64)key[i];
		}

		return m_HashKey;
	}
};

 

// HashTable.h

#pragma once

#include "Hash.h"
#include "List.h"

template <typename KEY, typename VALUE>
class CHashNode
{
	template <typename KEY, typename VALUE, int HASHSIZE>
	friend class CHashTable;

private:
	CHashNode()
	{

	}
	
	~CHashNode()
	{
		typename CList<PNODE>::iterator iter = m_Chain.begin();
		typename CList<PNODE>::iterator iterEnd = m_Chain.end();

		for (; iter != iterEnd; ++iter)
		{
			delete *iter;
		}
	}
private:
	typedef CHashNode<KEY, VALUE>* PNODE;
	typedef CHashNode<KEY, VALUE> NODE;

public:
	KEY first;
	VALUE second;

private:
	CList<PNODE> m_Chain;

public:
	// chain에 노드를 추가해주는 함수
	void Add(const KEY& key, const VALUE& value)
	{
		PNODE NewNode = new NODE;
		NewNode->first = key;
		NewNode->second = value;

		m_Chain.push_back(NewNode);
	}

	// chain에서 key를 갖고 있는 Node를 찾아서 반환
	PNODE Get(const KEY& key)
	{
		typename CList<CHashNode<KEY, VALUE>*>::iterator iter = m_Chain.begin();
		typename CList<CHashNode<KEY, VALUE>*>::iterator iterEnd = m_Chain.end();

		for (; iter != iterEnd; ++iter)
		{
			if ((*iter)->first == key)
			{
				return *iter;
			}
		}

		return nullptr;
	}
};

template <typename KEY, typename VALUE>
class CHashIterator
{
	template <typename KEY, typename VALUE, int HASHSIZE>
	friend class CHashTable;

public:
	CHashIterator()
	{
		m_Node = nullptr;
	}
	~CHashIterator()
	{

	}

private:
	CHashNode<KEY, VALUE>* m_Node;

public:
	bool operator == (const CHashIterator<KEY, VALUE>& iter)	const
	{
		return this->m_Node == iter.m_Node;
	}

	bool operator != (const CHashIterator<KEY, VALUE>& iter)	const
	{
		return this->m_Node != iter.m_Node;
	}

	CHashNode<KEY, VALUE>* operator * ()	const
	{
		return m_Node;
	}

};

template <typename KEY, typename VALUE, int HASHSIZE = 1223>
class CHashTable
{

public:
	CHashTable()
	{

	}
	~CHashTable()
	{

	}

private:
	typedef CHashNode<KEY, VALUE> NODE;
	typedef CHashNode<KEY, VALUE>* PNODE;

public:
	typedef CHashIterator<KEY, VALUE> iterator;

private:
	NODE m_Array[HASHSIZE];
	int m_Size;

public:
	// hash table내에서 key, value를 가진 Node를 삽입
	void insert(const KEY& key, const VALUE& value)
	{
		CHash hash;
		unsigned __int64 HashValue = hash.GetHash<KEY>(key);

		int Index = (int)(HashValue % HASHSIZE);

		m_Array[Index].Add(key, value);

		++m_Size;
	}

	// hash table내에서 key를 가진 노드를 찾아서 반환
	iterator Find(const KEY& key)
	{
		CHash hash;
		unsigned __int64 HashValue = hash.GetHash<KEY>(key);

		int Index = (int)(HashValue % HASHSIZE);

		PNODE FindNode = m_Array[Index].Get(key);

		iterator iter;
		iter.m_Node = FindNode;

		return iter;
	}

	int size()	const
	{
		return m_Size;
	}

	bool empty()	const
	{
		return m_Size == 0;
	}

	VALUE& operator [](const KEY& key)
	{
		CHash hash;
		unsigned __int64 HashValue = hash.GetHash<KEY>(key);

		int Index = (int)(HashValue % HASHSIZE);

		PNODE FindNode = m_Array[Index].Get(key);

		// 찾는 노드가 없으면 삽입해주기
		if (!FindNode)
		{
			// 어차피 value는 쓰레기 값이어도 무관
			VALUE	addValue;
			m_Array[Index].Add(key, addValue);
			FindNode = m_Array[Index].Get(key);
		}

		return FindNode->second;
	}

	bool IsValid(const iterator& iter)	const
	{
		return iter.m_Node != nullptr;
	}
};

 

리스트는 저번에 구현한 것을 그대로 이용하였다.

 

#pragma once

#include <assert.h>

// friend : 아래처럼 Listnode에 List를 friend로 지정하면 List에서는 ListNode의
// private에 접근할 수 있다.

template <typename T>
class CListNode
{
	template <typename T>
	friend class CList;

	template <typename T>
	friend class CListIterator;

	template <typename T>
	friend class CListReverseIterator;

private:
	CListNode() :
		m_Next(nullptr),
		m_Prev(nullptr)
	{
	}

	~CListNode()
	{
	}

private:
	T		m_Data;
	CListNode<T>* m_Next;
	CListNode<T>* m_Prev;
};

/*
iterator : 노드를 순차적으로 방문하기 위해서  iterator를 만들어서 제공한다.
iterator를 이용해서 앞에서 부터 혹은 뒤에서부터 노드를 순차적으로 접근할 수 있게 해준다.
*/
template <typename T>
class CListIterator
{
	template <typename T>
	friend class CList;

public:
	CListIterator() :
		m_Node(nullptr)
	{
	}

	~CListIterator()
	{
	}

private:
	CListNode<T>* m_Node;

public:
	// iterator끼리 서로 가지고 있는 노드가 같을 경우 같다고 판단한다.
	bool operator == (const CListIterator<T>& iter)	const
	{
		return m_Node == iter.m_Node;
	}

	bool operator != (const CListIterator<T>& iter)	const
	{
		return m_Node != iter.m_Node;
	}

	bool operator == (const CListNode<T>* Node)	const
	{
		return m_Node == Node;
	}

	bool operator != (const CListNode<T>* Node)	const
	{
		return m_Node != Node;
	}

	void operator ++ ()
	{
		m_Node = m_Node->m_Next;
	}

	void operator ++ (int)
	{
		m_Node = m_Node->m_Next;
	}

	void operator -- ()
	{
		m_Node = m_Node->m_Prev;
	}

	void operator -- (int)
	{
		m_Node = m_Node->m_Prev;
	}

	T& operator * ()	const
	{
		return m_Node->m_Data;
	}
};

template <typename T>
class CList
{
public:
	CList()
	{
		m_Size = 0;

		// Begin과 End노드를 생성하고 두 노드를 서로 연결한다.
		m_Begin = new NODE;
		m_End = new NODE;

		m_Begin->m_Next = m_End;
		m_End->m_Prev = m_Begin;
	}

	~CList()
	{
		// Begin노드는 이전노드가 nullptr이다.
		// End노드는 다음노드가 nullptr이다.
		PNODE	DeleteNode = m_Begin;

		while (DeleteNode)
		{
			PNODE	Next = DeleteNode->m_Next;

			delete	DeleteNode;

			DeleteNode = Next;
		}
	}

private:
	typedef CListNode<T>	NODE;
	typedef CListNode<T>* PNODE;

public:
	typedef CListIterator<T>	iterator;
	typedef CListReverseIterator<T>	reverse_iterator;

private:
	// Begin과 End는 데이터를 저장하기 위한 노드는 아니다.
	// 명시적으로 시작과 끝을 의미하는 노드로 사용하기 위해 할당해두고 사용한다.
	// 실제 데이터를 저장하는 노드는 Begin과 End노드 사이에 위치하게 될것이다.
	PNODE	m_Begin;
	PNODE	m_End;
	int		m_Size;

public:
	void push_back(const T& data)
	{
		// 데이터를 저장해둘 노드를 생성한다.
		PNODE	Node = new NODE;

		Node->m_Data = data;

		// End노드와 End노드의 이전노드 사이에 새로 생성된 노드를 추가해주도록 한다.
		PNODE	Prev = m_End->m_Prev;

		// End노드의 이전노드의 다음노드를  End에서 새로 생성된 노드로 교체한다.
		Prev->m_Next = Node;

		// 새로 생성된 노드의 이전노드를 End의 이전노드로 지정한다.
		Node->m_Prev = Prev;

		// 새로 생성된 노드의 다음 노드를 End노드르 지정한다.
		Node->m_Next = m_End;

		// End노드의 이전노드를 새로 생성한 노드로 지정한다.
		m_End->m_Prev = Node;

		++m_Size;
	}

	void push_front(const T& data)
	{
		PNODE	Node = new NODE;

		Node->m_Data = data;

		// Begin노드와 Begin노드의 다음노드 사이에 새로 생성된 노드를 추가한다.
		PNODE	Next = m_Begin->m_Next;

		// 새로 생성된 노드의 다음노드로 Begin노드의 다음노드를 지정한다.
		Node->m_Next = Next;

		// Begin노드의 다음노드의 이전노드를 새로 생성된 노드로 지정한다.
		Next->m_Prev = Node;

		// Begin노드의 다음노드로 새로 생성된 노드를 지정한다.
		m_Begin->m_Next = Node;

		// 새로 생성된 노드의 이전노드로 Begin노드를 지정한다.
		Node->m_Prev = m_Begin;

		++m_Size;
	}

	// 마지막 노드를 제거한다.
	void pop_back()
	{
		if (empty())
		{
			assert(false);
		}

		// End노드의 이전 노드를 지워준다. 그러므로 End의 이전노드를 얻어오고
		// End의 이전 노드의 이전노드를 얻어와서 End와 연결해준다.
		PNODE	DeleteNode = m_End->m_Prev;

		PNODE	Prev = DeleteNode->m_Prev;

		// 지울노드의 이전노드의 다음노드를 End노드로 지정한다.
		Prev->m_Next = m_End;

		// End노드의 이전노드로 지울노드의 이전노드를 지정한다.
		m_End->m_Prev = Prev;

		// 노드를 제거한다.
		delete	DeleteNode;

		--m_Size;
	}

	void pop_front()
	{
		if (empty())
		{
			assert(false);
		}

		// Begin노드의 다음 노드를 지워준다.
		PNODE	DeleteNode = m_Begin->m_Next;

		// 지울 노드의 다음 노드를 얻어온다.
		PNODE	Next = DeleteNode->m_Next;

		// Begin노드의 다음노드로 지울 노드의 다음노드를 지정한다.
		m_Begin->m_Next = Next;

		// 지울노드의 다음노드의 이전노드로 Begin노드를 지정한다.
		Next->m_Prev = m_Begin;

		// 노드를 제거한다.
		delete	DeleteNode;

		--m_Size;
	}

	const T& front()	const
	{
		if (empty())
			assert(false);

		return m_Begin->m_Next->m_Data;
	}

	const T& back()	const
	{
		if (empty())
			assert(false);

		return m_End->m_Prev->m_Data;
	}


	// 클래스의 멤버함수는 함수의 뒤에 const를 붙일 수 있다.
	// 멤버함수의 뒤에 const가 붙으면 이 함수에서는 멤버변수의 값을 변경할 수 없게 된다.
	// const 객체가 만들어진 경우라면 뒤에 const가 붙은 멤버함수만 호출이 가능하다.
	int size()	const
	{
		return m_Size;
	}

	bool empty()	const
	{
		return m_Size == 0;
	}

	// 추가된 모든 노드를 제거한다.
	void clear()
	{
		if (empty())
			return;

		PNODE	DeleteNode = m_Begin->m_Next;

		// End노드가 아니라면 while을 돌면서 제거해주도록 한다.
		while (DeleteNode != m_End)
		{
			// 지울 노드의 다음노드를 미리 받아둬야 한다.
			// 왜냐하면 노드를 먼저 지워버린다면 다음노드를 얻어올 수 없기 때문이다.
			PNODE	Next = DeleteNode->m_Next;

			// 노드를 제거한다.
			delete	DeleteNode;

			// 지울 노드를 다음 노드로 갱신한다.
			DeleteNode = Next;
		}

		// Begin노드와 End노드를 서로 연결해준다.
		m_Begin->m_Next = m_End;
		m_End->m_Prev = m_Begin;

		// 노드의 개수를 0으로 초기화해준다.
		m_Size = 0;
	}

	iterator begin()	const
	{
		iterator	iter;
		iter.m_Node = m_Begin->m_Next;
		return iter;
	}

	iterator end()	const
	{
		iterator	iter;
		iter.m_Node = m_End;
		return iter;
	}

	reverse_iterator rbegin()	const
	{
		reverse_iterator	iter;
		iter.m_Node = m_End->m_Prev;
		return iter;
	}

	reverse_iterator rend()	const
	{
		reverse_iterator	iter;
		iter.m_Node = m_Begin;
		return iter;
	}

	void operator = (const CList<T>& list)
	{
		// 기존 정보를 제거한다.
		clear();

		m_Size = list.m_Size;

		PNODE	Node = list.m_Begin->m_Next;

		while (Node != list.m_End)
		{
			push_back(Node->m_Data);
			Node = Node->m_Next;
		}
	}

	// 삭제한 노드의 다음 노드를 가지고 있는 iterator를 반환해준다.
	iterator erase(const T& data)
	{
		iterator	iter = Find(data);

		if (iter == end())
			return iter;

		return erase(iter);
	}

	iterator erase(const iterator& iter)
	{
		if (iter == end() || iter == m_Begin)
			return end();

		// 지울 노드의 이전노드와 다음노드를 얻어온다.
		// 지울 노드의 이전노드와 다음노드를 서로 연결시켜준다.
		PNODE	Prev = iter.m_Node->m_Prev;
		PNODE	Next = iter.m_Node->m_Next;

		Prev->m_Next = Next;
		Next->m_Prev = Prev;

		delete	iter.m_Node;

		--m_Size;

		iterator	result;
		result.m_Node = Next;

		return result;
	}

	iterator Find(const T& data)
	{
		iterator	iter = begin();
		iterator	iterEnd = end();

		for (; iter != iterEnd; ++iter)
		{
			if (*iter == data)
				return iter;
		}

		// 못찾을 경우 end를 리턴한다.
		return iterEnd;
	}

	// 정렬
	void sort(bool (*pFunc)(const T&, const T&))
	{
		iterator	iter1 = begin();
		iterator	iter1End = end();
		--iter1End;

		iterator	iter2;
		iterator	iter2End = end();

		// 예를 들어 10개라면 0 ~ 8 까지만 반복한다.
		// 마지막 9번은 반복을 안한다.
		for (; iter1 != iter1End; ++iter1)
		{
			iter2 = iter1;
			++iter2;
			for (; iter2 != iter2End; ++iter2)
			{
				if (pFunc(*iter1, *iter2))
				{
					T	data = *iter1;
					*iter1 = *iter2;
					*iter2 = data;
				}
			}
		}
	}
};
#include <iostream>
#include "HashTable.h"

int main()
{
	CHashTable<const char*, const char*>	Table;

	Table.insert("김철수", "010-1234-5678");
	Table.insert("최영희", "010-4494-4494");
	Table.insert("이준수", "010-1234-8888");
	Table.insert("박지훈", "010-3333-4444");

	std::cout << Table["김철수"] << std::endl;
	std::cout << Table["이준수"] << std::endl;

	Table["정민수"] = "010-1111-1111";

	std::cout << Table["정민수"] << std::endl;


	CHashTable<const char*, const char*>::iterator	iter = Table.Find("최영희");

	if (Table.IsValid(iter))
		std::cout << "최영희는 정상적인 키값입니다." << std::endl;

	iter = Table.Find("이미영");

	if (Table.IsValid(iter))
		std::cout << "이미영은 정상적인 키값입니다." << std::endl;

	else
		std::cout << "이미영은 잘못된 키값입니다." << std::endl;


	return 0;
}

main.cpp 출력 결과

 

해시 테이블은 탐색이 O(1)에 가능하다는 장점을 갖고 있다. 물론 충돌이 많고, chaining 기법을 이용했을 때 하나의 리스트에 노드가 다 들어가있다면 사실상 hash table의 장점을 전혀 못살리게 될 것이다. 그래서 hashing 함수가 정말 중요하다. 여기선 숫자는 8bit씩 AND연산으로 누적해서 더하는 방식으로, 문자열은 짝수 인덱스는 누적해서 더하고, 홀수 인덱스는 누적해서 곱하는 형식으로 매우 간단하게 구현했지만 많은 노드가 삽입된다면 충돌 피할 수 없을 것이며, 충돌을 최소화 하려면 더 정교한 hashing 함수를 사용해야 할 것이다.