C++進階(雜湊)

一隻少年AAA發表於2022-12-24

vector容器補充(下面會用到)

我們都知道vector容器不同於陣列,能夠進行動態擴容,其底層原理:所謂動態擴容,並不是在原空間之後接續新空間,因為無法保證原空間之後尚有可配置的空間。而是以原大小的兩倍另外配置一塊較大空間,然後將原內容複製過來,並釋放原空間。因此,對 vector 的任何操作需要注意,當引起空間重新配置時,指向原 vector 的所有迭代器就都失效,因為此時迭代器仍然指向的是原空間的地址,原空間已經被釋放,迭代器就是個野指標,所以一旦引起空間重新配置,對迭代器一定要重新賦值

問題來了,容器滿後,是我每新增一個資料,就需要重新建立容器擴充一次嗎??

答案當然是否定的,我們看圖說話,我們可以觀察到,當容器滿了需要擴容的時候,並不是只增加一個空間,而是會多擴幾個空間,以防下次再次增加資料,具體擴多少由底層決定,我們不需要關心

C++進階(雜湊)

這樣做時有好處的,因為當資料量比較少的時候,將舊容器複製至新容器可能會很快,當時當資料量比較大的時候呢??這種擴充很浪費資源。這可能算是利用空間換時間的例子吧。

這樣雖然有好處,但是也有弊端。舉個例子,如下圖:

C++進階(雜湊)

當我們將一個容量為一百萬的容器,使用 resize(5) 函式重新指定大小為5的時候,它並不是向上圖一樣,就剩下5個空間,將其餘空間釋放掉,是錯誤的,而是所有的空間都存在,只是採用了一種特殊的手段,無法讓我們訪問後面的空間罷了,對比之下我們應該猜的出,resize()做的操作大概是直接修改了 資料量 大小,讓我不能訪問後面的資料,而不是釋放空間

swap()可以解決這個問題

用法:vector<int>(v).swap(v); //v是容器名

解釋:首先,vector(v)這條語句,透過複製建構函式建立了一個匿名物件,這個匿名物件擁有v的全部資料,但是,沒有空閒的空間,也就是說,這個匿名物件的容量資料量是相等的。如下圖:

C++進階(雜湊)

所以經過 vector(v) 建立出來的匿名物件是沒有空閒空間的,此時,透過 swap(v) 呼叫該匿名物件的swap()方法,交換v與匿名物件的內容,結果如下:

C++進階(雜湊)

我們都知道匿名物件在執行完程式碼之後會自動呼叫解構函式,那麼空間被釋放,最終結果就是,原容器中的空位被釋放,swap就是這麼強大

總結:

  • push_back擴容機制:當push_back一個元素時,

    • 如果發現size() == capacity(),那麼會以兩倍空間擴容,然後將元素插入到finish迭代器的下一個元素(注意會申請一個新的空間,並將老的元素複製到新空間中,然後釋放老的空間)
    • 如果發現size() < capacity(),那麼會插入到finish迭代器的下一個元素
    • 如果發現size() > capacity(),永遠不可能出現這樣的情況
  • pop_back、earse、clear縮容機制

    • pop_back會減少一個size(),但是不會改變capacity() 【finish迭代器前移一位】
    • earse會減少一個size(),但是不會改變capacity() 【finish迭代器前移一位】
    • clear令size()為0,但是不會改變capacity()(將finish迭代器移動到start相同位置)
  • 對於resize(new_size)

    • 如果new_size== curr.size,什麼也不做
    • 如果new_size< curr.size, 那麼 curr.size = new_size,curr.capacity不變
    • 如果new_size> curr.size, 那麼 curr.size = new_size,curr.capacity = new_size,將容器capacity 擴大到能容納new_size的大小,改變容器的curr.size,並且建立物件。
  • 對於reserve(new_size)

    • 如果new_size== curr.size,什麼也不做
    • 如果new_size< curr.size,什麼也不做
    • 如果new_size> curr.size,curr.size不變,curr.capacity=new_size,將容器capacity 擴大到能容納new_size的大小,在空間內不真正建立物件,所以不改變curr.size

所以所謂的縮容操作,並不是真正意義上的縮容,沒有做任何與記憶體釋放相關的工作,而僅僅是進行了邏輯資料的處理,僅僅是做了迭代器的前移。這一點事實也是可以理解的,clear操作是要把容器清空,只要在資料層面它能對外展示的資訊為空,然後對它的訪問都基於該空間資訊,比如按照索引讀取和寫入等操作,這些只要能基於正確的空間資料,那麼我們完全沒必要再去釋放記憶體,釋放記憶體這一步只需要等容器最終被銷燬的時候一起做就可以了,“資料還在那裡啊?”,在那裡你訪問不到跟不存在有什麼區別呢,它已經是編外的孤魂野鬼,不必搭理,最後佛祖會收拾的~~。
那麼我們如何做到真正的釋放記憶體呢?

  • 上面介紹的swap
#include<iostream>
#include<vector>
using namespace std;
int main()
{
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);

    cout << "size:" << v.size() << endl;
    cout << "capacity:" << v.capacity() << endl;

    vector<int>().swap(v);
    cout << "after swap size:" << v.size() << endl;
    cout << "after swap capacity:" << v.capacity() << endl;
    return 0;
}
//輸出:
size:5
capacity:6
after swap size:0
after swap capacity:0
  • 在C++11中新增了shink_to_fit()用於指導縮減記憶體空間,但不強制要求呼叫之後capacity()==size()。各個庫提供方可以用自己的策略判斷是否需要將資料遷移到較小空間
#include<iostream>
#include<vector>
using namespace std;
int main()
{
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    v.push_back(4);
    v.push_back(5);

    cout << "size:" << v.size() << endl;
    cout << "capacity:" << v.capacity() << endl;

    v.clear();
    v.shrink_to_fit();
    cout << "after swap size:" << v.size() << endl;
    cout << "after swap capacity:" << v.capacity() << endl;
    return 0;
}
//輸出:
size:5
capacity:6
after swap size:0
after swap capacity:0

雜湊概念

不經過任何比較,一次直接從表中得到要搜尋的元素。 如果構造一種儲存結構,透過某種函式(HashFunc)使元素的儲存位置與它的關鍵碼之間能夠建立一一對映的關係,那麼在查詢時透過該函式可以很快找到該元素,其中雜湊方法中用到的轉換函式稱為雜湊函式,構造出來的結構叫雜湊表(雜湊表)

下面是該結構中插入元素和搜尋元素的方法(時間複雜度都可以達到O(1)):

  • 插入元素: 根據待插入元素的關鍵碼,透過雜湊函式計算出該元素的儲存位置,並按此位置進行存放
  • 查詢元素: 對要查詢的元素的關鍵碼用樣的計算方法得出該元素的儲存位置,然後與該位置的元素進行比較,相同就表示查詢成功

雜湊函式

常見的有以下幾種:

  • 直接定製法: 取關鍵字的某個線性函式為雜湊地址:Hash(Key)= A*Key + B,其中A和B為常數
    優點: 簡單,均勻
    缺點: 需要事先知道關鍵字的分佈情況,如果關鍵字分佈很散(範圍很大),就需要浪費很多的空間
    使用範圍: 關鍵字分佈範圍小且最好連續的情況
  • 除留餘數法: 取關鍵字被某個不大於雜湊表表長m的數p除後所得的餘數為雜湊地址。即 H(key) = key % p,p<=m(p的選擇很重要,一般取素數或m)
    優點: 可以將範圍很大的關鍵字都模到一個範圍內
    缺點: 對p的選擇很重要
    使用範圍: 關鍵字分佈不均勻
  • 平方取中法(不常用): 取關鍵字平方後的中間幾位作為雜湊地址
  • 隨機數法(不常用): 選擇一隨機函式,取關鍵字作為隨機函式的種子生成隨機值作為雜湊地址,通常用於關鍵字長度不同的場合
  • 摺疊法(不常用): 將關鍵字分割成位數相同的幾部分,最後一部分位數可以不同,然後取這幾部分的疊加和(去除進位)作為雜湊地址

雜湊衝突

看下面一個例子:
有一組元素{0,1,3,15,9}用雜湊的方式存放,其中雜湊函式是Hash(key)=key%10 (存放後的結果如下)

C++進階(雜湊)

用這種方式儲存和查詢資料顯然很快,但是如果此時插入一個元素5,它應該放在那個位置?
Hash(5) = 5%10 = 5,但是3這個位置中已經有元素5,難道我們要選擇覆蓋元素9嗎?
顯然這樣是不妥的。(後面有解決的方法)
總結: 不同關鍵字透過相同的雜湊函式計算出相同的雜湊地址, 這裡的這種現象稱為雜湊衝突雜湊碰撞

負載因子以及增容

雜湊衝突出現的較為密集,往往代表著此時資料過多,而能夠對映的地址過少,而要想解決這個問題,就需要透過 負載因子(裝填因子) 的判斷來進行增容

負載因子的大小 = 表中資料個數 / 表的容量(長度)

對於閉雜湊
對於閉雜湊來說,因為其是一種線性的結構,所以一旦負載因子過高,就很容易出現雜湊衝突的堆積,所以當負載因子達到一定程度時就需要進行增容,並且增容後,為了保證對映關係,還需要將資料重新對映到新位置。

經過演算法科學家的計算, 負載因子應當嚴格的控制在 0.7-0.8 以下,所以一旦負載因子到達這個範圍,就需要進行增容。

因為除留餘數法等方法通常是按照表的容量來計算,所以科學家的計算,當對一個質數取模時,衝突的機率會大大的降低,並且因為增容的區間一般是 1.5-2 倍,所以演算法科學家列出了一個增容質數表,按照這樣的規律增容,衝突的機率會大大的降低。
這也是 STLunordered_map/unordered_set 使用的增容方法

//演算法科學家總結出的一個增容質數表,按照這樣增容的效率更高
	const int PRIMECOUNT = 28;
	
	const size_t primeList[PRIMECOUNT] = 
	{
	 53ul, 97ul, 193ul, 389ul, 769ul,
	 1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
	 49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
	 1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
	 50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
	 1610612741ul, 3221225473ul, 4294967291ul
	};

hashmap 的負載因子為什麼預設是 0.75 ?

比如說當前的容器容量是 16,負載因子是 0.75,16*0.75=12,也就是說,當容量達到了 12 的時候就會進行擴容操作。而負載因子定義為 0.75 的原因是:

  • 當負載因子是 1.0 的時候,也就意味著,只有當雜湊地址全部填充了,才會發生擴容。意味著隨著資料增長,最後勢必會出現大量的衝突,底層的紅黑樹變得異常複雜。雖然空間利用率上去了,但是查詢時間效率降低了
  • 負載因子是 0.5 的時候,這也就意味著,當陣列中的元素達到了一半就開始擴容。雖然時間效率提升了,但是空間利用率降低了。 誠然,填充的元素少了,Hash衝突也會減少,那麼底層的連結串列長度或者是紅黑樹的高度就會降低。查詢效率就會增加。但是,這時候空間利用率就會大大的降低,原本儲存 1M 的資料,現在就意味著需要 2M 的空間

對於開雜湊結構
因為雜湊桶是開雜湊的鏈式結構,發生了雜湊衝突是直接在對應位置位置進行頭插,而桶的個數是固定的,而插入的資料會不斷增多,隨著資料的增多,就可能會導致某一個桶過重,使得效率過低。

所以最理想的情況,就是每個桶都有一個資料。這種情況下,如果往任何一個地方插入,都會產生雜湊衝突,所以當資料個數與桶的個數相同時,也就是負載因子為 1 時就需要進行擴容。

雜湊衝突的解決

閉雜湊

概念

閉雜湊: 也叫開放定址法,當發生雜湊衝突時,如果雜湊表未被裝滿,說明在雜湊表中必然還有空位置,那麼可以把key存放到衝突位置中的“下一個” 空位置中去(下面介紹兩種尋找空位置的方式)。
兩種尋找空位置的方法:

1.線性探測: 從發生衝突的位置開始,依次向後探測,直到尋找到下一個空位置為止。在上面雜湊衝突的場景中,插入元素3時,因為此時的位置被佔了,所以元素3選擇下一個空位置,就是下標為4的位置

思考下面幾個問題:

  • 如何實現插入元素?

​ 先透過雜湊函式確定待插入元素的位置,如果該位置為空,直接插入,如果不為空就需要透過線性探測尋找下一個位置,如下面動圖所示:

C++進階(雜湊)
  • 如何實現刪除元素?

​ 先透過雜湊函式確定待刪除元素的起始位置,然後線性探測往後找到要刪除元素,此時不可以直接把這個元素刪除,否則會影響到其它元素的搜尋。所以這裡對每個位置狀態進行了標記,EMPTY(空)EXITS(存在)DELETE(刪除) 三種狀態,用DELETE標記刪除的位置(這是一種偽刪除的方式)

​ 為什麼不能直接刪除?我們來看圖結束

C++進階(雜湊)

顯然,這種刪除方式會影響後期元素的查詢,所以我們採用三種狀態記錄每個位置的狀態,只有為空才結束元素的查詢,具體操作如下

  • 如何查詢元素?

​ 先透過雜湊函式確定待查詢元素的起始位置,然後線性探測往後找,如果當前位置不為DELETE 就繼續往後找,直到當前位置為EMPTY,就停止查詢表示該元素不存在;當前位置為EXIT 就進行比較,一樣就查詢成功,否則去下一個位置;如果當前位置為DELETE,就繼續往下探測

C++進階(雜湊)

  • 何時增容?

​ 要注意的是,雜湊表不能滿了才增容,這樣會導致雜湊衝突的機率增大。雜湊表中有一個衡量雜湊表負載的量,叫負載因子負載因子(Load Factor) = 資料個數/雜湊表大小。

​ 一般我們選擇負載因子為0.7-0.8的時候開始增容,如果這個值選取太小,會導致空間浪費;如果這個值選取太大,會導致雜湊衝突的機率變大

2.二次探測:

線性探測的缺陷是產生衝突的資料堆積在一塊,這與其找下一個空位置有關係,因為找空位置的方式就是挨著往後逐個去找,因此二次探測為了避免該問題,找下一個空位置的方法為:H(i) = H(0) + i^2。其中:i = 1,2,3…, 是透過雜湊函式Hash(x)對元素的關鍵碼 key 進行計算得到的位置,m是表的大小
C++進階(雜湊)

增容問題:當表的長度為質數且表裝載因子a不超過0.5時,新的表項一定能夠插入,而且任何一個位置都不會被探查兩次。因此只要表中有一半的空位置,就不會存在表滿的問題。在搜尋時可以不考慮表裝滿的情況,但在插入時必須確保表的裝載因子a不超過0.5,如果超出必須考慮增容

總結:線性探測的優點是實現起來很簡單,缺點就是會有累加效應(我的位置如果被佔了,那麼我就佔別人的位置);二次探測的優點減輕了累加效應,因為雜湊衝突的時候搶佔的位置會在相對遠一點的地方,這樣元素排列就相對稀疏了。閉雜湊最大的缺陷就是空間利用率不高,這同樣也是雜湊的缺陷

雜湊表閉雜湊的實現(採用線性探測)

整體框架

概念:這裡採用線性探測的方式構建雜湊表,下面是整體框架,其中模板引數第一個是key關鍵字,第二個是雜湊表儲存的元素的資料型別,可以是K,也可以是pair<K,V>型別,主要就是為了同時實現K模型KV模型。第三個引數就是一個仿函式,為了獲取T中K的值,這裡要實現兩個仿函式,一個是對K模型,一個是對KV模型。這裡其實和上一篇部落格中透過改造紅黑樹同時實現map和set容器的方式是一樣的。雜湊表底層我們借用vector容器來實現。
雜湊表資料存什麼?
用一個類組織起來,裡面有每個位置的狀態和每個位置存放的元素

template<class K, class V>
struct KeyOfvalue
{
	const K& operator()(const K& key)
	{
		return key;
	}
	const K& operator()(const pair<K, V>& kv)
	{
		return kv.first;
	}
};
//狀態標誌位
enum State
{
	EMPTY,
	DELETE,
	EXITS
};
template<class T>
struct HashData
{
	T data;
	State state;
};
template<class K,class T,class KOFV>
class HashTable
{
	typedef HashData<T> HashData;
public:
private:
	vector<HashData> tables;
	size_t num = 0;//記錄已經存放了多少個資料
};

插入元素

有以下幾個步驟:

  • 一:先判斷負載因子是否大於0.7,如果大於0.7,就要考慮增容(下面詳細介紹);否則就直接插入
  • 二:用雜湊函式計算出要插入的元素的起始位置,然後找空位置(狀態為EMPTYDELETE),然後進行插入,並把狀態改為EXITS(這裡不用擔心沒有空位置,因為雜湊表不可能滿,他不是滿了才增容的)
  • 三:如果此過程中發現要插入的元素存在,則返回FALSE代表元素插入失敗;否則返回TRUE

增容問題: 我們需要把原來空間中的元素全部轉移到新的空間中,此過程相當於往新空間重新插入元素,且要對它們進行重新定位

一般有以下兩種方法:

  • 直接開一個新的vector(大小為增容後空間的大小),然後一個元素一個元素地進行轉移,最後把雜湊表中的vector和新的vector進行交換,讓這個新的vector帶走舊空間,並清理資源

  • 建立一個臨時的雜湊表,然後把vector成員的空間設定為增容後空間的大小,然後複用insert函式方法,對舊錶中元素進行轉移,最後新表和舊錶的vector進行交換。(這裡其實和上面的方法區別就在這裡對insert進行了複用,且都用到了利用臨時物件的解構函式清理舊空間的資源)

程式碼實現如下:

bool Insert(const T& data)
	{
		KOFV kofv;
		// 雜湊表不能滿了在增容,這樣會導致雜湊衝突的機率增大
		// 不能太小,太小會導致空間浪費;也不能太大,太大會導致雜湊衝突的機率很大
		// 負載因子(Load Factor)等於0.7就增容  num/tables.size()>=0.7
		// 負載因子 = 資料個數/雜湊表大小
		if (tables.size() == 0 || 10 * num / tables.size() >= 7) 
		{
			//建立一個新的vector容器
			vector<HashData> newtables;
			size_t newsz = tables.size() == 0 ? 10 : tables.size() * 2;
			//給新的vector設定的大小
			newtables.resize(newsz);
			// 先把舊錶的資料重新放到新表中
			// 因為表的大小發生變化,所以資料在舊錶中的位置和新表的位置不一樣,需要重新調整
			// 寫法1
			for (size_t i = 0; i < tables.size(); i++)
			{
				//for迴圈內就是把舊錶中的資料放到新表中,並重新分配位置
				if (tables[i].state == EXITS)
				{
					int index = kofv(tables[i].data) % newsz;
					while (newtables[index].state == EXITS)
					{
						// 不會存在重複資料,因為舊錶中不可能有重複的資料
						++index;
						if (index == newsz)
						{
							index = 0;
						}
					}
					newtables[index] = tables[i];
				}
			}
			tables.swap(newtables);// 把臨時空間和舊空間進行交換,交換後,舊空間的將由臨時物件的解構函式來釋放
			// 寫法2
			/*
				HashTable<K, T, KOFV> newht;
				size_t newsz = tables.size() == 0 ? 10 : tables.size() * 2;
				newht._tables.resize(newsz);
				for (size_t i = 0; i < tables.size(); ++i)
				{
					if (tables[i].state == EXITS)
					{
						newht.Insert(tables[i].data);
					}
				}
				tables.swap(newht.tables);
			*/
		}
		int index = kofv(data) % tables.size();
		/*二次探測
			int start = index;
			int i = 1;
		*/
		while (tables[index].state == EXITS)
		{
			if (tables[index].data == data)
			{
				return false;
			}
			//二次探測
			/*
				index = start + pow(i,2);
				index %= tables.size();
				++i;
			*/
			//線性探測
			++index;
			//走到末尾
			if (index == tables.size())
			{
				index = 0;
			}
		}
		//DELETE和EMPTY都可以直接插入
		tables[index].data = data;
		tables[index].state = EXITS;
		++num;
		return true;
	}

查詢元素

前面介紹過了,先透過雜湊函式確定待查詢元素的起始位置,然後線性探測往後找,如果當前位置不為DELETE 就繼續往後找,直到當前位置為EMPTY,就停止查詢表示該元素不存在;當前位置為EXIT 就進行比較,一樣就查詢成功,否則去下一個位置;如果當前位置為DELETE,就繼續往下探測

程式碼實現如下:

//查詢元素
	HashData* Find(const K& key)
	{
		KOFV kofv;
		int index = key % tables.size();
		int start = index;//標誌位,尋找一遍的標誌位
		while (tables[index].state != EMPTY)
		{
			if (kofv(tables[index].data) == key)
			{
				if (tables[index].state == EXITS)
				{
					return &tables[index];
				}
				//tables[index].state == DELETE
				//這表情要找的元素被刪除了
				else
				{
					return nullptr;
				}
			}
			++index;
			if (index == tables.size())
			{
				index = 0;
				//找完了一遍沒有就退出
				if (index == start)
				{
					return nullptr;
				}
			}
		}
		return nullptr;
	}

刪除元素

前面介紹過了,這裡不多說,比較簡單

程式碼實現如下:

//刪除元素
	bool Erase(const K& key)
	{
		HashData* ret = Find(key);
		if (ret != nullptr)
		{
			ret->state = DELETE;
			num--;
			return true;
		}
		else
		{
			return false;
		}
	}

完整程式碼

template<class K, class V>
struct KeyOfvalue
{
	const K& operator()(const K& key)
	{
		return key;
	}
	const K& operator()(const pair<K, V>& kv)
	{
		return kv.first;
	}
};
namespace CLOSE_HASH
{
	//狀態標誌位
	enum State
	{
		EMPTY,
		DELETE,
		EXITS
	};
	template<class T>
	struct HashData
	{
		T data;
		State state;
	};
	template<class K, class T, class KOFV>
	class HashTable
	{
		typedef HashData<T> HashData;
	public:
		bool Insert(const T& data)
		{
			KOFV kofv;
			// 雜湊表不能滿了在增容,這樣會導致雜湊衝突的機率增大
			// 不能太小,太小會導致空間浪費;也不能太大,太大會導致雜湊衝突的機率很大
			// 負載因子(Load Factor)等於0.7就增容  num/tables.size()>=0.7
			// 負載因子 = 資料個數/雜湊表大小
			if (tables.size() == 0 || 10 * num / tables.size() >= 7)
			{
				//建立一個新的vector容器
				vector<HashData> newtables;
				size_t newsz = tables.size() == 0 ? 10 : tables.size() * 2;
				//給新的vector設定的大小
				newtables.resize(newsz);
				// 先把舊錶的資料重新放到新表中
				// 因為表的大小發生變化,所以資料在舊錶中的位置和新表的位置不一樣,需要重新調整
				// 寫法1
				for (size_t i = 0; i < tables.size(); i++)
				{
					//for迴圈內就是把舊錶中的資料放到新表中,並重新分配位置
					if (tables[i].state == EXITS)
					{
						int index = kofv(tables[i].data) % newsz;
						while (newtables[index].state == EXITS)
						{
							// 不會存在重複資料,因為舊錶中不可能有重複的資料
							++index;
							if (index == newsz)
							{
								index = 0;
							}
						}
						newtables[index] = tables[i];
					}
				}
				tables.swap(newtables);// 把臨時空間和舊空間進行交換,交換後,舊空間的將由臨時物件的解構函式來釋放
				// 寫法2
				/*
					HashTable<K, T, KOFV> newht;
					size_t newsz = tables.size() == 0 ? 10 : tables.size() * 2;
					newht._tables.resize(newsz);
					for (size_t i = 0; i < tables.size(); ++i)
					{
						if (tables[i].state == EXITS)
						{
							newht.Insert(tables[i].data);
						}
					}
					tables.swap(newht.tables);
				*/
			}
			int index = kofv(data) % tables.size();
			/*二次探測
				int start = index;
				int i = 1;
			*/
			while (tables[index].state == EXITS)
			{
				if (tables[index].data == data)
				{
					return false;
				}
				//二次探測
				/*
					index = start + pow(i,2);
					index %= tables.size();
					++i;
				*/
				//線性探測
				++index;
				//走到末尾
				if (index == tables.size())
				{
					index = 0;
				}
			}
			//DELETE和EMPTY都可以直接插入
			tables[index].data = data;
			tables[index].state = EXITS;
			++num;
			return true;
		}
		//查詢元素
		HashData* Find(const K& key)
		{
			KOFV kofv;
			int index = key % tables.size();
			int start = index;//標誌位,尋找一遍的標誌位
			while (tables[index].state != EMPTY)
			{
				if (kofv(tables[index].data) == key)
				{
					if (tables[index].state == EXITS)
					{
						return &tables[index];
					}
					//tables[index].state == DELETE
					//這表情要找的元素被刪除了
					else
					{
						return nullptr;
					}
				}
				++index;
				if (index == tables.size())
				{
					index = 0;
					//找完了一遍沒有就退出
					if (index == start)
					{
						return nullptr;
					}
				}
			}
			return nullptr;
		}
		//刪除元素
		bool Erase(const K& key)
		{
			HashData* ret = Find(key);
			if (ret != nullptr)
			{
				ret->state = DELETE;
				num--;
				return true;
			}
			else
			{
				return false;
			}
		}
	private:
		vector<HashData> tables;
		size_t num = 0;//記錄已經存放了多少個資料
	};
	void TestHashTable1()
	{
		HashTable<int, int, KeyOfvalue<int, int>> ht;
		// HashTable<int, pair<int, int>, KeyOfValue<int, int>> ht;

		int arr[] = { 10,20,14,57,26,30,49,72,43,55,82 };
		for (auto e : arr)
		{
			if (e == 72)
			{
				int a = 0;
			}
			ht.Insert(e);
		}

		for (auto e : arr)
		{
			ht.Erase(e);
		}
	}

}

開雜湊

概念

開雜湊法: 又叫鏈地址法(開鏈法),首先對關鍵碼集合用雜湊函式計算雜湊地址,具有相同地址的關鍵碼歸於同一子集合,每一個子集合稱為一個桶,各個桶中的元素透過一個單鏈錶連結起來,各連結串列的頭結點儲存在雜湊表中。(如下圖)

C++進階(雜湊)

注意: 開雜湊中每個桶放的都是雜湊衝突的元素。雜湊桶下面掛著的是一個一個的節點(一條連結串列),如果該位置雜湊衝突的元素過多時,我們會選擇在這裡掛一顆紅黑樹

雜湊表開雜湊實現(整數版本雜湊桶)

整體框架

雜湊桶下面掛著的是一個一個的節點(一條連結串列),也就是每個位置存放連結串列頭節點的地址。這裡和開雜湊一樣,我們還是用vector來存放元素。模板引數列表中前三個就不過多介紹,和閉雜湊是一樣的,第四個引數後面介紹

template<class K, class V>
struct KeyOfvalue
{
	const K& operator()(const K& key)
	{
		return key;
	}
	const K& operator()(const pair<K, V>& kv)
	{
		return kv.first;
	}
};
template <class T>
struct HashNode
{
	HashNode(const T& data):data(data),next(nullptr){}
	T data;
	HashNode<T>* next;
};
template <class K,class T,class KOFV,class Hash>
class HashBucket
{
typedef HashNode<T> Node;
public:
private:
	vector<Node*> table;
	int num = 0;//記錄表中的資料個數
};

插入元素

有以下幾個步驟:

  1. 先根據元素個數考慮增容問題(下面詳細介紹)
  2. 再透過雜湊函式確定關鍵字的位置,然後把節點掛到這個桶下面(可以是連結串列的頭,也可以是連結串列的尾部)

增容問題: 當雜湊桶中元素個數打的一定個數時,就要增容,否則雜湊衝突的機率會變得,且時間複雜度會下降的很快。所以,雜湊桶一般是在元素個數等於桶的大小,也就是負載因子為1時,就開始增容。

  1. 先遍歷一遍雜湊桶的每個位置,然後對舊桶上的元素節點進行轉移
  2. 最後插入新節點

程式碼實現如下:

bool Insert(const T& data)
		{
			KOFV kofv;
			//插入之前,判斷是否需要增容,負載因子為1就增容
			if (num == tables.size())
			{
				vector<Node*> newtables;
				size_t newsize = tables.size() == 0 ? 10 : 2 * tables.size();
				newtables.resize(newsize);
				for (size_t i = 0; i < tables.size(); i++)
				{
					Node* prev = nullptr;
					Node* cur = tables[i];
					//把一個位置的所有節點轉義,然後換下一個位置
					while (cur)
					{
						//記錄下一個節點的位置
						Node* next = cur->next;
						int index = HashFunc(kofv(cur->data)) % newtables.size();
						//把cur連線到新的表上,頭插法
						cur->next = newtables[index];
						newtables[index] = cur;
						cur = next;// cur會發生變化,需要提前記錄next
					}
				}
				tables.swap(newtables);
			}
			int index = HashFunc(kofv(data)) % tables.size();
			//先查詢該條連結串列上是否有要插入的元素
			Node* cur = tables[index];
			while (cur)
			{
				if (kofv(cur->data) == kofv(data))
				{
					return false;
				}
				cur = cur->next;
			}
			//插入資料,選擇頭插,要注意的是,插入的元素必須在堆中建立,不能被釋放
			Node* newnode = new Node(data);
			newnode->next = tables[index];
			tables[index] = newnode;
			num++;
			return true;
		}

查詢元素

步驟:

  1. 先確定要查詢的元素在哪個桶
  2. 然後在該桶下的連結串列對元素進行查詢

程式碼實現如下:

Node* Find(const K& key)
		{
			KOFV kofv;
			int index = HashFunc(key) % tables.size();
			Node* cur = tables[index];
			while (cur)
			{
				if (key == cur->data)
				{
					return cur;
				}
				cur = cur->next;
			}
			return nullptr;
		}

刪除元素

步驟:

  1. 先找到元素
  2. 然後對元素節點進行刪除,沒找到就刪除失敗

程式碼實現如下:

bool Erase(const K& key)
		{
			KOFV kofv;
			int index = HashFunc(key) % tables.size();
			Node* prev = nullptr;
			Node* cur = tables[index];
			while (cur)
			{
				//如果找到了元素
				if (key == kofv(cur->data))
				{
					//找到了元素,並且就是連結串列中第一個節點的元素
					if (prev == nullptr)
					{
						tables[index] = cur->next;
					}
					else
					{
						prev->next = cur->next;
					}
					num--;
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->next;
			}
			return false;
		}

完整程式碼

template<class K, class V>
struct KeyOfvalue
{
	const K& operator()(const K& key)
	{
		return key;
	}
	const K& operator()(const pair<K, V>& kv)
	{
		return kv.first;
	}
};
namespace OPEN_HASH
{
	template <class T>
	struct HashNode
	{
		HashNode(const T& data):data(data),next(nullptr){}
		T data;
		HashNode<T>* next;
	};
	template <class K,class T,class KOFV,class Hash>
	class HashBucket
	{
		typedef HashNode<T> Node;
	public:
		bool Insert(const T& data)
		{
			KOFV kofv;
			//插入之前,判斷是否需要增容,負載因子為1就增容
			if (num == tables.size())
			{
				vector<Node*> newtables;
				size_t newsize = tables.size() == 0 ? 10 : 2 * tables.size();
				newtables.resize(newsize);
				for (size_t i = 0; i < tables.size(); i++)
				{
					Node* prev = nullptr;
					Node* cur = tables[i];
					//把一個位置的所有節點轉義,然後換下一個位置
					while (cur)
					{
						//記錄下一個節點的位置
						Node* next = cur->next;
						int index = HashFunc(kofv(cur->data)) % newtables.size();
						//把cur連線到新的表上,頭插法
						cur->next = newtables[index];
						newtables[index] = cur;
						cur = next;// cur會發生變化,需要提前記錄next
					}
				}
				tables.swap(newtables);
			}
			int index = HashFunc(kofv(data)) % tables.size();
			//先查詢該條連結串列上是否有要插入的元素
			Node* cur = tables[index];
			while (cur)
			{
				if (kofv(cur->data) == kofv(data))
				{
					return false;
				}
				cur = cur->next;
			}
			//插入資料,選擇頭插,要注意的是,插入的元素必須在堆中建立,不能被釋放
			Node* newnode = new Node(data);
			newnode->next = tables[index];
			tables[index] = newnode;
			num++;
			return true;
		}
		Node* Find(const K& key)
		{
			KOFV kofv;
			int index = HashFunc(key) % tables.size();
			Node* cur = tables[index];
			while (cur)
			{
				if (key == cur->data)
				{
					return cur;
				}
				cur = cur->next;
			}
			return nullptr;
		}
		bool Erase(const K& key)
		{
			KOFV kofv;
			int index = HashFunc(key) % tables.size();
			Node* prev = nullptr;
			Node* cur = tables[index];
			while (cur)
			{
				//如果找到了元素
				if (key == kofv(cur->data))
				{
					//找到了元素,並且就是連結串列中第一個節點的元素
					if (prev == nullptr)
					{
						tables[index] = cur->next;
					}
					else
					{
						prev->next = cur->next;
					}
					num--;
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->next;
			}
			return false;
		}
	private:
		vector<Node*> tables;
		int num = 0;//記錄表中的資料個數
	};
}

字串雜湊(最終版本雜湊桶)

在上面的雜湊桶中,只能存放key為整形的元素,這個問題應該如何解決呢?

答案:我們上面雜湊函式採用除留餘數法,key必須為整形才可以進行處理。所以我們需要採取一些措施,將這些key轉為整形

字串雜湊函式

因為雜湊函式的常用方法如直接定址、除留餘數、平方取中等方法需要用的 key值為整型,而大部分時候我們的 key 都是 string,由於無法對 string 進行算數運算,所以需要考慮新的方法。

常見的字串雜湊演算法有 BKD、SDB、RS 等,這些演算法大多透過一些公式來對字串每一個 字元的 ascii值 或者 字串的大小 進行計算,來推匯出一個不容易產生衝突的 key值 ,下面是一些字串轉換整數的Hash函式的比較: 戳這裡

我們選擇上面的一種,來進行使用。
實現如下: 因為較多情況下,key都是可以取模的,所以雜湊桶的模板引數列表中選擇直接返回key的函式作為預設引數。有因為字串雜湊用的也比較多,所以這裡對key為string型別進行一個特化

template<class K>
struct _Hash
{
	// 大多樹的型別就是是什麼型別就返回什麼型別
	const K& operator()(const K& key)
	{
		return key;
	}
};

// 特化string
template<>
struct _Hash<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		// 把字串的所有字母加起來   hash = hash*131 + key[i]
		for (size_t i = 0; i < key.size(); ++i)
		{
			hash *= 131;
			hash += key[i];
		}
		return hash;
	}
};

我們再實現一個雜湊函式,裡面是對key進行對應地轉換,然後返回整形。
實現如下:

size_t HashFunc(const K& key)
{
	Hash hash;
	return hash(key);
}

最終版本程式碼(整型字串型均適用)

#define _CRT_SECURE_NO_WARNINGS
#include<iostream> //引入標頭檔案
#include<vector>
#include<algorithm> 
using namespace std; //標準名稱空間
//演算法科學家總結出的一個增容質數表,按照這樣增容的效率更高
const int PRIMECOUNT = 28;
const size_t primeList[PRIMECOUNT] =
{
 53ul, 97ul, 193ul, 389ul, 769ul,
 1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
 49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
 1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
 50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
 1610612741ul, 3221225473ul, 4294967291ul
};
template<class K, class V>
struct KeyOfvalue
{
	const K& operator()(const K& key)
	{
		return key;
	}
	const K& operator()(const pair<K, V>& kv)
	{
		return kv.first;
	}
};
namespace Open_Hash
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{}
	};

	// 前置宣告
	template<class K, class T, class KOFV, class Hash = _Hash<K>>
	class HashBucket;

	template<class K, class T, class Ref, class Ptr, class KOFV, class Hash>
	struct __HashBucket_Iterator
	{
		typedef __HashBucket_Iterator<K, T, Ref, Ptr, KOFV, Hash> Self;
		typedef HashNode<T> Node;
		typedef HashBucket<K, T, KOFV, Hash> HashBucket;

		Node* _node;
		HashBucket* _phb;
		//Node* _node;
		//int _index;// 記錄此時迭代器在表中那個位置
		//vector<Node*>& _tables;

		//__HashBucket_Iterator(Node* node, int index, vector<Node*>& tables)
		//	:_node(node)
		//	,_index(index)
		//	,_tables(tables)
		//{}

		__HashBucket_Iterator(Node* node, HashBucket* phb)
			:_node(node)
			, _phb(phb)
		{}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		Self& operator++()
		{
			if (_node->_next)
			{
				_node = _node->_next;
				return *this;
			}
			else
			{
				KOFV kofv;
				int index = _phb->HashFunc(kofv(_node->_data)) % _phb->_tables.size();

				for (size_t i = index + 1; i < _phb->_tables.size(); ++i)
				{
					if (_phb->_tables[i])
					{
						_node = _phb->_tables[i];
						return *this;
					}
				}
				_node = nullptr;
				return *this;
			}
		}

		bool operator==(const Self& self) const
		{
			return _node == self._node
				&& _phb == self._phb;
		}

		bool operator!=(const Self& self) const
		{
			return !this->operator==(self);
		}
	};


	template<class K>
	struct _Hash
	{
		// 大多樹的型別就是是什麼型別就返回什麼型別
		const K& operator()(const K& key)
		{
			return key;
		}
	};

	// 特化string
	template<>
	struct _Hash<string>
	{
		size_t operator()(const string& key)
		{
			size_t hash = 0;
			// 把字串的所有字母加起來   hash = hash*131 + key[i]
			for (size_t i = 0; i < key.size(); ++i)
			{
				hash *= 131;
				hash += key[i];
			}
			return hash;
		}
	};
	// string型別用的比較多,所以就特化一個版本出來
	template<class K, class T, class KOFV, class Hash>
	class HashBucket
	{
		typedef HashNode<T> Node;
		friend struct __HashBucket_Iterator<K, T, T&, T*, KOFV, Hash>;
	public:
		typedef __HashBucket_Iterator<K, T, T&, T*, KOFV, Hash> iterator;

		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i] != nullptr)
					return iterator(_tables[i], this);// 雜湊桶的第一個節點 
			}
			return end();// 沒有節點返回最後一個迭代器
		}
		iterator end()
		{
			return iterator(nullptr, this);
		}
		~HashBucket()
		{
			Clear();
		}
		void Clear()
		{
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
			}
		}
		size_t HashFunc(const K& key)
		{
			Hash hash;
			return hash(key);
		}
		pair<iterator, bool> Insert(const T& data)
		{
			KOFV kofv;
			// 負載因子為1時就增容
			if (_num == _tables.size())
			{
				vector<Node*> newtables;
				//size_t newsize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				size_t newsize = GetNextPrime(_tables.size());
				newtables.resize(newsize);

				for (size_t i = 0; i < _tables.size(); ++i)
				{
					Node* prev = nullptr;
					Node* cur = _tables[i];

					// 把一個位置的所有節點轉移,然後換下一個位置
					while (cur)
					{
						// 記錄下一個節點的位置
						Node* next = cur->_next;

						int index = HashFunc(kofv(cur->_data)) % newtables.size();
						// 把cur連線到新的表上
						cur->_next = newtables[index];
						newtables[index] = cur;

						cur = next;// cur會發生變化,需要提前記錄next
					}
				}
				_tables.swap(newtables);
			}
			int index = HashFunc(kofv(data)) % _tables.size();
			// 先查詢該條連結串列上是否有要插入的元素
			Node* cur = _tables[index];
			while (cur)
			{
				if (kofv(cur->_data) == kofv(data))
					return make_pair(iterator(cur, this), false);
				cur = cur->_next;
			}
			// 插入資料,選擇頭插(也可以尾插)
			Node* newnode = new Node(data);
			newnode->_next = _tables[index];
			_tables[index] = newnode;
			++_num;

			return make_pair(iterator(newnode, this), true);
		}

		iterator Find(const K& key)
		{
			KOFV kofv;
			int index = HashFunc(key) % _tables.size();
			Node* cur = _tables[index];

			while (cur)
			{
				if (key == kofv(cur->_data))
				{
					return iterator(cur, this);
				}
				cur = cur->_next;
			}
			return iterator(nullptr);
		}

		bool Erase(const K& key)
		{
			KOFV kofv;
			int index = HashFunc(key) % _tables.size();

			Node* prev = nullptr;
			Node* cur = _tables[index];

			while (cur)
			{
				if (key == kofv(cur->_data))
				{
					// 刪第一個節點時
					if (prev == nullptr)
					{
						_tables[index] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					--_num;
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}
	private:
		size_t GetNextPrime(size_t prime)
		{
			size_t i = 0;
			for (; i < PRIMECOUNT; ++i)
			{
				//返回比那個數大的下一個質數 
				if (primeList[i] > prime)
					return primeList[i];
			}
			//如果比所有都大,還是返回最後一個,因為最後一個已經是32位最大容量
			return primeList[PRIMECOUNT - 1];
		}
	private:
		vector<Node*> _tables;
		int _num = 0;// 記錄表中的資料個數
	};

	void TestHashBucket1()
	{
		HashBucket<int, int, KeyOfvalue<int, int>> ht;
		int arr[] = { 15,23,57,42,82,26,30,49,72,43,55 };
		for (auto e : arr)
		{
			ht.Insert(e);
		}

		for (auto e : arr)
		{
			HashBucket<int, int, KeyOfvalue<int, int>>::iterator it = ht.begin();

			while (it != ht.end())
			{
				cout << *it << " ";
				++it;
			}
			cout << endl;
			ht.Erase(e);
		}
	}

	void TestHashBucket2()
	{
		HashBucket<string, string, KeyOfvalue<string, string>> ht;

		ht.Insert("sort");
		ht.Insert("pass");
		ht.Insert("cet6");
		HashBucket<string, string, KeyOfvalue<string, string>>::iterator it = ht.begin();
		while (it != ht.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

	}
}
int main()
{
	Open_Hash::TestHashBucket1();
	Open_Hash::TestHashBucket2();
	system("pause");
	return EXIT_SUCCESS;
}

相關文章