在未達(dá)成目的之前,一切具有誘惑力的事物都顯得那么不堪一擊
一、unordered系列關(guān)聯(lián)式容器
1.
在C++98中,STL提供了底層為紅黑樹結(jié)構(gòu)的一系列關(guān)聯(lián)式容器,在查詢時(shí)效率可達(dá)到
l
o
g
2
N
log_2 N
log2?N,即最差情況下需要比較紅黑樹的高度次,當(dāng)樹中的節(jié)點(diǎn)非常多時(shí),查詢效率也不理想。
最好的查詢是,只要進(jìn)行很少的比較次數(shù)就能夠?qū)⒃卣业剑虼嗽贑++11中,STL又提供了4個(gè)unordered系列的關(guān)聯(lián)式容器,這四個(gè)容器與紅黑樹結(jié)構(gòu)的關(guān)聯(lián)式容器使用方式基本類似,只是其底層結(jié)構(gòu)不同,他們不再以紅黑樹作為底層結(jié)構(gòu),而是以掛哈希桶的哈希表作為底層結(jié)構(gòu),就是用存儲(chǔ)結(jié)點(diǎn)指針的vector來實(shí)現(xiàn)哈希表,哈希表的每個(gè)位置是一個(gè)桶,桶結(jié)構(gòu)是一個(gè)存儲(chǔ)value的單鏈表,unordered_set的桶中結(jié)點(diǎn)存儲(chǔ)的是一個(gè)key值,unordered_map的桶中結(jié)點(diǎn)存儲(chǔ)的是一個(gè)鍵值對(duì)。
2.
哈希最大的作用就是查找,如果你想進(jìn)行排序什么的,哈希迭代器遍歷的結(jié)果是無序的,只有map和set遍歷的結(jié)果才是有序的,所以哈希并不具有排序的功能,unordered_map和unordered_set僅僅只有去重的功能而已。
所以如果你想快速查找一個(gè)值,那就用哈希,如果你想排序什么的,就不要用哈希了,哈希只能幫助你快速查找,因?yàn)樗牟檎倚驶旧辖咏?shù)次,效率很快。
二、哈希函數(shù)和哈希沖突
1.通過某種映射關(guān)系得到關(guān)鍵碼在哈希表中的哈希地址,這樣的計(jì)算關(guān)系其實(shí)就是哈希函數(shù)。
- 直接定址法–(常用)
取關(guān)鍵字的某個(gè)線性函數(shù)為散列地址:Hash(Key)= A*Key + B,常用的A是1,B是0。
優(yōu)點(diǎn):簡(jiǎn)單、均勻
缺點(diǎn):需要事先知道關(guān)鍵字的分布情況,若分布較廣,則空間消耗比較高。
使用場(chǎng)景:適合查找比較小且連續(xù)的情況 - 除留余數(shù)法–(常用)
設(shè)散列表中允許的地址數(shù)為m,取一個(gè)不大于m,但最接近或者等于m的質(zhì)數(shù)p作為除數(shù),
按照哈希函數(shù):Hash(key) = key% p(p<=m),將關(guān)鍵碼轉(zhuǎn)換成哈希地址,一般不這么干,最常用的就是拿vector.size()作為除數(shù),每次擴(kuò)容將vector.size()擴(kuò)容二倍。但后面開散列的解決方式那里,我們會(huì)仿照庫(kù),用質(zhì)數(shù)的集合作為vector.size(),然后用其作為除數(shù)。
下面是SGI版本的質(zhì)數(shù)集合,容器每次擴(kuò)容的大小用的都是下面集合中的某一個(gè)質(zhì)數(shù)。
2.
當(dāng)多個(gè)關(guān)鍵碼key在通過哈希函數(shù)映射之后,得到了相同的哈希地址,也就是多個(gè)key映射到同一個(gè)位置上時(shí),這種現(xiàn)象稱為哈希沖突或哈希碰撞。解決哈希沖突的辦法一般為兩種,一種是閉散列的方式解決,即用線性探測(cè)或二次探測(cè)的方式向后尋找空的哈希位置,一種是開散列的方式解決,即將哈希沖突的元素通過單鏈表鏈接,邏輯上像哈希表掛了一個(gè)個(gè)的桶,所以這樣的解決方式也可稱為拉鏈法,或哈希桶方式。
數(shù)據(jù)集合{1, 7, 6, 4, 5, 9}
在下面的哈希表中插入一個(gè)31,則其映射位置是1,但是1位置已經(jīng)有元素1了,此時(shí)就會(huì)發(fā)生哈希沖突,那就需要向后找空的位置插入31,這就是閉散列。
數(shù)據(jù)集合{3, 13, 4, 7, 17, 1, 21, 6, 46, 2, 16}
發(fā)生哈希沖突的元素都被掛到原有結(jié)點(diǎn)的下面去了,邏輯上就像哈希表掛了一個(gè)個(gè)的桶。桶里面是哈希沖突元素的集合。
三、閉散列(你搶我的位置,我搶他的位置)
1.哈希表結(jié)構(gòu)
1.
由于這里的閉散列方法無須重點(diǎn)掌握,所以在實(shí)現(xiàn)時(shí)我們就不分key和鍵值對(duì)分別為存儲(chǔ)元素時(shí)的情況了,這里只用鍵值對(duì)作為存儲(chǔ)元素講解哈希閉散列的方法。
2.
對(duì)于閉散列結(jié)構(gòu),我們采用vector作為底層容器,用vector來存儲(chǔ)哈希結(jié)點(diǎn),哈希結(jié)點(diǎn)是一個(gè)結(jié)構(gòu)體,其中存儲(chǔ)鍵值對(duì)和狀態(tài)值,_state用于標(biāo)定哈希映射位置為空、存在、刪除三種狀態(tài)。
為了判斷什么時(shí)候進(jìn)行哈希表的擴(kuò)容,在hashTable類中多增加了一個(gè)無符號(hào)整型的_n變量,表示當(dāng)前哈希表中存儲(chǔ)數(shù)據(jù)的個(gè)數(shù),方便我們用數(shù)據(jù)個(gè)數(shù)和vector.size()作除法,看結(jié)果是否大于負(fù)載因子,如果大于則擴(kuò)容,如果不大于則繼續(xù)插入。
enum state
{
EMPTY,
EXIST,
DELETE
};
template <class K, class V>
struct HashNode
{
HashNode()
: _state(EMPTY)
{}
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _state(EMPTY)
{}
pair<K, V> _kv;
enum state _state;
};
template <class K, class V, class Hash = BKDRHash<K>>
class HashTable
{
public:
typedef HashNode<K, V> Node;
HashTable()
:_n(0)
{
_tables.resize(10);//需要給vector的size一個(gè)初始值,否則計(jì)算負(fù)載因子可能產(chǎn)生除0錯(cuò)誤
}
………………省略
private:
vector<Node> _tables;
size_t _n;//利用_n和_tables.size()去搞負(fù)載因子
};
2.Insert()
1.
閉散列的解決方式即為通過哈希函數(shù)求出key對(duì)應(yīng)的映射位置后,如果自己的映射位置已存在元素,則線性探測(cè)向后尋找空的位置進(jìn)行插入,比如下面的21的映射位置應(yīng)該是1,但是1號(hào)位有元素1了,那21只能向后探測(cè)為空的位置進(jìn)行插入,此時(shí)就會(huì)引發(fā)一個(gè)問題,21占了別的元素的映射位置,如果此時(shí)插入一個(gè)元素2,則2的映射位置也被搶了,那2就只能向后探測(cè)為空的位置了。
所以閉散列的解決方法說白了就是你搶我的位置,那我就會(huì)去搶別人的位置。
2.
我們不希望哈希表的所有空間都被占用,這樣在查找的時(shí)候,哈希表的效率會(huì)非常的低,因?yàn)樾枰闅v,所以在哈希表中存儲(chǔ)元素到達(dá)一定程度后,要對(duì)哈希表進(jìn)行擴(kuò)容,重新建立映射關(guān)系,緩解哈希沖突。
3.
在實(shí)現(xiàn)擴(kuò)容時(shí),可以新建立一個(gè)vector,然后遍歷原來的vector的每一個(gè)元素,重新計(jì)算每一個(gè)元素在新vector的映射關(guān)系,然后將每一個(gè)元素進(jìn)行插入,交換兩個(gè)vector,則哈希表的擴(kuò)容就完成了,但是這樣的寫法代碼有點(diǎn)冗余,因?yàn)橹匦掠?jì)算哈希映射位置的過程還需要重寫一份。
所以另一種寫法就是代碼復(fù)用,我們不再新建立vector,而是新建立一個(gè)哈希表,對(duì)新哈希表中的vector進(jìn)行擴(kuò)容,然后調(diào)用哈希表的Insert函數(shù),將原vector中的鍵值對(duì)的關(guān)鍵碼插入到新哈希表當(dāng)中,這樣就不需要自己在寫代碼,進(jìn)行代碼復(fù)用即可。最后將新哈希表中的vector和原哈希表的vector進(jìn)行swap即可,這樣就完成了原有數(shù)據(jù)到新表中的挪動(dòng),然后再插入要插入的kv即可。
在函數(shù)調(diào)用結(jié)束之后,臨時(shí)對(duì)象newHT會(huì)被銷毀,那我們還需要寫哈希表的析構(gòu)函數(shù)嗎?其實(shí)是不需要的,哈希表類默認(rèn)生成的析構(gòu)函數(shù)對(duì)內(nèi)置類型_n不處理,對(duì)自定義類型vector調(diào)用其析構(gòu)函數(shù),vector存儲(chǔ)內(nèi)容都可以看作是內(nèi)置類型,因?yàn)殒I值對(duì)說到底也就是單一的結(jié)構(gòu)體,所以vector的析構(gòu)函數(shù)直接將vector所占用的一大塊空間全部還給操作系統(tǒng)即可。那么我們就不需要自己寫哈希表的析構(gòu)函數(shù),vector會(huì)幫我們做析構(gòu)處理,并且內(nèi)置類型_n成員還不用處理。
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
//大于標(biāo)定的負(fù)載因子,進(jìn)行擴(kuò)容,降低哈希沖突的概率
if (_n * 10 / _tables.size() > 7)//可能會(huì)出現(xiàn)除0錯(cuò)誤
{
//舊表數(shù)據(jù),重新計(jì)算,映射到新表
/*vector<Node> newtables;
newtables.resize(2 * _tables.size()); */
HashTable<K, V, BKDRHash<K>> newHT;
newHT._tables.resize(2 * _tables.size());
for (auto& e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
//取原表中的數(shù)據(jù)插入到新表的vector里面,鍵值對(duì)之間發(fā)生賦值重載。因?yàn)閚ewHT是新開的初始化好的哈希表
//遞歸通常是自己調(diào)用自己,這里不是遞歸,僅僅是代碼復(fù)用而已。
}
}
_tables.swap(newHT._tables);
}
size_t hashi = Hash()(kv.first) % _tables.size();//這里不能%capacity,某些位置不是可用的,vector[]會(huì)對(duì)下標(biāo)檢查
while (_tables[hashi]._state == EXIST)
{
//線性探測(cè)
++hashi;
//二次探測(cè)
//hashi = hashi + i * i;//降低沖突概率,但還是有可能會(huì)沖突,占其他位置
hashi %= _tables.size();
}
/*_tables[hashi] = Node(kv);
_tables[hashi]._state = EXIST;*/
//在構(gòu)造新表對(duì)象時(shí),默認(rèn)構(gòu)造已經(jīng)初始化好哈希表里面的結(jié)點(diǎn)空間了,你再開空間拷貝數(shù)據(jù)浪費(fèi)。
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_n;
return true;
}
3.Erase()(標(biāo)記的偽刪除法)
1.
大部分?jǐn)?shù)據(jù)結(jié)構(gòu)容器的刪除其實(shí)都是偽刪除或者叫做惰性刪除,因?yàn)槲覀儫o法做到釋放一大塊空間的某一部分空間,所以在數(shù)據(jù)結(jié)構(gòu)這里的刪除基本都是用標(biāo)記的偽刪除,另一種常見的方式就是,堆排序的刪除,我們當(dāng)時(shí)用的也是size標(biāo)識(shí)可用空間大小的方式,實(shí)際空間并沒有改變,我們只是改變了自己的訪問邏輯,不去訪問array的size下標(biāo)空間,但空間其實(shí)還原封不動(dòng)的在那里。
2.
哈希表的刪除也一樣,我們?cè)诿總€(gè)結(jié)點(diǎn)里面增加一個(gè)狀態(tài)標(biāo)記,用狀態(tài)來標(biāo)記當(dāng)前結(jié)點(diǎn)是否被刪除。如果刪除結(jié)點(diǎn)不存在,則返回false。
bool Erase(const K& key)
{
Node* ret = Find(key);
if (ret == nullptr)
return false;
ret->_state = DELETE;
--_n;
return true;
}
4.Find()
1.
查找的代碼其實(shí)比較簡(jiǎn)單,只需要先利用要查找的key值求出映射的哈希地址,如果映射地址對(duì)應(yīng)的key值不是我們要查找的key值,則說明發(fā)生了哈希沖突,那就需要映射的哈希地址向后線性探測(cè)尋找要查找的鍵值對(duì),找到后返回對(duì)應(yīng)鍵值對(duì)的地址。
2.
那什么時(shí)候查找結(jié)束呢?或者說向后循環(huán)查找的條件是什么呢?一般來說,如果映射位置的key和要查找的key不匹配,則一定說明要查找的key是沖突元素,如果這個(gè)沖突元素在哈希表里面,就比如31和21,則從1位置向后找,一定能夠找到他們。如果這個(gè)沖突元素不在哈希表里面,比如27,那從7位置開始找,7后面是空,此時(shí)就不用在向后繼續(xù)查找了,因?yàn)橹灰?7存在,那7和27之間一定是連續(xù)的,所以如果從映射位置向后找,遇到empty狀態(tài)的結(jié)點(diǎn),就不用查找了,你要查找的哈希沖突元素一定不在哈希表里面。
3.
在線性探測(cè)中,如果查找到尾部了,則讓hashi%=vector的size即可,讓hashi回到開頭的位置。但有一種極端特殊情況,就是邊插入邊刪除,這樣整個(gè)哈希表中的結(jié)點(diǎn)狀態(tài)有可能都是delete或exist,則在線性探測(cè)中不會(huì)遇到empty,while會(huì)陷入死循環(huán),所以在while里面多加一層判斷,如果start等于hashi,說明在哈希表中已經(jīng)線性探測(cè)一圈了,那此時(shí)就返回,因?yàn)檎伊艘蝗Χ紱]找到key,那就說明key不在哈希表里面。
4.
上面所說的是一種解決方式,但還有另外一種解決方式,由于delete和exist的存在,不是找不到empty嗎?而且delete的作用和empty的作用是一致的,那我們可以直接摒棄掉原來的枚舉值,將枚舉值縮減為empty和exist,去掉delete,在erase之后,將結(jié)點(diǎn)狀態(tài)設(shè)置為empty,這樣哈希表中的結(jié)點(diǎn)狀態(tài)只有空和存在,那就不會(huì)發(fā)生下面這樣沒有empty的情況,其實(shí)在枚舉的設(shè)計(jì)上確實(shí)有點(diǎn)問題,empty和exist已經(jīng)足夠了,完全不需要delete。(兄弟們,這樣的解決方式是錯(cuò)誤的,因?yàn)槿绻@么干了,比如一片哈希沖突,刪除中間的某個(gè)元素之后,后面的沖突元素就找不到了,因?yàn)槲覀冋J(rèn)為的是沖突元素之間一定是連續(xù)的,不能出現(xiàn)empty,delete狀態(tài)的出現(xiàn)就是為了應(yīng)對(duì)這樣的情況的)
Node* Find(const K& key)
{
size_t hashi = Hash()(key) % _tables.size();
size_t start = hashi;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._kv.first == key && _tables[hashi]._state == EXIST)
{
return &_tables[hashi];
}
++hashi;
hashi %= _tables.size();//防止越界
if (start == hashi)
break;
}
return nullptr;
}
5.
上面閉散列的哈希表是不用寫拷貝,賦值,析構(gòu)的,因?yàn)槟J(rèn)生成的拷貝對(duì)vector會(huì)調(diào)用他的拷貝,pair也有自己的拷貝,析構(gòu)也不用,直接釋放vector的空間即可。
5.哈希表key值不能取模無法映射的解決方法(BKDRHash)
1.
上面舉例子時(shí),鍵值對(duì)的key值都是整型,整型當(dāng)然可以完成映射,那如果是自定義類型string呢?string如何對(duì)vector的size取模呢?此時(shí)就需要仿函數(shù)來完成自定義類型轉(zhuǎn)換為整型的操作了,只有轉(zhuǎn)換為整型,我們才能取模,進(jìn)而才能完成哈希映射的工作。
對(duì)于其他類型,比如int,char,short,double等,我們直接強(qiáng)轉(zhuǎn)為size_t,這樣就可以完成哈希映射。
2.
哈希和紅黑樹都有key,兩者的key各自都有什么要求呢?紅黑樹要求key能比較大小,因?yàn)樗尨蟮膋ey到右面,小的key到左面。哈希要求key能夠取模,也就是要求key能轉(zhuǎn)成整型,因?yàn)樗瓿晒S成?/strong>。
3.
字符串轉(zhuǎn)換為整型的場(chǎng)景還是比較常見的,所以有人整理了一篇字符串哈希算法,思路就是將每一個(gè)字符對(duì)應(yīng)的ascll碼分別拆下來,每次的hash值都為上一次的hash值×131后再加上字符的ascll碼值,遍歷完字符串后,最后的hash為字符串轉(zhuǎn)成整型的結(jié)果,這樣每個(gè)字符串轉(zhuǎn)換后的整型是極大概率不重復(fù)的,是一個(gè)非常不錯(cuò)的哈希算法,被人們稱為BKDRHash。
字符串哈希算法
template <class K>
struct BKDRHash
{
size_t operator()(const K& key)
{
return (size_t)key;//只要這個(gè)地方能轉(zhuǎn)成整型,那就可以映射,指針浮點(diǎn)數(shù)負(fù)數(shù)都可以,但string不行
}
};
template <>
struct BKDRHash<string>
{
size_t operator()(const string& key)
{
//return key[0];//字符串第一個(gè)字符是整型,那就可以整型提升,只要是個(gè)整型能進(jìn)行%模運(yùn)算,完成映射即可。
size_t hash = 0;
for (auto ch : key)
{
hash = hash * 131 + ch;
}
return hash;
}
};
四、開散列(掛哈希桶的方式)
1.哈希表結(jié)構(gòu) && 構(gòu)造和析構(gòu)函數(shù)
1.
開散列的哈希表是最常用的方式,庫(kù)里面的unordered_map和unordered_set用的也是哈希桶的方式實(shí)現(xiàn)的,我們模擬實(shí)現(xiàn)的哈希桶也仿照庫(kù)實(shí)現(xiàn),哈希結(jié)點(diǎn)node里面存儲(chǔ)鍵值對(duì)和下一個(gè)結(jié)點(diǎn)指針。
在哈希表的模板參數(shù)中,也多加了一個(gè)缺省仿函數(shù)類的參數(shù),也就是Hash,因?yàn)槲覀冃枰狧ash的仿函數(shù)對(duì)象或匿名構(gòu)造,將key轉(zhuǎn)成整型。
template <class K, class V>
struct hashNode
{
hashNode(const pair<K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
pair<K, V> _kv;
hashNode<K, V>* _next;
};
template <class K, class V, class Hash = BKDRHash<K>>
class hashTable
{
public:
typedef hashNode<K, V> Node;
…………省略
private:
vector<Node*> _table;
size_t _n;
};
2.
對(duì)于哈希桶,我們必須寫出析構(gòu)函數(shù),因?yàn)榫幾g器默認(rèn)生成的析構(gòu)函數(shù)會(huì)調(diào)用vector的析構(gòu),而vector的析構(gòu)僅僅只能將自己的空間還給操作系統(tǒng),如果某些節(jié)點(diǎn)指針指向了具體的節(jié)點(diǎn),則只歸還vector的空間是不夠的,還需要?dú)w還那些申請(qǐng)的節(jié)點(diǎn)空間。
所以需要遍歷每一個(gè)哈希桶,將每一個(gè)桶里面的節(jié)點(diǎn)都還給操作系統(tǒng),這里就用到單鏈表的節(jié)點(diǎn)刪除的知識(shí)了,在刪除前需要保留下一個(gè)位置,要不然delete歸還空間之后就找不到下一個(gè)節(jié)點(diǎn)的位置了。
hashTable()
:_n(0)
{
_table.resize(10);
}
~hashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;//把桶中所有結(jié)點(diǎn)空間的使用權(quán)還給操作系統(tǒng),這就是析構(gòu)函數(shù)的作用
cur = next;
}
_table[i] = nullptr;
}
}
2.Insert()(單鏈表的頭插)
1.
我下面畫的圖只是想說明一下哈希桶的邏輯結(jié)構(gòu)和擴(kuò)容之后緩解哈希沖突的場(chǎng)景,但實(shí)際在插入節(jié)點(diǎn)時(shí)并不是像我下面畫的那樣對(duì)單鏈表進(jìn)行尾插,因?yàn)槲膊暹€需要找尾,那就需要遍歷桶,這樣的效率太低,并且桶中也不要求次序什么的,所以我們直接進(jìn)行頭插即可,頭插的效率很高,因?yàn)橛成湔业焦5刂分蠹纯蛇M(jìn)行頭插。
2.
哈希桶的負(fù)載因子,官方默認(rèn)值為1.0,那就是_n和vector.size()相等的時(shí)候進(jìn)行擴(kuò)容,擴(kuò)容的目的還是重新建立映射關(guān)系,緩解哈希沖突,因?yàn)槿绻骋粋€(gè)哈希桶的結(jié)點(diǎn)個(gè)數(shù)過多,在哈希映射之后還需要遍歷哈希桶尋找結(jié)點(diǎn),會(huì)降低哈希查找的效率,所以擴(kuò)容就是多增加哈希桶的個(gè)數(shù),減少平均哈希桶中結(jié)點(diǎn)的個(gè)數(shù),提高哈希查找的效率。
3.
擴(kuò)容這里的思路和閉散列的哈希表比較相似,如果我們遍歷原有結(jié)點(diǎn)的數(shù)據(jù),將每個(gè)結(jié)點(diǎn)的數(shù)據(jù)重新new一個(gè)結(jié)點(diǎn)出來,然后插入到新的vector里面,或者是代碼復(fù)用的方式進(jìn)行插入,這兩種都可以,但是效率太低了,上面所說的兩種代碼寫法都是新new結(jié)點(diǎn)插入到新表的,如果有1w個(gè)結(jié)點(diǎn)呢?我們平白無故的拷貝出來1w個(gè)結(jié)點(diǎn),然后臨時(shí)對(duì)象銷毀的時(shí)候又析構(gòu)1w個(gè)結(jié)點(diǎn)?這不是冤大頭么。為什么不將原來的結(jié)點(diǎn)鏈接到新的vector上面呢?
4.
所以另一種寫法就是遍歷原表的每個(gè)結(jié)點(diǎn)指針,將每個(gè)指針指向結(jié)點(diǎn)的key重新計(jì)算哈希映射關(guān)系,頭插到新的vector里面,在每完成一個(gè)桶的重新映射關(guān)系后,將原vector中的桶位置的指針置為空,否則析構(gòu)的時(shí)候,結(jié)點(diǎn)會(huì)被析構(gòu)兩遍。等到原表的所有結(jié)點(diǎn)遍歷完之后,將新的vector和原來的vector一交換即可,臨時(shí)對(duì)象_newtable在離開函數(shù)棧幀時(shí)會(huì)被銷毀,調(diào)用vector的默認(rèn)析構(gòu)完成空間的歸還即可。
5.
研究表明,每次除留余數(shù)法最好模一個(gè)素?cái)?shù),這會(huì)大概率降低哈希沖突的可能性。所以我們下面的擴(kuò)容大小每次挑選小于2倍的最大素?cái)?shù)作為擴(kuò)容后的vector大小,這里復(fù)用了一下stl庫(kù)里面的素?cái)?shù)表。
inline unsigned long __stl_next_prime(unsigned long n)
{
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
for (size_t i = 0; i < __stl_num_primes; i++)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return __stl_prime_list[__stl_num_primes - 1];
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))//不允許重復(fù)元素
return false;
//負(fù)載因子控制在1,超過就擴(kuò)容
if (_n == _table.size())
{
/*hashTable<K, V, Hash> _newHT;
_newHT._table.resize(2 * _table.size());*/
//for (int hashi = 0; hashi < _table.size(); hashi++)
//{
// Node* cur = _table[hashi];
// while (cur)
// {
// _newtable.Insert(cur);//需要重新改變映射關(guān)系
// cur = cur->_next;
// }
//}
//_table.swap(_newHT._table);
//上面是冤大頭的做法,下面是正常的做法。
vector<Node*> _newtable;
_newtable.resize(__stl_next_prime(_table.size()), nullptr);//resize開空間后,默認(rèn)值為Node*()的構(gòu)造,我們也可以自己寫
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = Hash()(cur->_kv.first) % _newtable.size();
cur->_next = _newtable[hashi];
_newtable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(_newtable);
}
size_t hashi = Hash()(kv.first) % _table.size();
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];//newnode的next指向當(dāng)前表哈希映射位置的結(jié)點(diǎn)地址
_table[hashi] = newnode;//讓newnode做頭
++_n;
return true;
}
3.Erase()(歸還結(jié)點(diǎn)空間的使用權(quán))
1.
哈希桶的erase其實(shí)就是單鏈表結(jié)點(diǎn)的刪除,如果是頭刪,那就是下一個(gè)指針作頭,如果是中間刪除,則記錄前一個(gè)結(jié)點(diǎn)位置,讓前一個(gè)結(jié)點(diǎn)的next指向刪除結(jié)點(diǎn)的next。
然后歸還結(jié)點(diǎn)空間的使用權(quán),即為delete結(jié)點(diǎn)指針。
bool Erase(const K& key)
{
Node* ret = Find(key);
if (!ret)
return false;
size_t hashi = Hash()(key) % _table.size();
Node* cur = _table[hashi];
if (cur->_kv.first == key)//頭刪
{
_table[hashi] = cur->_next;
delete cur;
cur = nullptr;
}
else//中間刪除
{
while (cur)
{
Node* prev = cur;
cur = cur->_next;
if (cur->_kv.first == key)
{
prev->_next = cur->_next;
delete cur;
cur = nullptr;
}
}
}
--_n;
return true;
}
4.Find()
1.
哈希桶的查找和閉散列的哈希表很相似,先通過key找到映射的哈希桶,然后去對(duì)應(yīng)的哈希桶里面找查找的結(jié)點(diǎn)即可,找到返回結(jié)點(diǎn)地址,未找到返回nullptr即可。
Node* Find(const K& key)
{
size_t hashi = Hash()(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
五、封裝實(shí)現(xiàn)unordered系列容器(不一樣的const迭代器)
1.普通迭代器(單向迭代器)
1.
封裝實(shí)現(xiàn)unordered系列容器的insert,find,erase等接口并不是什么難事,直接調(diào)用開散列哈希表的接口即可,而封裝的主要關(guān)鍵點(diǎn)其實(shí)是實(shí)現(xiàn)容器的迭代器操作,只要實(shí)現(xiàn)了迭代器的操作,那我們自己封裝的unordered系列容器基本上就能跑起來了。
2.
如果要實(shí)現(xiàn)迭代器++的操作,如果我們只有結(jié)點(diǎn)的指針是無法完成迭代器++的,因?yàn)槿绻闅v所有的哈希桶的結(jié)點(diǎn),則必須需要哈希表本身,只有這樣才能確定下一個(gè)哈希桶的位置,所以開散列哈希表的迭代器需要多封裝一個(gè)哈希表指針,通過哈希表指針和哈希結(jié)點(diǎn)的指針,就可以訪問整個(gè)表里面所有的結(jié)點(diǎn)了。
3.
迭代器的++就是看當(dāng)前指針的下一個(gè)是否為空,如果為空說明當(dāng)前哈希桶里面只有他一個(gè)結(jié)點(diǎn),那就需要尋找下一個(gè)哈希桶,將結(jié)點(diǎn)指針的指向移動(dòng)到下一個(gè)哈希桶的結(jié)點(diǎn)指針處,如果向后找的過程中結(jié)點(diǎn)指針都是nullptr,那就是沒有哈希桶,我們將_node置為空即可,說明此時(shí)迭代器已經(jīng)到end()的位置了,不能繼續(xù)遍歷下去了。
需要額外多說一點(diǎn)的是,由于迭代器內(nèi)部用了哈希表指針,所以需要在迭代器的前面加上哈希表的模板聲明。而且在迭代器內(nèi)部還訪問了哈希表的私有成員_table,則需要在哈希表里面進(jìn)行友元聲明,將迭代器模板聲明為哈希表的友元。
//前置聲明
template <class K, class T, class Hash, class KeyOfT>
class hashTable;
template <class K, class T, class Hash, class KeyOfT>
struct __HTIterator
{
typedef hashNode<T> Node;
typedef hashTable<K, T, Hash, KeyOfT> HT;
typedef __HTIterator<K, T, Hash, KeyOfT> Self;
Node* _node;
HT* _ht;
__HTIterator(Node* node, HT* ht)
:_node(node)
,_ht(ht)
{}
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
//當(dāng)前桶走完了,要去哈希表里面找下一個(gè)桶
size_t hashi = Hash()(KeyOfT()(_node->_data)) % _ht->_table.size();
hashi++;
while (hashi != _ht->_table.size() && _ht->_table[hashi] == nullptr)
{
hashi++;
}
if (hashi == _ht->_table.size())
_node = nullptr;
else
_node = _ht->_table[hashi];
}
return *this;
}
T& operator->()
{
return &_node->_data;
}
T* operator*()
{
return _node->_data;
}
bool operator!=(const Self& it)const
{
return _node != it._node;
}
bool operator==(const Self& it)const
{
return _node == it._node;
}
};
2.為什么hashTable的const迭代器要重新寫一個(gè)類?
2.1 庫(kù)里面是怎么做的?
1.
我們可以先來看一下常規(guī)的迭代器操作,比如紅黑樹的迭代器,他的const迭代器并沒有重寫一個(gè)類,而是直接增加模板參數(shù)Ref和Ptr來完成對(duì)應(yīng)普通引用和常引用的值的返回。
并且支持普通迭代器構(gòu)造const迭代器的操作,實(shí)際上STL的所有容器在實(shí)現(xiàn)迭代器的時(shí)候,都會(huì)用下面的方式來支持普通迭代器構(gòu)造const迭代器,如果是普通迭代器調(diào)用,那這里就是普通和普通之間的拷貝,沒啥用因?yàn)榫幾g器也支持這樣的操作,但下面那段代碼目的不是支持普通迭代器之間的拷貝,而是支持普通到const迭代器的構(gòu)造操作。
2.
但是哈希表的迭代器卻沒有通過增加模板參數(shù)來解決,而是重寫了一個(gè)struct __hashtable_const_iterator { }類,以這樣的方式來實(shí)現(xiàn)unordered_map和unordered_set的const迭代器操作。
2.2 不能通過增加模板參數(shù)解決嗎?(權(quán)限不能放大)
1.
其實(shí)能否通過增加模板參數(shù)解決const迭代器主要取決于迭代器類中的構(gòu)造函數(shù),之前能通過增加模板參數(shù)解決是因?yàn)闊o論是構(gòu)造const迭代器還是構(gòu)造普通迭代器,我們傳給構(gòu)造函數(shù)的指針都是普通指針,當(dāng)然可以構(gòu)造出普通迭代器和const迭代器了,因?yàn)閮蓚€(gè)迭代器的差別主要是*和→重載的返回值是可修改的還是不可修改的,這個(gè)并不難解決。
2.
而現(xiàn)在不一樣了,哈希迭代器傳的指針不是普通指針了,而是const指針,當(dāng)調(diào)用begin()或end()函數(shù)的對(duì)象是const對(duì)象時(shí),this指針指向的內(nèi)容是不可修改的,并且由于對(duì)象是const對(duì)象,vector的[ ]返回的也是const引用,而vector存儲(chǔ)的是指針,那就說明哈希node結(jié)點(diǎn)指針指向的內(nèi)容也是不可修改的。
如果我們此時(shí)用這兩個(gè)指針去構(gòu)造const迭代器,而哈希迭代器類成員變量是兩個(gè)普通指針,那在構(gòu)造函數(shù)處就會(huì)發(fā)生const指針拷貝給普通指針的情況,此時(shí)權(quán)限會(huì)放大,所以如果你用增加模板參數(shù)來實(shí)現(xiàn)const迭代器,則編譯器一定會(huì)報(bào)錯(cuò),因?yàn)橹羔樅鸵迷谫x值時(shí),權(quán)限只能平移和縮小,不能放大。
3.
所以唯一的解決方式就是重寫const迭代器類,將const迭代器類的私有成員變量改為兩個(gè)const指針,這樣在構(gòu)造函數(shù)處構(gòu)造const迭代器時(shí),就不會(huì)出現(xiàn)權(quán)限的放大了,只是發(fā)生權(quán)限的平移。
template <class K, class T, class Hash, class KeyOfT>
class hashTable
{
public:
friend struct __HTIterator<K, T, Hash, KeyOfT>;
typedef hashNode<T> Node;
typedef __HTIterator<K, T, Hash, KeyOfT> iterator;
iterator begin()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i])
{
return iterator(_table[i], this);//如果是const對(duì)象,這里返回const指針
}
}
return iterator(nullptr, this);//如果哈希表是空的或者沒有桶,則構(gòu)造空迭代器。
}
iterator end()
{
return iterator(nullptr, this);
}
2.3 哈希表和其他STL容器 支持普通迭代器構(gòu)造const迭代器的本質(zhì)是什么?(權(quán)限的縮小和平移)
1.
哈希表的迭代器是個(gè)特殊的存在,因?yàn)樗腸onst和非const迭代器是兩個(gè)類模板,而STL的其他容器的const和非const迭代器都是出自一個(gè)類模板。
2.
本質(zhì)還是因?yàn)楣1淼腸onst迭代器的私有成員變量得是const指針,而其他容器的const迭代器的私有成員變量只是普通指針。所以哈希表的普通迭代器構(gòu)造const迭代器其實(shí)是權(quán)限的縮小,由普通指針賦值到const指針。而其他容器的普通迭代器構(gòu)造const迭代器其實(shí)是權(quán)限的平移,因?yàn)闊o論是普通迭代器還是const迭代器,他們的成員變量都是普通指針。
3.unordered_map的[ ]操作
1.
支持map的[ ]操作并不困難,因?yàn)樗腫 ]操作其實(shí)就是通過insert返回迭代器和bool的鍵值對(duì)來實(shí)現(xiàn)的。當(dāng)[ ]內(nèi)的key不存在,則調(diào)用哈希表的Inset完成key和V()構(gòu)造的鍵值對(duì)的插入,并返回插入鍵值對(duì)的迭代器和true的bool值構(gòu)造的鍵值對(duì)。當(dāng)[ ]內(nèi)的key在哈希表中存在時(shí),則哈希表的Insert也會(huì)返回指向key和value鍵值對(duì)的迭代器以及false的bool值構(gòu)造的鍵值對(duì)。
2.
所以實(shí)現(xiàn)[ ]的重?fù)?dān)主要是在Insert上面,只要Insert返回迭代器,那就能通過迭代器拿到鍵值對(duì)的value值,再通過返回value值的引用就可以修改哈希表中某一鍵值對(duì)的value值了。文章來源:http://www.zghlxwxcb.cn/news/detail-408260.html
下面是[ ]函數(shù)和哈希表底層的Insert函數(shù),Insert的邏輯沒有變,只是將他的返回值從bool改為了鍵值對(duì)而已。文章來源地址http://www.zghlxwxcb.cn/news/detail-408260.html
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
pair<iterator,bool> Insert(const T& data)
{
KeyOfT kot;
iterator it = Find(kot(data));
if (it != end())
return make_pair(it, false);//如果插入的值已經(jīng)存在那就不再進(jìn)行插入,返回對(duì)應(yīng)位置迭代器即可。
if (_n == _table.size())
{
vector<Node*> _newtable;
_newtable.resize(__stl_next_prime(_table.size()), nullptr);
for (int i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = Hash()(kot(cur->_data)) % _newtable.size();
cur->_next = _newtable[hashi];
_newtable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(_newtable);
}
size_t hashi = Hash()(kot(data)) % _table.size();
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return make_pair(iterator(newnode, this), true);
}
void test_unordered_map()
{
string arr[] = { "蘋果", "西瓜", "香蕉", "草莓", "蘋果", "西瓜", "蘋果", "蘋果", "西瓜",
"蘋果", "香蕉", "蘋果", "香蕉" , "藍(lán)莓" ,"草莓" };
unordered_map<string, int> countMap;
for (auto& e : arr)
{
countMap[e]++;
}
unordered_map<string, int>::iterator it = countMap.begin();
//1.不用語(yǔ)法糖,一點(diǎn)一點(diǎn)遍歷也可以
while (it != countMap.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
//2.我們實(shí)現(xiàn)了迭代器,直接用語(yǔ)法糖也可以。
for (const auto& kv : countMap)//將解引用后的迭代器賦值給kv
{
cout << kv.first << ":" << kv.second << endl;
}
}
到了這里,關(guān)于【C++】開散列哈希表封裝實(shí)現(xiàn)unordered_map和unordered_set的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!