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