【C++】哈希(闭散列,开散列)
哈希
- unordered系列關(guān)聯(lián)式容器
- unordered_map&& unordered_set
- 關(guān)聯(lián)性容器介紹
- 介紹
- 底層結(jié)構(gòu)
- 哈希概念
- 哈希沖突
- 閉散列
- 閉散列介紹
- 實(shí)現(xiàn)
- 開散列
- 介紹
- 開散列增容機(jī)制
- 實(shí)現(xiàn)
- 開散列和閉散列比較
unordered系列關(guān)聯(lián)式容器
unordered_map&& unordered_set
關(guān)聯(lián)性容器介紹
在STL底層map和set使用紅黑樹封裝實(shí)現(xiàn)的,而其復(fù)雜度為基本為logn因?yàn)楦叨瓤煽?#xff0c;但往后發(fā)展還是有大神想出了另一種容器結(jié)構(gòu)也就是哈希。也就是底層用哈希來封裝出map和set
介紹
注意:unordered_map中key是不能重復(fù)的,因此count函數(shù)的返回值最大為1
無論是unordered_map/se其使用規(guī)則和map和set沒有多大差別就是底層實(shí)現(xiàn)不同。效率不同。
底層結(jié)構(gòu)
哈希概念
順序結(jié)構(gòu)以及平衡樹中,元素關(guān)鍵碼與其存儲位置之間沒有對應(yīng)的關(guān)系,因此在查找一個(gè)元素時(shí),必須要經(jīng)過關(guān)鍵碼的多次比較。順序查找時(shí)間復(fù)雜度為O(N),平衡樹中為樹的高度,即O(N),搜索的效率取決于搜索過程中元素的比較次數(shù)。
理想的搜索方法:可以不經(jīng)過任何比較,一次直接從表中得到要搜索的元素。 如果構(gòu)造一種存儲結(jié)構(gòu),通過某種函數(shù)(hashFunc)使元素的存儲位置與它的關(guān)鍵碼之間能夠建立一一映射的關(guān)系,那么在查找時(shí)通過該函數(shù)可以很快找到該元素。
插入元素:
根據(jù)待插入元素的關(guān)鍵碼,以此函數(shù)計(jì)算出該元素的存儲位置并按此位置進(jìn)行存放
搜索元素:
對元素的關(guān)鍵碼進(jìn)行同樣的計(jì)算,把求得的函數(shù)值當(dāng)做元素的存儲位置,在結(jié)構(gòu)中按此位置取元素比較,若關(guān)鍵碼相等,則搜索成功
該方式即為哈希(散列)方法,哈希方法中使用的轉(zhuǎn)換函數(shù)稱為哈希(散列)函數(shù),構(gòu)造出來的結(jié)構(gòu)稱為哈希表(Hash Table)(或者稱散列表)
例如:
每次給一個(gè)數(shù)對其進(jìn)行取模,得到一個(gè)值對其進(jìn)行插入,有點(diǎn)之前計(jì)數(shù)排序那意思。
差的元素多了不可避免地就會(huì)有重復(fù)的字眼。
哈希沖突
對于不同關(guān)鍵字取模相同出一樣的哈希地址,這個(gè)我們叫做哈希沖突。
通過哈希函數(shù)來解決。
設(shè)計(jì)原則:
常見哈希函數(shù):直接定制法,除留余數(shù)法,平方取中法,折疊法,隨機(jī)數(shù)法,數(shù)學(xué)分析法。
經(jīng)常用的就是前兩個(gè)
注意:哈希函數(shù)設(shè)計(jì)的越精妙,產(chǎn)生哈希沖突的可能性就越低,但是無法避免哈希沖突
閉散列
閉散列介紹
閉散列:也叫開放定址法,當(dāng)發(fā)生哈希沖突時(shí),如果哈希表未被裝滿,說明在哈希表中必然還有空位置,那么可以把key存放到?jīng)_突位置中的“下一個(gè)”空位置中去
1.線性探測
對于已經(jīng)沖突的關(guān)鍵字,我們對其進(jìn)行線性查找,通過哈希函數(shù)找到對應(yīng)的位置,若沒有就插入,若有就一次往后找空位置插入。
這里就對于散列的每一個(gè)位置進(jìn)行狀態(tài)的定義,采用閉散列處理哈希沖突時(shí),不能隨便物理刪除哈希表中已有的元素,若直接刪除元素會(huì)影響其他元素的搜索。比如刪除元素4,如果直接刪除掉,44查找起來可能會(huì)受影響。因此線性探測采用標(biāo)記的偽刪除法來刪除一個(gè)元素
擴(kuò)容機(jī)制:
當(dāng)所有元素插滿時(shí)候,其狀態(tài)都為存在那么就無法找出對應(yīng)空的位置一致循環(huán)下去,所以我們要保證閉散列不能為滿或者不能使它快滿了,在達(dá)到一定的比例時(shí),就要對其進(jìn)行擴(kuò)容機(jī)制。
2.二次探測
可以通過一次探測得出如果同一塊區(qū)域有大量的數(shù)據(jù)堆積在一起,效率會(huì)降低,也就是將一個(gè)個(gè)探測變成次方的查找
研究表明:當(dāng)表的長度為質(zhì)數(shù)且表裝載因子a不超過0.5時(shí),新的表項(xiàng)一定能夠插入,而且任何一個(gè)位置都不會(huì)被探查兩次。因此只要表中有一半的空位置,就不會(huì)存在表滿的問題。在搜索時(shí)可以不考慮表裝滿的情況,但在插入時(shí)必須確保表的裝載因子a不超過0.5,如果超出必須考慮增容。
空間利用率比較低,哈希的缺陷
實(shí)現(xiàn)
enum State{EXIST,EMPTY,DELETE,};template<class T>struct HashNode{State _state = EMPTY; // 狀態(tài)T _t;};template<class K, class T, class HashFunc = Hash<K>>class HashTable{public:bool Insert(const T& t){// 負(fù)載因子>0.7就增容if (_tables.size() == 0 || _size * 10 / _tables.size() == 7){size_t newsize = GetNextPrime(_tables.size());HashTable<K, T> newht;newht._tables.resize(newsize);for (auto& e : _tables){if (e._state == EXIST){// 復(fù)用沖突時(shí)探測的邏輯newht.Insert(e._t);}}_tables.swap(newht._tables);}HashNode<T>* ret = Find(t);if (ret)return false;size_t start = t % _tables.size();// 線性探測,找一個(gè)空位置size_t index = start;size_t i = 1;while (_tables[index]._state == EXIST){index = start + i;index %= _tables.size();++i;}_tables[index]._t = t;_tables[index]._state = EXIST;_size++;return true;}如果傳入的是個(gè)字符串,那么我們就需要將其轉(zhuǎn)成ASC||碼進(jìn)行計(jì)算,就需要引入仿函數(shù)特化一個(gè)結(jié)構(gòu)體如果傳入的是string就去調(diào)特化的。
template<class K> struct Hash {size_t operator()(const K& key){return key;} };// 特化 template<> struct Hash < string > {size_t operator()(const string& s){size_t hash = 0;for (auto ch : s){//hash += ch;hash = hash * 131 + ch;}return hash;} };當(dāng)然查找和刪除也類似:查找存在并且相等的值,遇到空則失敗。刪除就得先查找然后設(shè)置狀態(tài)。插入的時(shí)候要先對其進(jìn)行查找如果有了就失敗
開散列
介紹
開散列法又叫鏈地址法(開鏈法),首先對關(guān)鍵碼集合用散列函數(shù)計(jì)算散列地址,具有相同地址的關(guān)鍵碼歸于同一子集合,每一個(gè)子集合稱為一個(gè)桶,各個(gè)桶中的元素通過一個(gè)單鏈表鏈接起來,各鏈表的頭結(jié)點(diǎn)存儲在哈希表中。
開散列增容機(jī)制
開散列的擴(kuò)容機(jī)制不會(huì)讓自己的插入影響,如果通過一個(gè)桶中插入的節(jié)點(diǎn)過多不回去影響其他的只會(huì)影響自己的效率,最好的情況就是每個(gè)桶中一個(gè)節(jié)點(diǎn)就是不需要二次搜索,不會(huì)出現(xiàn)哈希沖突。
實(shí)現(xiàn)
當(dāng)進(jìn)入size為0就給第一個(gè)素?cái)?shù)(這里大神們研究的通過開成某些素?cái)?shù)大小的容量不容易沖突),創(chuàng)建一個(gè)新對象并將舊表上的點(diǎn)一個(gè)個(gè)的插入新表當(dāng)中,然后釋放舊表節(jié)點(diǎn)并至空。
pair<Node*, bool> Insert(const T& t){KeyOfT kot;// 負(fù)載因子 == 1時(shí)增容if (_size == _tables.size()){size_t newsize = GetNextPrime(_tables.size());vector<Node*> newtables;newtables.resize(newsize, nullptr);for (size_t i = 0; i < _tables.size(); i++){// 舊表中節(jié)點(diǎn)直接取下來掛到新表Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t index = HashFunc(kot(cur->_t), newtables.size());// 頭插cur->_next = newtables[index];newtables[index] = cur;cur = next;}_tables[i] = nullptr;}newtables.swap(_tables);}size_t index = HashFunc(kot(t), _tables.size());// 查找t在在不在Node* cur = _tables[index];while (cur){if (kot(cur->_t) == kot(t))return make_pair(cur, false);cur = cur->_next;}Node* newnode = new Node(t);newnode->_next = _tables[index];_tables[index] = newnode;return make_pair(newnode, true);}開散列和閉散列比較
應(yīng)用鏈地址法處理溢出,需要增設(shè)鏈接指針,似乎增加了存儲開銷。事實(shí)上: 由于開地址法必須保持大量的空閑空間以確保搜索效率,如二次探查法要求裝載因子a <= 0.7,而表項(xiàng)所占空間又比指針大的多,所以使用鏈地址法反而比開地址法節(jié)省存儲空間
總結(jié)
以上是生活随笔為你收集整理的【C++】哈希(闭散列,开散列)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle 触发器(根据条件修改插入后
- 下一篇: VC _T和L