C++에 대한 자세한 설명---해시 폐쇄형 해싱

해싱을 이해하기 위한 질문

여기에 이미지 설명을 삽입하세요.
이 질문을 시도하려면 여기를 클릭하세요

우선 질문에 따르면 이 문자열에는 영어 소문자만 포함되어 있고 영어 소문자는 26개만 있으므로 크기 26의 문자 배열을 만들어 각 문자가 문자열에 나타나는 횟수를 기록할 수 있습니다. for 루프를 생성하면 전체 문자열을 순회하여 문자열의 각 문자 발생 횟수를 가져옵니다. 예를 들어 다음 코드는 다음과 같습니다.

 char firstUniqChar(string s) {
    
    
    int arr[26]={
    
    0};
    for(auto ch:s)
    {
    
    
        arr[ch-'a']++;
     }
}

각 문자의 발생 횟수를 얻은 후 문자열을 왼쪽에서 오른쪽으로 순서대로 순회하여 배열에서 해당 문자의 발생 횟수에 따라 한 번만 나타나는 첫 번째 문자를 가져와서 마지막으로 반환할 수 있습니다. 한 번만 나타나는 문자가 없으면 공백 문자가 반환되며, 여기서 코드는 다음과 같습니다.

char firstUniqChar(string s) {
    
    
    int arr[26]={
    
    0};
    for(auto ch:s)
    {
    
    
        arr[ch-'a']++;
    }
    for(auto ch:s)
    {
    
    
        if(arr[ch-'a']==1)
        {
    
    
            return ch;
        }
    }
    return ' ';
}

코드를 제출하면 여기의 코드가 올바르게 실행되는 것을 볼 수 있습니다.
여기에 이미지 설명을 삽입하세요.
그러면 위의 아이디어는 해싱과 유사합니다. 26개의 요소가 포함된 정수 배열을 만들고 배열의 각 요소는 문자열의 문자를 나타냅니다. 예를 들어 요소 아래 첨자가 0인 요소는 문자 a를 나타내고 아래 첨자가 1인 요소는 문자 b를 나타냅니다. 배열의 각 요소 크기는 문자열의 문자를 반영합니다. 요소의 발생 횟수입니다. 배열에서 첨자 1이 있는 요소의 크기가 3이면 문자열에서 요소 b가 나타나는 횟수가 3이라는 의미입니다. 배열에서 첨자 2가 있는 요소의 크기가 4인 경우 문자열에서 요소 c가 나타나는 횟수는 다음과 같습니다. 문자열 발생 횟수는 4 등입니다. 그런 다음 배열을 통한 이 일대일 대응을 해시 구조라고 부릅니다. 데이터 더미의 각 요소는 배열의 위치에 해당합니다. 이 위치에 있는 요소의 크기는 데이터의 특정 요소의 속성을 반영합니다. 예를 들어 위 배열의 요소 크기는 문자열에서 특정 요소의 발생 횟수를 반영합니다. 그러면 이것이 해시 구조이고 모두가 이해할 수 있기를 바랍니다.

해시 구현 원리

방법 1

해싱의 핵심은 키 값을 저장 위치와 연관시키는 해시 매핑입니다. 예를 들어 데이터 더미에서 각 데이터의 발생 횟수를 얻으려는 경우 이러한 데이터의 특징은 모두 범위는 0부터 1000까지이고, 크기가 1001인 배열을 만들 수 있습니다. 배열의 요소는 데이터의 요소 수를 나타냅니다. 이 방법을 직접 가치 방법이라고 합니다. Value를 직접 가져와 위치를 결정하거나 값을 사용하여 상대적으로 위치를 결정합니다. 예를 들어 데이터 범위가 1000~2000인 경우 크기가 1001인 배열을 생성하지만 아래 첨자가 있는 배열을 만듭니다. 배열의 0은 데이터 1000을 나타냅니다. 따라서 이 방법은 데이터가 매우 집중된 상황에만 적합합니다. 데이터가 매우 분산된 경우 이 방법의 효율성은 매우 낮습니다. 예를 들어 데이터 범위가 1~10000이면 10000 크기의 배열을 만들어야 하는데, 대부분의 데이터가 9900~10000 사이에 분포되어 있습니다. 예를 들어 한 데이터만 1이고 나머지 데이터는 9900보다 크다면, 그러면 9,899개의 공간이 낭비되고 효과가 없으므로 이 접근 방식은 적합합니다. 데이터가 매우 집중되어 있습니다.

방법 2

나누어서 남기는 방식: 공간이 얼마나 있는지는 상관하지 않고, 제공하는 데이터 개수에 따라 얼마나 공간을 열어야 하는지 결정하는 방식입니다. 예를 들어 현재 데이터가 7개라면 10개의 공간을 열어줍니다. 여기에서 데이터의 값과 공간의 값을 터치하면 그 결과가 저장 위치의 첨자가 됩니다. 예를 들어 현재 공간의 크기가 10이라면 처리된 데이터가 18이라면, 18을 10으로 터치하면 결과는 8이 됩니다. 따라서 첨자 8이 있는 위치에 18을 넣으면 아무리 큰 데이터라도 작은 배열에서 해당 위치를 찾을 수 있지만 이 처리 방법은 새로운 결과를 가져옵니다. 문제는 해시 충돌입니다. 예를 들어 한 값은 3이고 다른 값은 13이고 배열의 공간 크기가 10이면 이 두 숫자의 결과는 동일하므로 공간에 정확히 무엇이 저장되어 있는지 아래 첨자 3으로? 여기에 문제가 있습니다. 이 현상을 해시 충돌이라고 부릅니다. 여기서는 이를 해결하는 두 가지 방법이 있습니다. 먼저 첫 번째 방법을 설명하겠습니다. 폐쇄형 해시-개방 최상위 주소 방법입니다. 이 방법은 매핑된 위치에 이미 값을 찾은 다음 일정한 규칙에 따라 다른 위치를 찾습니다. 예를 들어 뒤쪽을 점유하는 것은 선형 감지입니다. 당신이 내 위치를 점유하면 나는 현재 위치 다음 위치를 점유합니다. 후자라면 위치도 점유하므로 내가 점유합니다. position 뒤에 예를 들어 다음 그림과 같습니다.
여기에 이미지 설명을 삽입하세요.
13은 3의 해당 위치에 저장됩니다. 23이 삽입되면 이 23은 아래 첨자 3이 있는 위치에 해당해야 하지만 3 이미 데이터가 있으므로 뒤쪽에 삽입합니다. 3 뒤의 위치, 즉 4에 놓습니다.
여기에 이미지 설명을 삽입하세요.
데이터 33을 다시 삽입하면 해당 위치는 여전히 3이지만 데이터는 3에 저장되고 3 4 다음 위치에도 데이터가 저장되지만 4 뒤의 5 데이터를 저장하지 않으므로 5보다 큰 데이터를 넣습니다. 예를 들어 아래 그림은 다음과 같습니다.
여기에 이미지 설명을 삽입하세요.
그러면 이것이 삽입의 법칙이고, 검색도 마찬가지입니다.해당 위치부터 검색을 시작하여 계속 검색합니다.찾았거나 검색된 위치가 비어 있으면 검색을 중지합니다.삭제는 어떻습니까? 여기서 삭제를 어떻게 삭제하나요? 위의 아이디어를 통해 실제로 삭제하고 싶은 데이터를 찾을 수 있는데, 삭제한다면 이 값을 무엇으로 설정해야 할까요? 비워두시겠습니까? 비어있는 금액은 얼마입니까? 0인가요? 잘못된 것 같습니다. 우리가 찾고 있는 데이터가 우연히 0이 된다면 뭔가 문제가 생길까요? 빈 숫자를 음수로 간주할 수 있나요? 안되는 것 같죠?빈 숫자가 음수라면 해시 테이블은 절대 음수를 저장할 수 없게 되는데 그럼 빈 숫자는 무엇일까요? 이 문제를 먼저 해결하지 말자. 합리적인 값이 비어 있다고 가정하고 삭제는 지정된 데이터를 비어 있도록 설정하는 것을 의미하므로 이 방법에는 실제로 문제가 없습니까? 예를 들어, 아래 그림:
여기에 이미지 설명을 삽입하세요.
23을 삭제하려면 아래 첨자 4를 사용하여 데이터를 비워야 합니다. 그러면 여기의 코드는 다음과 같습니다.
여기에 이미지 설명을 삽입하세요.
이때 데이터 33을 찾으려면 오류가 발생합니까? 검색 규칙 요소를 찾았거나 현재 요소가 비어 있으면 검색을 중지하고 여기서 33번 요소를 검색하려면 아래 첨자 3부터 검색을 시작합니다. 3에 해당하는 값은 13이므로 분명히 같지 않습니다. 그러나 첨자 4에 해당하는 값이 비어 있으므로 규칙에 따라 검색을 종료해야 하므로 데이터 33은 존재하지만 해당 요소를 삭제하는 방법이 올바르지 않아 검색이 실패합니다. .. 요소를 비워 놓는다고 문제가 해결되지 않는다는 뜻이다. , 우선 어떤 값인지 모르겠고, 둘째 이 방법은 검색에 문제가 생길 수 있어서 다른 방법을 생각하는 친구들도 있다. 삭제된 요소 뒤의 값을 하나씩 앞으로 이동하여 삭제할 수 있나요? 예를 들어 아래 그림에서
여기에 이미지 설명을 삽입하세요.
23을 삭제하면 빈 공간이 나올 때까지 모든 후속 데이터를 한 칸 앞으로 이동해야 합니다. 예를 들어 아래 그림에서는 다음과 같습니다.
여기에 이미지 설명을 삽입하세요.
데이터를 이동하면 이전에 해당했던 데이터와 대응이 불가능하므로 직접 이동 방식으로 데이터를 삭제하는 것은 절대 불가능하므로 여기서의 해결 방법은 배열을 추가하여 상태를 표현하는 것입니다. 세 가지 유형으로 나뉘는데, 비어 있음(Empty)은 현재 위치에 데이터가 없음을 의미하고, 삭제(Erase)는 현재 위치 이전에 데이터가 있었지만 데이터가 삭제되었음을 의미하며, 존재(Ire)는 현재 위치에 요소가 있음을 의미합니다. 위치가 존재하면 삭제는 현재 위치의 상태를 존재하도록 변경하는 것을 의미하며, 검색 시에는 상태가 비어 있는 위치를 찾을 때까지 지정된 위치부터 검색하면 됩니다. 데이터가 존재하지 않고 반환값도 존재하지 않는 경우 검색, 삽입 및 삭제의 논리입니다.다음으로 위의 기능 코드를 시뮬레이션으로 구현합니다.

준비

우선, 각 노드에는 상태가 있습니다. 세 가지 상태가 있으므로 여기서 세 가지 상태를 설명하기 위해 열거형을 만들 수 있습니다. 해시의 맨 아래 레이어는 벡터입니다. 둘째, 각 요소에는 데이터를 저장하는 쌍과 변수라는 이름의 상태는 현재 노드의 상태를 기록하는 데 사용되므로 여기에 노드를 설명하기 위한 구조체를 만들어야 하고, 노드는 다양한 형태의 데이터를 저장해야 하기 때문에 노드 클래스에 템플릿을 추가해야 하고, 템플릿에 두 개의 매개변수가 있고 여기에 있는 코드는 다음과 같습니다.

enum state
{
    
    
	EMPTY,
	EXIST,
	ERASE,
};

template<class K, class V>
struct HashNode
{
    
    
	pair<K, V> _kv;
	state _state = EMPTY;
};

해싱은 배열을 통해 시뮬레이션하고 구현하기 때문에 여기 해시 클래스의 최하위 레이어는 벡터를 통해 구현됩니다. 벡터의 데이터 유형은 HashNode입니다. 해싱은 다양한 데이터에 직면하므로 여기에도 템플릿을 추가해야 합니다. 템플릿에 두 개의 매개변수가 있는 경우 코드는 다음과 같습니다.

 template<class K,class V>
 struct HashTable
 {
    
    
 public:

 private:
	 vector<HashNode<K,V>> _tables;
 };

삽입 논리는 데이터를 양의 정수로 변환한 다음 이 양의 정수를 기반으로 벡터에 데이터를 삽입하는 것이지만 해시 테이블에서 처리되는 데이터는 사용자 정의 유형을 가질 수 있으므로 여기에 매개변수를 추가해야 합니다. 템플릿 펑터 수신 펑터의 기능은 저장된 데이터 유형에서 변환된 값을 가져오는 것입니다. 우리는 이 값을 사용하여 데이터가 저장되어야 하는 위치를 계산합니다. 예를 들어 문자열의 데이터 내용이 유형이 abcd이면 abcd는 functor에 의해 처리된 후 이 값이 각 문자의 합이라고 가정하여 값으로 변환되므로 abcd는 394로 변환되고 이 394를 사용하여 저장 위치를 ​​계산합니다. 예를 들어 이때 배열의 용량이 10이라면 위치는 4가 저장되지만, 보통 사람들이 해시 테이블을 사용하여 정수 데이터와 문자열 유형의 데이터를 처리할 때 펑터를 거치지 않고도 정상적으로 실행될 수 있다는 것입니다. 이거요? 대답은 템플릿 매개변수에 정수 데이터를 처리하는 펑터의 기본값이 전달되고, 문자열 유형을 처리하는 펑터는 정수 데이터를 처리하는 특수화라는 것입니다. 여기에 질문이 있습니다. 해시 테이블의 유효한 문자 수가 벡터의 크기와 같을 수 있습니까? 가능할 것 같지만 정말 가능할까요? 벡터에 많은 데이터가 포함되어 있으면 검색, 삭제 및 삽입 시 해시 충돌이 매우 분명해지며 이로 인해 해시 충돌이 점점 더 분명해지고 해시 테이블이 점점 더 효율적이게 됩니다. . 낮으므로 여기에 로드 팩터라는 것을 추가해야 합니다. 그 값은 테이블의 유효한 데이터 수/테이블 크기와 같습니다. 이 로드 팩터의 값은 약 0.7이 가장 좋습니다. 로드 팩터가 가 0.7이면 용량을 확장하겠습니다. 로드 팩터가 작을수록 할당량 충돌 가능성은 낮아지지만 소비되는 공간이 커지므로 유효한 값을 기록하려면 클래스에 변수를 추가해야 합니다. 현재 테이블에서 코드는 다음과 같습니다.

template<class K, class V>
struct HashNode
{
    
    
	pair<K, V> _kv;
	state _state = EMPTY;
};
template<class K>
struct HashFunc
{
    
    
	size_t operator()(const K& key)
	{
    
    
	//对于内置类型直接将其转换成为无符号整型进行处理
		return (size_t)key;
	}
};
template<>
struct HashFunc<string>
{
    
    
	size_t operator()(const string& key)
	{
    
    
		size_t res = 0;
		for (auto ch : key)
		{
    
    
			res *= 131;
			res += 131;
		}
		//这样的处理方式可以更好的降低重复率
		return res;
	}
};
 template<class K,class V,class Hash=HashFunc<K>>
 struct HashTable
 {
    
    
 public:

 private:
	 vector<HashNode<K,V>> _tables;
	 size_t _n;//记录有效值的个数
 };

그런 다음 마지막 단계는 생성자를 추가하는 것입니다. 여기서 생성자는 벡터 데이터를 확장하고 변수 _n을 0으로 초기화하는 것입니다. 그런 다음 여기의 코드는 다음과 같습니다.

HashTable()
	:_n(0)
{
    
    
	 _tables.resize(10);
}

이제 준비 작업입니다.다음으로 삽입 기능을 구현해야 합니다.

끼워 넣다

먼저 insert 함수에는 pair 형태의 매개변수가 필요하고, 함수 본문의 첫 번째 단계는 functor 객체를 생성하는 것이고, 두 번째 단계는 데이터가 삽입될 위치를 계산하는 것입니다.여기의 코드는 다음과 같습니다.

 bool Insert(const pair<K, V>& kv)
 {
    
    
	 Hash hf;
	 size_t hashi = hf(kv.first) %  _tables.size();
 }

그런 다음 원하는 위치를 찾은 후 해당 위치에서 끝까지 탐색할 수 있으며, 현재 위치의 상태가 존재하면 계속해서 뒤로 돌아갈 수 있고, 삭제되었거나 비어 있는 경우 요소를 삽입할 수 있습니다. , 범위를 벗어나는 것을 방지하기 위해 각 순회는 i 값을 크기별로 모듈로화하고, 요소가 발견되면 삽입하고, 현재 상태를 수정하고, 크기 변수에 1을 추가합니다. 그런 다음 여기의 코드는 다음과 같습니다.


bool Insert(const pair<K, V>& kv)
 {
    
    
	 Hash hf;
	 size_t hashi = hf(kv.first) % _tables.size();
	 while (_tables[hashi]._state == EXIST)
	 {
    
    
		 if (_tables[hashi]._kv.first == kv.first)
		 {
    
    
			 cout << "数据重复" << endl;
			 return false;
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 _tables[hashi]._kv = kv;
	 _tables[hashi]._state = EXIST;
	 ++_n;
	 return true;
 }

다음 단계는 확장을 달성하는 것입니다. 유효한 데이터 수가 테이블의 모든 요소의 70%를 차지하면 확장해야 하므로 여기서 새로운 해시 테이블을 생성합니다. 테이블 크기는 테이블 크기의 2배가 됩니다. 이전 테이블. 그런 다음 for 루프를 통해 원본 해시 테이블의 각 요소를 순회한 다음 각 요소를 새 해시 테이블에 삽입하고 마지막으로 두 해시 테이블의 내부 벡터를 교환합니다. 여기의 코드는 다음과 같습니다.

bool Insert(const pair<K, V>& kv)
 {
    
    
	 if (_n * 10 / _tables.size()>=7)
	 {
    
    
		 HashTable<K, V, HashFunc<K>> newHash;
		 newHash._tables.resize(_tables.size() * 2);
		 for (auto &ch : _tables)
		 {
    
    
			 if (ch._state == EXIST)
			 {
    
    
				 newHash.Insert(ch._kv);
			 }
		 }
		 _tables.swap(newHash._tables);
	 }
	 Hash hf;
	 size_t hashi = hf(kv.first) % _tables.size();
	 while (_tables[hashi]._state == EXIST)
	 {
    
    
		 if (_tables[hashi]._kv.first == kv.first)
		 {
    
    
			 cout << "数据重复" << endl;
			 return false;
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 _tables[hashi]._kv = kv;
	 _tables[hashi]._state = EXIST;
	 ++_n;
	 return true;
 }

여기서 용량을 확장할 때 단순히 내부 벡터를 용량의 2배로 확장할 수는 없는데, 확장 후에는 내부 데이터의 해당 관계가 모두 바뀌기 때문입니다.예를 들어 용량이 10이라면 해당 데이터인 18은 8. 확장 후 크기는 다음과 같습니다. 20에 도달하면 18은 위치 18에 해당하므로 여기서 단순히 용량을 확장할 수는 없습니다. 두 번째로 확장을 달성하는 또 다른 방법이 있습니다. 즉, 배열의 벡터 변수를 다시 생성하여 기존 배열은 각 값에 해당하는 새로운 첨자를 찾아 하나씩 삽입하는데, 이렇게 구현하면 반복 작업이 많이 발생하게 됩니다. 기존의 것을 빌려서 반복되는 작업을 많이 줄이는 것입니다.

기능 찾기

먼저 요소가 존재해야 할 위치를 찾은 후 빈 위치를 검색하여 비어 있으면 false를 반환하고 값이 같고 현재 위치에 요소가 존재하면 true를 반환하지만 그렇지 않습니다. 찾은 후 나타납니다. 값을 수정할 수 있습니다. 여기서 찾지 못하면 nullptr을 반환합니다. 찾으면 요소의 주소를 반환합니다. 그러면 이것이 의 구현 아이디어입니다. find 함수 이 함수의 코드는 다음과 같이 구현됩니다.

 HashNode<K,V>* Find(const K& key)
 {
    
    
	 HashFunc<K> hf;
	 size_t hashi = hf(key) % _tables.size();
	 while (_tables[hashi]._state != EMPTY)
	 {
    
    
		 if (_tables[hashi]._kv.first == key&&
			 _tables[hashi]._state==EXIST)
		 {
    
    
			 return &_tables[hashi];
		 }
		 ++hashi;
		 hashi%= _tables.size();
	 }
	 return nullptr;
 }

지우기 기능

먼저 find 함수를 통해 데이터가 존재하는 위치를 찾아냅니다. find 함수는 데이터의 주소를 반환해주기 때문에 데이터가 존재하면 지울 데이터의 상태를 직접 수정하고, 데이터가 존재하지 않으면 직접 데이터의 상태를 수정합니다. false를 반환하고 여기에 있는 코드는 다음과 같습니다.

bool erase(const K& key)
 {
    
    
	 HashNode<K, V>* Date = Find(key);
	 if (Date)
	 {
    
    
		 Date->_state = ERASE;
		 --_n;
		 return true;
	 }
	 else
	 {
    
    
		 return false;
	 }
 }

계측 코드

감지된 코드는 다음과 같습니다.

void TestHT1()
{
    
    
	HashTable<int, int> ht;
	int a[] = {
    
     18, 8, 7, 27, 57, 3, 38, 18 };
	for (auto e : a)
	{
    
    
		ht.Insert(make_pair(e, e));
	}
	ht.Insert(make_pair(17, 17));
	ht.Insert(make_pair(5, 5));
	if (ht.Find(7)){
    
    cout << "存在" << endl;}
	else{
    
    cout << "不存在" << endl;}
	ht.erase(7);
	if (ht.Find(7)) {
    
     cout << "存在" << endl; }
	else {
    
     cout << "不存在" << endl; }
}

코드의 실행 결과는 다음과 같습니다.
여기에 이미지 설명을 삽입하세요.
우리의 기대에 맞춰 다음 탐지 코드를 살펴보겠습니다.

void TestHT2()
{
    
    
	string arr[] = {
    
     "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//HashTable<string, int, HashFuncString> countHT; 
	HashTable<string, int> countHT;
	for (auto& e : arr)
	{
    
    
		HashNode<string, int>* ret = countHT.Find(e);
		if (ret)
		{
    
    
			ret->_kv.second++;
		}
		else
		{
    
    
			countHT.Insert(make_pair(e, 1));
		}
	}
}

이 코드의 실행 결과는 다음과 같습니다.
여기에 이미지 설명을 삽입하세요.
기대에 부응합니다. 즉, 코드가 올바르게 완성되었음을 의미합니다. 이것으로 이 글을 마칩니다.

추천

출처blog.csdn.net/qq_68695298/article/details/131445819