해시 테이블과 블룸 필터

0. 들어가며

  • 룩업(조회) : 특정 원소가 컨테이너에 있는지 확인하거나 또는 컨테이너에서 특정 키에 해당하는 값을 찾는 작업
  • 해당 작업을 훨씬 빠르게 수행할 수 있는 해시 테이블과 블룸 필터 구현

1. 해시 테이블

  • 선형 검색 O(N) / BST 검색 O(log N)
  • 해시 테이블의 핵심은 해싱

1) 해싱

  • 각각의 데이터를 가급적 고유한 숫자 값으로 표현, 같은 숫자 값을 사용하여 데이터의 유무를 확인하거나 원본 데이터를 추출하는 작업
  • 가장 간단한 해시 함수는 나머지를 반환하는 모듈러함수(%)를 사용
  • x를 입력 → (x%n) 연산 결과 반환 (해시 값 반환) → (x%n)위치에 x 값 삽입
  • 모듈러 함수의 문제점은 서로 다른 데이터에 대해 같은 해시 값 반환 가능 (충돌 가능)
  • 충돌 : 해시 함수가 서로 다른 키에 대해 같은 해시 값을 반환하여 다수의 키가 같은 해시 값을 갖게 된는 현상

2) 연습 문제 13: 정수 값을 저장하는 간단한 사전

#include<iostream>
#include<vector>

using uint=unsigned int;

class hash_map
{
	std::vector<int> data;
public:
	hash_map(size_t n)
	{
		data = std::vector<int>(n,-1);
	}
	void insert(uint value)
	{
		int n = data.size();
		data[value%n] = value;
		std::cout<<value<<"을 삽입했습니다."<<std::endl;
	}
	bool find(uint value)
	{
		int n = data.size();
		return (data[value%n] == value);
	}
	void erase(uint value)
	{
		int n = data.size();
		if(data[value%n]==value)
		{
			data[value%n]=-1;
			std::cout<<value<<"을 삭제했습니다."<<std::endl;
		}
	}
};

int main()
{
	hash_map map(7);
	auto print = [&](int value){
		if(map.find(value))
			std::cout<<"해시 맵에서 "<<value<<"을 찾았습니다."<<std::endl;
		else
			std::cout<<"해시 맵에서 "<<value<<"을 찾지 못했습니다."<<std::endl;
		std::cout<<std::endl;
	};
	map.insert(2);
	map.insert(25);
	map.insert(10);
	print(25);
	
	map.insert(100);
	print(100);
	print(2); // 2를 찾지 못함 -> 100에 의해 덮어 쓰워짐
	
	map.erase(25);
}

2. 해시 테이블에서 충돌

1) 체이닝

  • 해시테이블의 특정 위치에서 하나의 키를 저장하는 것이 아니라 하나의 연결 리스트를 저장
  • 충돌 발생 시 리스트의 맨 뒤에 새로운 키를 추가

2) 연습 문제 14: 체이닝을 사용하는 해시 테이블

#include<iostream>
#include<vector>
#include<list>
#include<algorithm>

using uint=unsigned int;

class hash_map
{
	std::vector<std::list<int>> data;
public:
	hash_map(size_t n)
	{
		data.resize(n);
	}
	void insert(uint value)
	{
		int n = data.size();
		data[value%n].push_back(value);
		std::cout<<value<<"을 삽입했습니다."<<std::endl;
	}
	bool find(uint value)
	{
		int n = data.size();
		auto& entries = data[value%n];
		return std::find(entries.begin(),entries.end(),value) != entries.end();
	}
	void erase(uint value)
	{
		int n = data.size();
		auto& entries = data[value%n];
		auto iter = std::find(entries.begin(),entries.end(),value);
		if(iter != entries.end())
		{
			entries.erase(iter);
			std::cout<<value<<"을 삭제했습니다."<<std::endl;
		}
	}
};

int main()
{
	hash_map map(7);
	auto print = [&](int value){
		if(map.find(value))
			std::cout<<"해시 맵에서 "<<value<<"을 찾았습니다."<<std::endl;
		else
			std::cout<<"해시 맵에서 "<<value<<"을 찾지 못했습니다."<<std::endl;
		std::cout<<std::endl;
	};
	map.insert(2);
	map.insert(25);
	map.insert(10);
	
	map.insert(100);
	map.insert(55);
	print(100);
	print(2);
	
	map.erase(2);
}
  • 모든 키가 같은 해시 값을 가질 경우 룩업 연산 O(N)
  • 부하율 = 전체 키 개수 / 해시 테이블의 크기
  • 테이블 크기를 적절히 조절하여 사용

3) 열린 주소 지정

  • 체이닝과 다르게 모든 원소를 해시 테이블 내부에 저장
  • 특정 해시 값에 해당하는 위치가 이미 사용하고 있을 경우 테이블의 다른 비어있는 위치 탐색

(1) 선형 탐색

  • 충돌이 발생하면 해당 위치에서 하나씩 다음 셀로 이동하며 비어있는 지 확인
  • 특정 위치에 클러스터링(군집화)의 문제가 발생

(2) 이차 함수 탐색

  • 선형 방정식이 아닌 이차 방정식을 사용하여 탐색 수행
  • 이미 사용 중인 경우 hash(x+1^2) → hash(x+2^2) → hash(x+3^2)와 같이 수행
  • 군집이 나타날 경우는 상대적으로 줄어듬

4) 뻐꾸기 해싱

  • 해싱 기법과 다름 점
    • 원소가 두 해시 테이블 중 어디든 저장 가능
    • 원소가 나중에 다른 위치로 이동가능
  • 룩업(조회)의 연산은 항상 O(1)
  • 원소 삽입의 경우 충돌이 발생하면 원래 있던 원소가 다른 해시 테이블로 이동
  • 뻐꾸기 해싱에서 순환이 발생가능
    • 새로운 함수를 이용하여 재해싱
    • 여러 번 재해싱도 가능

5) 연습 문제 15: 뻐꾸기 해싱

#include<iostream>
#include<vector>

class hash_map
{
	std::vector<int> data1;
	std::vector<int> data2;
	int size;
	
	int hash1(int key) const
	{
		return key%size;
	}
	int hash2(int key) const
	{
		return (key%size)%size;
	}

public:
	hash_map(int n) : size(n)
	{
		data1 = std::vector<int>(size,-1);
		data2 = std::vector<int>(size,-1);
	}

	std::vector<int>::iterator lookup(int key)
	{
		auto hash_value1 = hash1(key);
		if(data1[hash_value1]==key)
		{
			std::cout<<"1번 테이블에서 " << key <<"를 찾았습니다"<<std::endl;
			return data1.begin() + hash_value1;
		}
		
		auto hash_value2 = hash2(key);
		if(data2[hash_value2]==key)
		{
			std::cout<<"2번 테이블에서 " << key <<"를 찾았습니다"<<std::endl;
			return data2.begin() + hash_value2;
		}
		return data2.end();
	}

	void erase(int key)
	{
		auto position = lookup(key);
		if(position != data2.end())
		{
			*position = -1;
			std::cout<<key<<"에 해당하는 원소를 삭제했습니다."<<std::endl;
		}
		else
			std::cout<<key<<"키를 찾지 못했습니다."<<std::endl;
	}

	void insert(int key){
		insert_impl(key,0,1);
	}

	void insert_impl(int key, int cnt, int table)
	{
		if(cnt>=size)
		{
			std::cout<<key<<" 삽입 시 순환 발생! 재해싱이 필요합니다!" <<std::endl;
			return;
		}
		if(table == 1)
		{
			int hash = hash1(key);
			if(data1[hash] == -1)
			{
				std::cout<<table<<" 번 테이블에 "<<key<<" 삽입"<< std::endl;
				data1[hash]=key;
			}
			else
			{
				int old = data1[hash];
				data1[hash]=key;
				std::cout<<table<<"번 테이블에 "<<key<<" 삽입: 기존의 "<<old<<" 이동-> ";
				insert_impl(old,cnt+1,2);
			}
		}
		else
		{
			int hash = hash2(key);
			if(data2[hash] == -1)
			{
				std::cout<<table<<" 번 테이블에 "<<key<<" 삽입"<< std::endl;
				data2[hash]=key;
			}
			else
			{
				int old = data2[hash];
				data2[hash]=key;
				std::cout<<table<<"번 테이블에 "<<key<<" 삽입: 기존의 "<<old<<" 이동-> ";
				insert_impl(old,cnt+1,1);
			}
		}
	]
	void print()
	{
		std::cout<<"INDEX : ";
		for(int i =0;i<size;i++)
			std::cout<<i<<'\\t';
		std::cout<<std::endl;

		std::cout<<"DATA1 : ";
		for(auto i : data1)
			std::cout<<i<<'\\t';
		std::cout<<std::endl;

		std::cout<<"DATA2 : ";
		for(auto i:data2)
			std::cout<<i<<'\\t';
		std::cout<<std::endl;
	}
};

int main()
{
	hash_map map(7);
	map.print();
	std::cout<<std::endl;

	map.insert(10);
	map.insert(20);
	map.insert(30);
	std::cout<<std::endl;

	map.insert(104);
	map.insert(2);
	map.insert(70);
	map.insert(9);
	map.insert(90);
	map.insert(7);
	std::cout<<std::endl;

	map.print();
	std::cout<<std::endl;
	
	map.insert(14); // 순환 발생!
}

3. C++ 해시 테이블

1) 연습 문제 16: STL에서 제공하는 해시 테이블

#include <iostream>
#include <unordered_map>
#include <unordered_set>

void print(const std::unordered_set<int>& container)
{
	for (const auto& element : container)
		std::cout << element << " ";
	std::cout << std::endl;
}

void print(const std::unordered_map<int, int>& container)
{
	for (const auto& element : container)
		std::cout << element.first << " -> " << element.second << ", ";
	std::cout << std::endl;
}

void find(const std::unordered_set<int>& container, const int element)
{
	if (container.find(element) == container.end())
		std::cout << element << " 검색: 실패" << std::endl;
	else
		std::cout << element << " 검색: 성공" << std::endl;
}

void find(const std::unordered_map<int, int>& container, const int element)
{
	auto it = container.find(element);
	if (it == container.end())
		std::cout << element << " 검색: 실패" << std::endl;
	else
		std::cout << element << " 검색: 성공, 값 = " << it->second << std::endl;
}

int main()
{
	std::cout << "*** std::unordered_set 예제 ***" << std::endl;
	std::unordered_set<int> set1 = {1, 2, 3, 4, 5};

	std::cout << "set1 초깃값: "; print(set1);

	set1.insert(2);
	std::cout << "2 삽입: "; print(set1);

	set1.insert(10);
	set1.insert(300);
	std::cout << "10, 300 삽입: "; print(set1);

	find(set1, 4);
	find(set1, 100);

	set1.erase(2);
	std::cout << "2 삭제:"; print(set1);

	find(set1, 2);

	std::cout << "*** std::unordered_map 예제 ***" << std::endl;
	std::unordered_map<int, int> squareMap;

	squareMap.insert({2, 4});
	squareMap[3] = 9;
	std::cout << "2, 3의 제곱 삽입: "; print(squareMap);

	squareMap[20] = 400;
	squareMap[30] = 900;
	std::cout << "20, 30의 제곱 삽입: "; print(squareMap);

	find(squareMap, 10);
	find(squareMap, 20);
	std::cout << "squareMap[3] = " << squareMap[3] << std::endl;
	std::cout << "squareMap[100] = " << squareMap[100] << std::endl;
	print(squareMap);
}

2) 실습 문제 6: 긴 url을 짧은 url로 매핑하기

#include 
#include 
#include 

struct URLService
{
	using ActualURL = std::string;
	using TinyURL = std::string;

private:
	std::unordered_map<tinyurl, actualurl=""> data;

public:
	std::pair<bool, actualurl=""> lookup(const TinyURL& url) const
	{
		auto it = data.find(url);
		if (it == data.end()) // 단축 URL이 등록되어 있지 않은 경우
		{
			return std::make_pair(false, std::string());
		}
		else
		{
			return std::make_pair(true, it->second);
		}
	}

	bool registerURL(const ActualURL& actualURL, const TinyURL& tinyURL)
	{
		auto found = lookup(tinyURL).first;
		if (found)
		{
			return false;
		}

		data[tinyURL] = actualURL;
		return true;
	}

	bool deregisterURL(const TinyURL& tinyURL)
	{
		auto found = lookup(tinyURL).first;
		if (found)
		{
			data.erase(tinyURL);
			return true;
		}

		return false;
	}

	void printURLs() const
	{
		for (const auto& entry : data)
		{
			std::cout << entry.first << " -> " << entry.second << std::endl;
		}
		std::cout << std::endl;
	}
};

int main()
{
	URLService service;

	if (service.registerURL("<https://www.gilbut.co.kr/book/view?bookcode=BN002245>", "https://py_dojang"))
	{
		std::cout << "https://py_dojang 등록" << std::endl;
	}
	else
	{
		std::cout << "https://py_dojang 등록 실패" << std::endl;
	}

	if (service.registerURL("<https://www.gilbut.co.kr/book/view?bookcode=BN001484>", "https://c_dojang"))
	{
		std::cout << "https://c_dojang 등록" << std::endl;
	}
	else
	{
		std::cout << "https://c_dojang 등록 실패" << std::endl;
	}

	if (service.registerURL("<https://www.gilbut.co.kr/book/view?bookcode=BN002402>", ""))
	{
		std::cout << " 등록" << std::endl;
	}
	else
	{
		std::cout << " 등록 실패" << std::endl;
	}

	auto pythonBook = service.lookup("https://py_dojang");
	if (pythonBook.first)
	{
		std::cout << "https://py_dojang 원본 URL: " << pythonBook.second << std::endl;
	}
	else
	{
		std::cout << "https://py_dojang 원본 URL을 찾을 수 없습니다." << std::endl;
	}

	auto cppBook = service.lookup("https://cpp_dojang");
	if (cppBook.first)
	{
		std::cout << "https://cpp_dojang 원본 URL: " << cppBook.second << std::endl;
	}
	else
	{
		std::cout << "https://cpp_dojang 원본 URL을 찾을 수 없습니다." << std::endl;
	}

	if (service.deregisterURL("https://c_dojang"))
	{
		std::cout << "c_dojang 책 URL 등록 해제" << std::endl;
	}
	else
	{
		std::cout << "c_dojang 책 URL 등록 해제 실패" << std::endl;
	}

	auto findQtBook = service.lookup("https://c_dojang");
	if (findQtBook.first)
	{
		std::cout << "https://c_dojang 원본 URL: " << findQtBook.second << std::endl;
	}
	else
	{
		std::cout << "https://c_dojang 원본 URL을 찾을 수 없습니다." << std::endl;
	}

	std::cout << "등록된 URL 리스트:" << std::endl;
	service.printURLs();
}
</bool,></tinyurl,>

4. 블룸 필터

  • 블룸 필터는 해시 테이블보다 공간 효율이 매우 높음 하지만 결정적인 솔루션 대신 부정확한 결과를 얻을 수 있음
  • 여러 개의 해시 함수를 이용하여 정확도를 높임
  • 특정 비트가 다수의 원소에 의해 1로 설정될 수 있어 결정적이지 않음 (거짓-긍정 발생 가능)
  • 따라 거짓의 경우에는 해당 원소가 확실히 없음 (거짓-부정 발생 불가능)

1) 연습 문제 17: 블룸 필터 만들기

#include<iostream>
#include<vector>

class bloom_filter
{
	std::vector<bool> data;
	int nBits;
	
	int hash(int num, int key)
	{
		switch(num)
		{
			case 0: return key%nBits;
			case 1: return (key/7)%nBits;
			case 2: return (key/11)%nBits;
		}
		return 0;
	}
public:
	bloom_filter(int n) : nBits(n)
	{
		data = std::vector<bool>(nBits, false);
	}
	void lookup(int key)
	{
		bool result = data[hash(0,,key)]&data[hash(1,key)]&data[hash(2,key)];
		if(result)
		{
			std::cout<<key<<": 있을 수 있음" <<std::endl;
		}
		else
		std::cout<<key<<": 절대 없음"<<std::endl;
	}
	void insert(int key)
	{
		data[hash(0,key)] = true;
		data[hash(1,key)] = true;
		data[hash(2,key)] = true;
		std::cout<<key<<"를 삽입: ";

		for(auto a:data)
			std::cout<<a<<" ";
		std::cout<<std::endl;
	}
};

int main()
{
	bloom_filter bf(7);
	bf.insert(100);
	bf.insert(54);
	bf.insert(82);

	bf.lookup(5);
	bf.lookup(50);
	bf.lookup(20);
	bf.lookup(54);
}

2) 실습 문제 7: 이메일 주소 중복 검사

#include <iostream>
#include <vector>

#include <openssl/md5.h>

class BloomFilter
{
	int nHashes;
	std::vector<bool> bits;

	static constexpr int hashSize = 128 / 8; // 128비트(16바이트)

	unsigned char hashValue[hashSize];

public:
	BloomFilter(int size, int hashes) : bits(size), nHashes(hashes)
	{
		if (nHashes > hashSize)
		{
			throw("해시 함수 개수가 너무 많습니다.");
		}

		if (size > 255)
		{
			throw("블룸 필터 크기가 255보다 클 수 없습니다.");
		}
	}

	void hash(const std::string& key)
	{
		MD5(reinterpret_cast<const unsigned char*>(key.data()), key.length(), hashValue);
	}

	void add(const std::string& key)
	{
		hash(key);
		for (auto it = &hashValue[0]; it < &hashValue[nHashes]; it++)
		{
			bits[*it % bits.size()] = true;
		}

		std::cout << "블룸 필터에 추가: " << key << std::endl;
	}

	bool mayContain(const std::string& key)
	{
		hash(key);
		for (auto it = &hashValue[0]; it < &hashValue[nHashes]; it++)
		{
			if (!bits[*it % bits.size()])
			{
				std::cout << key << " : 사용할 수 있는 이메일입니다." << std::endl;
				return false;
			}
		}

		std::cout << key << " : 이미 사용 중입니다." << std::endl;
		return true;
	}
};

int main()
{
	BloomFilter bloom(128, 5);

	bloom.add("abc@gilbut.com");
	bloom.add("xyz@gilbut.com");

	bloom.mayContain("abc");
	bloom.mayContain("xyz@gilbut.com");
	bloom.mayContain("xyz");

	bloom.add("abcd@gilbut.com");
	bloom.add("ab@gilbut.com");

	bloom.mayContain("abcd");
	bloom.mayContain("ab@gilbut.com");
}

 

 

출처 : 코딩 테스트를 위한 자료 구조와 알고리즘 with C++

'Computer Science > Data Structure & Algorithm' 카테고리의 다른 글

그리디 알고리즘  (0) 2023.07.02
분할 정복  (0) 2023.07.01
트리/힙/그래프(2) - 힙&그래프  (0) 2023.06.28
트리/힙/그래프(1) - 트리  (0) 2023.06.27
리스트 / 스택 /큐  (0) 2023.06.26