[C++] "가장 강력한 검색" 해시 테이블의 기본 구현

해시 테이블 조회의 시간복잡도는 O(1)~

기사 디렉토리


머리말

해시 개념:

순차 구조와 균형 트리 에서는 요소 키와 저장 위치 사이에 해당하는 관계가 없으므로 요소를 찾을 때
, 키 코드의 여러 비교를 거쳐야 합니다 . 순차탐색의 시간복잡도는 O(N) 이며, 균형트리에서 트리의 높이는
O(logN) , 검색의 효율성은 검색하는 동안 요소의 비교 횟수에 따라 달라집니다.
이상적인 검색 방법: 검색할 요소는 비교 없이 테이블에서 한 번에 직접 얻을 수 있습니다 .
저장 구조가 구축되면 특정 함수 (hashFunc) 를 사용하여 요소의 저장 위치와 해당 키 코드 간의 관계를 설정할 수 있습니다.
일대일 매핑 관계로 검색 시 이 기능을 통해 요소를 빠르게 찾을 수 있습니다.
이 구조에 추가할 때:
요소 삽입: 삽입할 요소의 키 코드에 따라 이 기능을 사용하여 요소의 저장 위치를 ​​계산하고 이 위치에 따라 저장합니다.
요소 검색: 요소의 키 코드에 대해 동일한 계산을 수행하고 얻은 함수 값을 요소의 저장 위치로 사용하고 구조에서 이 위치에 따라 요소를 비교합니다.키 코드가 같으면 검색 성공
이 방식이 해시 ( hash ) 방식이고, 해시 방식에서 사용되는 변환 함수를 해시 ( hash ) 함수라고 하고, 구축된 구조를 호출한다.
해시 테이블 ( 또는 해시 테이블)
예: 데이터 세트 {1 , 7 , 6 , 4 , 5 , 9} ;
해시 함수는 다음과 같이 설정됩니다: 해시(키) = 키 % 용량 , 용량은 스토리지 요소의 기본 공간의 총 크기입니다.

 이 방법으로 검색하면 여러 키 코드를 비교할 필요가 없으므로 검색 속도가 비교적 빠릅니다


1. 해시 충돌 및 해시 함수

해시 충돌:

위에서 삽입한 숫자에 44를 삽입하면 어떻게 될까요? 44%10==4인데 4의 위치는 이미 점유되어 있는 해시 충돌이다.

서로 다른 키워드가 동일한 해시 번호를 통해 동일한 해시 주소를 계산하는 현상을 해시 충돌 또는 해시 충돌 이라고 합니다 .

키는 다르지만 해시 주소는 같은 데이터 요소를 " 동의어 " 라고 합니다 .

해시 함수:

해시 충돌의 한 가지 이유는 해시 함수의 설계가 충분히 합리적이지 않기 때문일 수 있습니다 .
해시 함수 설계 원칙 :
1. 해시 함수의 도메인은 저장해야 하는 모든 키 코드를 포함해야 하며 해시 테이블이 m 주소를 허용하는 경우 해당 값 도메인은 0 에서 m-1 사이여야 합니다 .
2. 해시 함수에 의해 계산된 주소는 전체 공간에 고르게 분포될 수 있습니다.
3. 해시 함수는 비교적 단순해야 합니다.
일반적인 해시 함수:
1. 직접 주소 지정 방법 -- ( 일반적으로 사용됨 )
키의 선형 함수를 해시 주소로 사용: Hash ( Key ) = A*Key + B
장점: 간단하고 균일합니다.
단점 : 사전에 키워드 분포를 알아야 함.
사용 시나리오: 비교적 작고 연속적인 상황을 찾는 데 적합합니다.
2. 나머지 방법 --( 일반적으로 사용됨 )
해시 테이블에 허용되는 주소의 수를 m 이라 하고 m 보다 크지 않고 m 에 가장 가깝거나 같은 소수 p를 제수로 취합니다 . 해시 함수에 따르면 Hash(key) = key% p (p<=m), 키는 해시 주소로 변환됩니다.
3. 정중앙을 찍는 방법 --( 이해하다 )
키워드가 1234 이고 정사각형이 1522756 이고 중간 3 자리 227이 해시 주소로 추출된다고 가정하면;
또 다른 예는 키워드가 4321 이고 사각형이 18671041 이며 가운데 3 자리 671( 또는 710)이 해시 주소로 추출된 경우입니다.
제곱 방법이 더 적합합니다. 키워드의 분포를 알 수 없으며 자릿수가 그다지 많지 않습니다.
4. 접는 방법 --( 이해 )
접는 방법은 키워드를 왼쪽에서 오른쪽으로 같은 숫자로 여러 부분으로 나눈 다음 ( 마지막 부분이 더 짧을 수 있음 ) 이 부분을 중첩 및 합산하고 해시 길이에 따라 마지막 몇 자리를 해시로 만듭니다. 테이블.열 주소.
접기 방식은 사전에 알 필요가 없는 키워드의 분포에 적합하며, 키워드가 많은 상황에 적합합니다.
5. 난수 방법 --( 이해 )
임의 함수를 선택하고 키워드의 임의 함수 값을 해시 주소로 취합니다. 즉, H(키) = 임의(키), 여기서
random 은 난수 함수입니다.
이 방법은 일반적으로 키워드의 길이가 같지 않을 때 사용됩니다.
6. 수학적 분석 방법 --( 이해 )
n d 자리 가 있고 , 각 비트는 r 개의 다른 기호를 가질 수 있으며 , 각 비트에 나타나는 r 개의 다른 기호 의 빈도는 반드시 일치 하지는 않습니다.
마찬가지로 일부 비트에 고르게 분포될 수 있으며 각 기호는 동일한 확률로 나타날 수 있으며 고르지 않은 분포는 일부 비트에만 해당됩니다.
특정 기호가 자주 나타납니다. 해시 테이블의 크기에 따라 다양한 심볼이 고르게 분포된 비트 수를 해시 테이블로 선택할 수 있다.
열 주소. 예를 들어:
예를 들어 특정 회사의 사원증을 저장하고 싶다고 가정하고, 휴대폰 번호를 키워드로 사용하면 앞 7 자리가 같을 가능성이 매우 높습니다.
예, 그러면 마지막 4비트를 해시 주소로 선택할 수 있습니다. 이러한 추출 작업이 충돌하기 쉬운 경우에도 여전히
추출된 숫자는 반전 ( 예: 1234가 4321 로 변경 ) , 오른쪽 링 이동 ( 예: 1234가 4123 으로 변경 ) , 왼쪽 링 이동이 가능합니다.
숫자, 처음 두 숫자와 마지막 두 숫자의 중첩 ( 예: 1234 를 12+34=46으로 변경 ) 및 기타 방법.

숫자 분석 방법은 일반적으로 키워드의 분포를 미리 알고 있고 키워드의 비트 수가 고르게 분포된 경우 키워드 수가 상대적으로 많은 상황을 처리하는 데 적합합니다.

참고: 해시 함수가 정교할수록 해시 충돌이 덜 발생하지만 해시 충돌을 피할 수는 없습니다.

해시 충돌 해결:

해시 충돌을 해결하는 두 가지 일반적인 방법 은 폐쇄형 해싱 개방형 해싱입니다.
1. 폐쇄형 해싱
폐쇄형 해싱: 개방형 주소 지정 방법이라고도 하며 해시 충돌이 발생할 때 해시 테이블이 가득 차 있지 않으면 해시 테이블에 해시 테이블이 있어야 함을 의미합니다.
빈 위치, 키는 충돌 위치의 " 다음 " 빈 위치 에 저장할 수 있습니다. 다음 빈 슬롯을 찾는 방법
모직물?
1. 선형 감지 : 충돌이 발생한 위치에서 시작하여 다음 빈 위치가 발견될 때까지 하나씩 뒤로 탐색합니다 .
삭제
닫힌 해싱을 사용하여 해시 충돌을 처리하는 경우 해시 테이블의 기존 요소를 물리적으로 삭제할 수 없습니다. 요소를 직접 삭제하는 경우
다른 요소 검색에 영향을 미칠 수 있습니다 . 예를 들어 요소 4 를 삭제하고 직접 삭제하면 검색 시 44에 영향을 줄 수 있습니다.
반지. 따라서 선형 조사는 표시된 의사 삭제를 사용하여 요소를 삭제합니다 .

선형 프로빙의 장점: 구현이 매우 간단합니다.

선형 감지의 단점: 해시 충돌이 발생하면 모든 충돌이 함께 연결되어 데이터가 " 쌓이는 " 경향이 있습니다 . 즉, 다른 키가 사용 가능한 빈 위치를 차지하므로 위치를 찾기 위해 많은 비교가 필요합니다. 특정 키의 결과로 검색 효율성이 감소합니다 . 그것을 완화하는 방법? 보조 탐지 방법 사용:

선형검출의 단점은 상반되는 데이터가 함께 쌓이는데 이는 다음 빈자리를 찾는 것과 관련이 있다.
설정 방법은 옆에 하나씩 찾는 것이므로 이러한 문제를 피하기 위해 두 번째 감지 방법은 다음 빈 위치를 찾는 것입니다.
为:hashi=hashi+i*i 혹은:hashi=hashi+i的n 次方 其中:i=  
1,2,3…
2.1 의 경우 44 를 삽입하려는 경우 충돌이 발생하며 해결된 상황은 다음과 같습니다.
연구에 따르면 테이블의 길이가 소수이고 테이블 로딩 계수 a가 0.5를 초과하지 않는 경우 새 항목을 삽입해야 하며 모든
단일 위치가 두 번 검색되지 않습니다. 따라서 테이블의 빈 위치의 절반이 있는 한 테이블이 가득 차는 문제는 없습니다. 존재하다
검색시 테이블의 꽉찬 정도는 무시해도 되지만 삽입 시에는 테이블의 적재율 a 가 0.5를 넘지 않도록 해야 합니다 .
용량을 고려해야 합니다.
따라서 해싱의 가장 큰 단점은 공간 활용률이 상대적으로 낮다는 점이며, 이는 해싱의 결점이기도 합니다.
2. 오픈 해시
1. 개방형 해시 개념
오픈해시방식은 체인주소방식 ( 오픈체인방식 ) 이라고도 하는데 , 먼저 해시함수를 이용하여 키코드셋에 대한 해시주소를 계산하는데, 이는 동일한
주소의 키는 동일한 하위 컬렉션에 속하고 각 하위 컬렉션은 버킷이라고 하며 각 버킷의 요소는 단일 연결 목록으로 연결됩니다.
다음으로 각 연결 리스트의 헤드 노드는 해시 테이블에 저장됩니다 .

위의 그림에서 볼 수 있듯이 열린 해시의 각 버킷에는 해시 충돌이 있는 요소가 포함되어 있습니다 .

개방 및 폐쇄 해시 비교:

오버플로를 처리하기 위해 체인 주소 방법을 적용하려면 링크 포인터를 추가해야 하는데, 이는 스토리지 오버헤드를 증가시키는 것으로 보입니다 . 사실은:
오픈 주소 방식은 검색 효율을 보장하기 위해 많은 여유 공간을 유지해야 하므로, 2차 검출 방식은 로드 팩터 a <= 가 필요합니다.
0.7 이고, 엔트리가 차지하는 공간이 포인터보다 훨씬 크기 때문에 체인 주소 방식을 사용하면 오픈 주소 방식에 비해 저장 공간을 절약할 수 있다.

둘째, 해시 테이블의 기본 구현

1. 오픈주소 방식

 먼저 네임스페이스에 코드를 넣어 나중에 네이밍 충돌을 방지한 다음 구조를 사용하여 각 위치에 어떤 종류의 데이터가 저장되는지 저장합니다.여기서는 kv 구조를 예로 들어 보겠습니다.

enum State
	{
		EMPTY,
		DELETE,
		EXIST
	};
	template <class K, class V>
	struct HashDate
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

우리가 정의한 열거 유형은 비어 있음, 삭제 및 세 가지 상태가 있습니다.해시 테이블의 데이터를 직접 삭제하는 대신 상태 표현을 사용하는 이유는 개방형 주소 방법이 충돌을 해결하기 때문에 모든 사람이 답을 가지고 있어야 합니다. 간혹 이 위치에 이미 숫자가 있으면 나중에 검색을 해야 하는데 이 위치를 삭제하면 다음 숫자를 어떻게 찾을 수 있을까요? HashDate를 초기화할 때, 나중에 상태에 따라 삽입하고 삭제할 것이기 때문에 처음에 각 위치를 EMPTY 상태로 설정해야 합니다.

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

    private:
		vector<HashDate<K, V>> _tables;
		size_t _n = 0;   //记录插入了多少个元素

	};

벡터의 기능은 매우 완벽하고 우리가 직접 구현하면 더 번거롭기 때문에 해시 테이블의 본체에 벡터를 직접 사용합니다. 각 벡터는 HashDate 유형의 데이터를 저장하고(템플릿 매개변수를 추가해야 함) 변수를 사용하여 테이블에 삽입되는 데이터의 양을 기록합니다. 여기서는 벡터의 size()를 직접 사용할 수 없습니다. 상태를 삭제합니다. size()로 상태를 삭제하면 함께 기록됩니다.

bool insert(const pair<K, V>& kv)
		{
			size_t hashi = kv.first % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state == EXIST)
			{
				index = hashi + i;
				index %= _tables.size();
				++i;
			}
			_tables[index]._kv = kv;
			_tables[index]._state = EXIST;
			++_n;
			return true;
		}

위는 해시 테이블을 삽입하기 위한 코드인데, 용량 확장 문제는 고려하지 말자. 여기서는 %capacity()에 매핑되는 요소의 위치를 ​​계산하면 안 된다.

 벡터를 사용하려면 [] 연산자를 사용해야 하는데 이 연산자는 size()의 값에만 접근할 수 있고 size()를 초과하면 array size() = 10과 같은 오류가 발생한다. , capacity() = 20 , 우리는 [5]에 접근할 수 있지만 [15]에 접근할 수 없으므로 매핑을 계산하는 위치는 %size()여야 합니다. 그런 다음 매핑 위치에 요소가 있는지 판단해야 합니다. 는 요소이므로 역방향으로 검출해야 합니다. Empty position, 인덱스를 사용하는 목적은 향후 2차 검출을 변경하는 것이 매우 간단할 것이라는 점입니다. 역방향으로 검색하는 과정에서 인덱스가 경계, 우리는 매번 해시 테이블의 실제 용량을 %로 할 것입니다.위치를 찾은 후 키-값 쌍을 삽입하고 존재하도록 상태를 변경한 다음 카운터를 증가시킵니다. 다음으로 용량 확장 문제를 고려합니다. 확장하기 전에 개념을 알아야 합니다.

bool insert(const pair<K, V>& kv)
		{
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				//扩容
				size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				HashTable<K, V> newtable;
				newtable._tables.resize(newsize);
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newtable.insert(e._kv);
					}
				}
				_tables.swap(newtable._tables);
			}
			size_t hashi = kv.first % _tables.size();
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state == EXIST)
			{
				index = hashi + i;
				index %= _tables.size();
				++i;
			}
			_tables[index]._kv = kv;
			_tables[index]._state = EXIST;
			++_n;
			return true;
		}

즉, 부하율이 무엇인지 확인해야 하는데, 부하율은 테이블의 실제 크기를 테이블에 실제로 삽입된 숫자로 나눈 값(실제 크기는 size()임을 기억하십시오)이지만 두 값이 컴퓨터의 정수는 어떻게 나누어도 변하지 않기 때문에 십진법으로 양변에 10을 곱하면 이 문제가 해결되며, 강제로 두 배로 제거할 수도 있습니다. 0으로 나누는 문제를 방지하기 위해 해쉬 테이블이 0인지 아니면 부하율이 0.7보다 큰지 판단하여 새로운 공간이 매번 원래 공간의 2배로 확장된다. 원래 어레이를 직접 확장할 수 있습니까? 원래 매핑된 위치는 확장 후에 변경되기 때문에 답은 아니오입니다.예를 들어 원래 size()는 10이고 숫자 11은 1의 위치에 배치됩니다. 숫자 11은 11의 위치가 정확합니다. 이 문제를 방지하기 위해 해시 테이블 객체를 직접 재생성하고 이 해시 테이블 객체의 테이블을 새로운 공간 크기로 확장합니다. 용량을 변경하려면 실제로 size()를 사용하므로 size()를 변경해야 합니다. 공간을 연 후 이전 테이블의 데이터를 탐색하여 각 위치에 요소가 있는지 확인하고 있는 경우 새 테이블에 삽입합니다(여기서 삽입을 호출하면 용량이 확장되지 않습니다. 삽입이 완료되면 원본 벡터와 새 테이블의 벡터를 직접 교환할 수 있습니다.

HashDate<K, V>* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			size_t hashi = key % _tables.size();
			size_t index = hashi;
			size_t i = 1;
			while (_tables[index]._state != EMPTY)
			{
				if (_tables[index]._state == EXIST
					&& _tables[index]._kv.first == key)
				{
					return &_tables[index];
				}
				index = hashi + i;
				index %= _tables.size();
				if (index == hashi)
				{
					break;
				}
				++i;
			}
			return nullptr;
		}

Find 인터페이스는 구현하기가 비교적 간단합니다. 테이블이 비어 있으면 빈 값을 반환합니다. 그런 다음 매핑된 위치를 계산하고 이 위치로 직접 이동하여 요소가 존재하는지 확인합니다. 이 위치가 삭제된 상태일 수 있으므로 검색할 때 이 위치가 비어 있지 않은 한 검색한다는 점에 유의해야 합니다. 삭제된 상태는 이 위치 뒤에서 검색해야 하므로 조건이 비어 있지 않습니다.루프에 진입한 후 현재 요소가 찾고 있는 요소의 키와 같은지 여부를 판단해야 하며 이 위치도 존재해야 합니다. 이 조건이 맞아야 이 위치에 데이터를 반환하게 된다(여기서는 참조를 사용하고 반환값은 포인터형인데 참조를 참조할 때 말했듯이 참조는 포인터에 의해 구현되기 때문에 존재한다) 여기서 반환 값에는 문제가 없습니다.) 주변을 검색하고 원래 매핑 위치로 돌아갈 때 시간이 확실히 발견되지 않으면 루프를 직접 종료하십시오.

bool eraser(const K& key)
		{
			HashDate<K, V>* tmp = Find(key);
			if (tmp)
			{
				tmp->_state = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}

삭제 인터페이스는 직접 찾기 기능을 사용하여 찾으면 현재 위치의 상태를 삭제로 설정한 다음 카운터를 감소시켜 true로 되돌립니다. 삽입할 때 Find를 이용하여 판단할 수도 있는데, 삽입할 값이 이미 존재하는 경우에는 삽입하지 않습니다.

 위는 공개 주소 방식의 세 가지 중요한 인터페이스입니다. 아래에서 테스트해 보겠습니다.

void TeshHashTable1()
	{
		int a[] = { 3,33,2,13,5,12,102 };
		HashTable<int, int> ht;
		for (auto& e : a)
		{
			ht.insert(make_pair(e, e));
		}
		ht.insert(make_pair(16, 16));
		auto t = ht.Find(13);
		if (t)
		{
			cout << "13在" << endl;
		}
		else
		{
			cout << "13不在" << endl;
		}
		ht.eraser(13);
		t = ht.Find(13);
		if (t)
		{
			cout << "13在" << endl;
		}
		else
		{
			cout << "13不在" << endl;
		}
	}

 문제 없습니다. 확장하는 동안 매핑이 성공했는지 확인해 보겠습니다.

 실행 결과는 괜찮습니다.다음으로 체인 주소 방법을 구현합니다.

2. 체인 주소 방식(해시 버킷)

 마찬가지로 네임스페이스에 코드를 넣은 다음 struct를 사용하여 노드를 구현해야 합니다. 이 노드는 향후 해시 테이블의 특정 위치에 매달릴 것입니다.

template <class K, class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		pair<K, V> _kv;
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			, _next(nullptr)
		{

		}
	};

노드는 다른 노드를 가리키는 다음 포인터만 있으면 키-값 쌍이 이루어집니다. 이 노드는 쌍을 통해.수 있습니다.

template <class K, class V>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:

    private:
		vector<Node*> _tables;
		size_t _n = 0;
	};

본문은 또한 노드에 대한 포인터를 저장하는 벡터를 사용하고 삽입된 요소 수를 기록하는 카운터도 필요합니다.

bool insert(const pair<K, V>& kv)
		{
			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

마찬가지로 확장 문제를 고려하지 않고 매핑 위치를 직접 계산한 다음 새 노드를 만든 다음 헤드를 삽입하고 새 노드의 다음 노드가 원래 테이블의 헤드 노드에 연결되도록 한 다음 새로운 노드를 매핑 위치로 하여 헤더 노드를 완성하고 헤더를 삽입한 후 카운터 ++로 설정합니다.확장 문제를 살펴보겠습니다.

bool insert(const pair<K, V>& kv)
		{
			if (_n == _tables.size())
			{
				//扩容
				size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				vector<Node*> newtable(newsize, nullptr);
				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = cur->_kv.first % newtable.size();
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newtable);
			}
			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

해시 버킷의 확장은 각 버킷의 데이터가 유사하도록 각 버킷에 요소가 있는 경우에만 확장하면 됩니다. 삽입된 요소를 실제 요소로 나누었을 때, 즉 부하율이 1일 때 해시 버킷의 용량을 확장할 수 있으며, 해시 버킷 확장을 위해 위와 같이 새로운 해시 테이블을 열 수도 있습니다. 주소 방식이지만 이것은 너무 비효율적입니다.버킷에 연결된 노드를 다시 삽입하고 삽입이 성공한 후 하나씩 공간을 해제하는 것은 너무 비효율적이므로 직접 벡터를 다시 열고 직접 다시 매핑하십시오. 이전 해시 테이블의 노드를 벡터에 하나씩 , 매핑이 완료된 후 이전 노드를 사용하여 다시 매핑하고 새 노드가 없기 때문에 매핑이 완료된 후 노드의 공간을 해제할 필요가 없도록 합니다. 리매핑도 매우 간단합니다 즉, 이전 해시 테이블을 순회하는 것입니다.이 위치의 노드가 비어 있지 않으면 이 노드의 다음 노드를 저장한 다음 이 노드의 새 매핑 위치를 계산합니다(여기서 계산은 매핑할 새 크기() 공간, 리매핑이라고 함), 현재 노드를 매핑 위치의 헤드 노드에 연결한 다음 현재 노드를 매핑 위치의 헤드 노드가 되도록 하여 헤드 삽입을 완료합니다. . 삽입이 완료되면 벡터를 교환할 수 있습니다.

Node* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			size_t hashi = key % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

조회 함수는 또한 먼저 테이블이 비어 있는지 확인하고 비어 있으면 널 포인터를 반환합니다. 그런 다음 매핑 위치를 계산하고 이 위치에서 헤드 노드를 직접 가져온 다음 헤드 노드에서 트래버스하여 찾고 있는 요소를 찾으면 현재 노드로 돌아갑니다. 루프가 끝나면 null 포인터를 반환합니다.

bool eraser(const K& key)
		{
			size_t hashi = key % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						Node* next = cur->_next;
						_tables[hashi] = next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

삭제 인터페이스는 먼저 매핑할 위치를 계산한 다음 이 위치의 헤드 노드를 가져오고, 변수를 사용하여 이전 노드를 저장하고, 헤드 노드가 비어 있지 않으면 루프에 들어가고, 삭제할 노드가 있으면 계속 트래버스합니다. not found, 순회하기 전에 prev 노드에 현재 위치를 주어 이전 위치를 기록하고 삭제할 노드를 찾으면 현재 노드가 헤드 노드인지 판단해야 합니다. 헤드 노드의 다음 헤드 노드는 직접 헤드 노드가 되며 원래 헤드 노드를 해제할 수 있습니다. 삭제할 노드가 헤드 노드가 아닌 경우 이전 노드의 다음 링크가 다음 노드를 삭제하도록 한 다음 노드를 해제하십시오.

아래 코드를 테스트해 보겠습니다.

void TeshHashTable2()
	{
		int a[] = { 3,33,2,13,5,12,1002 };
		HashTable<int, int> ht;
		for (auto& e : a)
		{
			ht.insert(make_pair(e, e));
		}
		ht.insert(make_pair(16, 16));
		ht.insert(make_pair(14, 14));
		ht.insert(make_pair(15, 15));
		ht.insert(make_pair(17, 17));
		auto t = ht.Find(13);
		if (t)
		{
			cout << "13在" << endl;
		}
		else
		{
			cout << "13不在" << endl;
		}
		ht.eraser(13);
		t = ht.Find(13);
		if (t)
		{
			cout << "13在" << endl;
		}
		else
		{
			cout << "13不在" << endl;
		}
	}

 인터페이스에는 문제가 없습니다. 확장 문제를 살펴보겠습니다.

 위는 확장이 없는 해시 테이블입니다. 확장 후 모습을 살펴보겠습니다.

 확장 후 모든 값이 다시 매핑된 것을 볼 수 있습니다.다음으로 소멸자를 구현해 보겠습니다. 프로그램이 종료되면 벡터는 자신의 공간만 해제하고 각 위치에 대한 연결 목록의 공간은 해제되지 않기 때문입니다. 해제해야 합니다. 수동으로 해제해야 하는 모든 것:

~HashTable()
		{
			for (auto& cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}

 파괴할 때는 직접 순회하며, 이 위치의 헤드 노드가 비어 있지 않으면 이 위치에 다음 노드를 저장한 다음 현재 노드를 해제하고 cur을 방금 저장한 노드로 두고 삭제 작업을 다시 실행합니다. 버킷의 모든 데이터가 해제되면 현재 버킷의 포인터를 null로 설정하기만 하면 됩니다.


요약하다

위는 해시 테이블의 기본 구현입니다.다음 기사에서는 해시 버킷을 캡슐화한 다음 unordered_map 및 unordered_set의 기본 레이어가 됩니다.우리는 전에 red-black 트리를 캡슐화했습니다.이번 캡슐화는 여전히 빨간색입니다. -검은 나무 차이는 별로 없지만 레드-블랙 트리보다 조금 더 번거로울 것입니다.

추천

출처blog.csdn.net/Sxy_wspsby/article/details/130790030