C++:哈希(闭散列、开散列)
文章目錄
- 哈希概念
- 哈希沖突
- 哈希函數
- 哈希沖突解決
- 閉散列
- 什么時機增容,如何增容?
- 線性探測的實現
- 開散列
- 開散列增容
- 開散列的實現
- 開散列與閉散列比較
- unordered_map模擬實現(應用開散列)
- 知識點習題:
哈希概念
順序結構以及平衡樹中,元素關鍵碼與其存儲位置之間沒有對應的關系,因此在查找一個元素時,必須要經過關鍵碼的多次比較。順序查找時間復雜度為O(N),平衡樹中為樹的高度,即O(log2N),搜索的效率取決于搜索過程中元素的比較次數。
理想的搜索方法:不經過任何比較,一次直接從表中得到要搜索的元素。 如果構造一種存儲結構,通過某種函數(hashFunc)使元素的存儲位置與它的關鍵碼之間能夠建立一一映射的關系,那么在查找時通過該函數可以很快找到該元素。
當向該結構中:
-
插入元素
根據待插入元素的關鍵碼,以此函數計算出該元素的存儲位置并按此位置進行存放
-
搜索元素
對元素的關鍵碼進行同樣的計算,把求得的函數值當做元素的存儲位置,在結構中按此位置取元素比較,若關鍵碼相等,則搜索成功
該方式即為哈希(散列)方法,哈希方法中使用的轉換函數稱為哈希(散列)函數,構造出來的結構稱為哈希表 (Hash Table)(或者稱散列表)
例如:數據集合{1,7,6,4,5,9};
哈希函數設置為:hash(key) = key % capacity; capacity為存儲元素底層空間總的大小。
用該方法進行搜索不必進行多次關鍵碼的比較,因此搜索的速度比較快。 問題:按照上述哈希方式,向集合中插入元素44,會出現什么問題?(哈希沖突)
哈希沖突
對于兩個數據元素的關鍵字ki和 kj(i != j),有 ki != kj ,但有:Hash( ki ) == Hash( kj ),即:不同關鍵字通過相同哈希哈數計算出相同的哈希地址,該種現象稱為哈希沖突或哈希碰撞。
把具有不同關鍵碼而具有相同哈希地址的數據元素稱為“同義詞”。 發生哈希沖突該如何處理呢?
哈希函數
引起哈希沖突的一個原因可能是:哈希函數設計不夠合理。 哈希函數設計原則:
-
哈希函數的定義域必須包括需要存儲的全部關鍵碼,而如果散列表允許有m個地址時,其值域必須在0 到m-1之間
-
哈希函數計算出來的地址能均勻分布在整個空間中
-
哈希函數應該比較簡單
常見哈希函數
取關鍵字的某個線性函數為散列地址:Hash(Key)= A*Key + B
- 優點:簡單、均勻
- 缺點:需要事先知道關鍵字的分布情況 使用場景:適合查找比較小且連續的情況
設散列表中允許的地址數為m,取一個不大于m,但最接近或者等于m的質數p作為除數,按照哈希函數:Hash(key) = key% p(p<=m),將關鍵碼轉換成哈希地址
假設關鍵字為1234,對它平方就是1522756,抽取中間的3位227作為哈希地址; 再比如關鍵字為 4321,對它平方就是18671041,抽取中間的3位671(或710)作為哈希地址
平方取中法比較適合:不知道關鍵字的分布,而位數又不是很大的情況
折疊法是將關鍵字從左到右分割成位數相等的幾部分(最后一部分位數可以短些),然后將這幾部分疊加求和,并按散列表表長,取后幾位作為散列地址。
折疊法適合事先不需要知道關鍵字的分布,適合關鍵字位數比較多的情況
選擇一個隨機函數,取關鍵字的隨機函數值為它的哈希地址,即H(key) = random(key),其中random為隨機數函數。
通常應用于關鍵字長度不等時采用此法
設有n個d位數,每一位可能有r種不同的符號,這r種不同的符號在各位上出現的頻率不一定相同,可能在某些位上分布比較均勻,每種符號出現的機會均等,在某些位上分布不均勻只有某幾種符號經常出現。可根據散列表的大小,選擇其中各種符號分布均勻的若干位作為散列地址。例如:
假設要存儲某家公司員工登記表,如果用手機號作為關鍵字,那么極有可能前7位都是相同的,那么我們可以選擇后面的四位作為散列地址,如果這樣的抽取工作還容易出現沖突,還可以對抽取出來的數字進行反轉(如1234改成4321)、右環位移(如1234改成4123)、左環移位、前兩數與后兩數疊加(如1234改 成12+34=46)等方法。
數字分析法通常適合處理關鍵字位數比較大的情況,如果事先知道關鍵字的分布且關鍵字的若干位分布較均勻的情況
注意:哈希函數設計的越精妙,產生哈希沖突的可能性就越低,但是無法避免哈希沖突
哈希沖突解決
閉散列
也叫開放定址法,當發生哈希沖突時,如果哈希表未被裝滿,說明在哈希表中必然還有空位置,那么可以把key存放到沖突位置中的“下一個” 空位置中去。 那如何尋找下一個空位置呢?
比如
現在需要插入元素44,先通過哈希函數計算哈希地址,hashAddr為4,因此44理論上應該插在該位置,但是該位置已經放了值為4的元素,即發生哈希沖突。
線性探測:從發生沖突的位置開始,依次向后探測,直到尋找到下一個空位置為止。
-
插入
通過哈希函數獲取待插入元素在哈希表中的位置
如果該位置中沒有元素則直接插入新元素,如果該位置中有元素發生哈希沖突,使用線性探測找到下一個空位置,插入新元素
- 刪除
采用閉散列處理哈希沖突時,不能隨便物理刪除哈希表中已有的元素,若直接刪除元素會影響其他元素的搜索。比如刪除元素4,如果直接刪除掉,44查找起來可能會受影響。因此線性探測采用標記的偽刪除法來刪除一個元素。
// 哈希表每個空間給個標記// EMPTY此位置空, EXIST此位置已經有元素, DELETE元素已經刪除 enum State{EMPTY, EXIST, DELETE};什么時機增容,如何增容?
線性探測的實現
#pragma once#include <map> #include <vector> using namespace std;namespace dg {enum State {EMPTY,EXIST,DELETE };// 整形數據不需要轉化 class dealInt { public:int operator()(int n){return n;} };// key為字符串類型,需要將其轉化為整形 class dealString { public:int operator()(const string& n){int sum = 0;int seed = 131; // 131進制for (const char& c : n){sum = sum * seed + c;}return sum & 0x7FFFFFFF; // 為了保證得到的index的第一位為0,也就是為了得到一個正數。} };template<class K, class V, class SW = dealInt> class hashTable {struct elem{pair<K, V> m_val;State m_state;elem(const K& key = K(), const V& val = V(), State state = EMPTY) :m_val(key, val),m_state(state){}};vector<elem> m_table;size_t m_size;static long long s_m_primeTable[30];int m_primePos; public:hashTable(size_t capacity = s_m_primeTable[0]) :m_table(capacity),m_size(0),m_primePos(0){}size_t capacity(){return m_table.size();}private:int hashFunc(const K& key){SW func;return func(key) % capacity();}void reserve(){vector<elem> tmp;m_table.swap(tmp);m_table.resize(s_m_primeTable[++m_primePos]);m_size = 0;for (auto& e : tmp){if (e.m_state == EXIST){insert(e.m_val);}}}public:bool insert(const pair<K, V>& val){if ((long long)size() * 100 / capacity() >= 75){reserve();}int n = hashFunc(val.first);while (m_table[n].m_state == EXIST){if (m_table[n].m_val.first == val.first){return false;}n++;if (n == capacity()){n = 0;}}m_table[n].m_val = val;m_table[n].m_state = EXIST;m_size++;return true;}int find(const K& key){int n = hashFunc(key);while (m_table[n].m_state != EMPTY){if (m_table[n].m_state == EXIST && m_table[n].m_val.first == key){return n;}n++;if (n == capacity()){n = 0;}}return -1;}bool erase(const K& key){int ret = find(key);if (ret < 0){return false; // 不存在}else{m_table[ret].m_state = DELETE;m_size--;}}size_t size(){return m_size;}bool empty(){return m_size == 0;}// 交換兩個表(容器)void Swap(hashTable<K, V>& ht){m_table.swap(ht.m_table);size_t tmp;tmp = m_size;m_size = ht.m_size;ht.m_size = tmp;} };// 素數表(除留余數法,最好模一個素數) template<class K, class V, class SW> long long hashTable<K, V, SW>::s_m_primeTable[30] = {11, 23, 47, 89, 179,353, 709, 1409, 2819, 5639,11273, 22531, 45061, 90121, 180233,360457, 720899, 1441807, 2883593, 5767169,11534351, 23068673, 46137359, 92274737, 184549429,369098771, 738197549, 1476395029, 2952790016u, 4294967291u };};線性探測優點: 實現非常簡單,
線性探測缺點: 一旦發生哈希沖突,所有的沖突連在一起,容易產生數據“堆積”,即:不同關鍵碼占據 了可利用的空位置,使得尋找某關鍵碼的位置需要許多次比較,導致搜索效率降低。 如何緩解呢?
線性探測的缺陷是產生沖突的數據堆積在一塊,這與其找下一個空位置有關系,因為找空位置的方式就 是挨著往后逐個去找,因此二次探測為了避免該問題,找下一個空位置的方法為:Hi = (H0 + i2)% m, 或者: Hi= ( H0 - i2 )% m。其中:i = 1,2,3…, 是通過散列函數Hash(x)對元素的關鍵碼 key 進行 計算得到的位置,m是表的大小。
研究表明:當表的長度為質數且表裝載因子a不超過0.5時,新的表項一定能夠插入,而且任何一個位置都不會被探查兩次。因此只要表中有一半的空位置,就不會存在表滿的問題。在搜索時可以不考慮表裝 滿的情況,但在插入時必須確保表的裝載因子a不超過0.5,如果超出必須考慮增容。
因此:閉散列最大的缺陷就是空間利用率比較低,這也是哈希的缺陷。
開散列
開散列法又叫鏈地址法(開鏈法),首先對關鍵碼集合用散列函數計算散列地址,具有相同地址的關鍵碼歸于同一子集合,每一個子集合稱為一個桶,各個桶中的元素通過一個單鏈表鏈接起來,各鏈表的頭結點存儲在哈希表中。
從上圖可以看出,開散列中每個桶中放的都是發生哈希沖突的元素
開散列增容
桶的個數是一定的,隨著元素的不斷插入,每個桶中元素的個數不斷增多,極端情況下,可能會導致一個桶中鏈表節點非常多,會影響的哈希表的性能,因此在一定條件下需要對哈希表進行增容,那該條件怎么確認呢?開散列最好的情況是:每個哈希桶中剛好掛一個節點,再繼續插入元素時,每一次都會發生哈希沖突,因此,在元素個數剛好等于桶的個數時,可以給哈希表增容。
開散列的實現
#pragma once #include <vector> using namespace std;template<class T> class HashBucketNode {T m_val;HashBucketNode<T>* m_next;HashBucketNode(const T& val = T()) :m_val(val),m_next(nullptr){}template<class T, class SW>friend class HashSet; };class dealInt { public:int operator()(int n){return n;} };template<class T, class SW = dealInt> class HashSet {vector<HashBucketNode<T>*> m_data;size_t m_size;static long long s_m_primeTable[30];int m_primePos; public:HashSet(size_t capacity = s_m_primeTable[0]) :m_data(capacity, nullptr),m_size(0),m_primePos(0){}private:int hashFunc(const T& key){SW func;return func(key) % capacity();}void checkCapacity(){if (m_size == capacity()){int mcapa = capacity();vector<HashBucketNode<T>*> tmp(s_m_primeTable[++m_primePos], nullptr);m_data.swap(tmp);m_size = 0;int i;HashBucketNode<T>* cur;for (i = 0; i < mcapa; i++){for (cur = tmp[i]; cur; cur = cur->m_next){insert(cur->m_val);}}}}public:bool insert(const T& val){checkCapacity();int hashnum = hashFunc(val);HashBucketNode<T>* tmp;if (m_data[hashnum]){for (tmp = m_data[hashnum]; tmp; tmp = tmp->m_next){if (tmp->m_val == val){return false;}}}tmp = new HashBucketNode<T>(val);tmp->m_next = m_data[hashnum];m_data[hashnum] = tmp;m_size++;return true;}bool erase(const T& val){int hashnum = hashFunc(val);HashBucketNode<T>* tmp;if (!m_data[hashnum]){return false;}if (m_data[hashnum]->m_val == val){tmp = m_data[hashnum];m_data[hashnum] = tmp->m_next;delete tmp;m_size--;return true;}else{for (tmp = m_data[hashnum]; tmp->m_next; tmp = tmp->m_next){if (tmp->m_next->m_val == val){HashBucketNode<T>* cur;cur = tmp->m_next;tmp->m_next = cur->m_next;delete cur;m_size--;return true;}}return false;}}HashBucketNode<T>* find(const T& val){int hashnum = hashFunc(val);HashBucketNode<T>* cur;for (cur = m_data[hashnum]; cur; cur = cur->m_next){if (cur->m_val == val){return cur;}}return nullptr;}void clear(){HashBucketNode<T>* tmp;for (auto& head : m_data){while (head){tmp = head;head = head->m_next;delete tmp;}}m_size = 0;}size_t capacity(){return s_m_primeTable[m_primePos];} };template<class T, class SW> long long HashSet<T, SW>::s_m_primeTable[30] = {11, 23, 47, 89, 179,353, 709, 1409, 2819, 5639,11273, 22531, 45061, 90121, 180233,360457, 720899, 1441807, 2883593, 5767169,11534351, 23068673, 46137359, 92274737, 184549429,369098771, 738197549, 1476395029, 2952790016u, 4294967291u };開散列與閉散列比較
應用鏈地址法處理溢出,需要增設鏈接指針,似乎增加了存儲開銷。事實上: 由于開地址法必須保持大 量的空閑空間以確保搜索效率,如二次探查法要求裝載因子a <= 0.5,而表項所占空間又比指針大的多,所以使用鏈地址法反而比開地址法節省存儲空間。
unordered_map模擬實現(應用開散列)
main函數文件
#include "unordered_map.h" #include <iostream> using namespace std;int main() {dg::unordered_map<int, int> hb;hb[2] = 7;hb[4] = 6;hb[11] = 14;hb[7] = 9;hb[5] = 1;for (auto& e : hb){cout << e.first << ' ' << e.second << endl;}return 0; }unordered_map頭文件
#include "HashBucket.h"namespace dg {template <class K, class V, class HF = dealInt> class unordered_map {class KeyofValue{public:const K& operator()(const pair<K, V>& data){return data.first;}};HashBucket<K, pair<K, V>, KeyofValue, HF> m_hb;public:// typename 是為了識別類型typename typedef HashBucket<K, pair<K, V>, KeyofValue, HF>::iterator iterator;unordered_map() :m_hb(){}~unordered_map(){m_hb.~HashBucket();}iterator begin(){return m_hb.begin();}iterator end(){return m_hb.end();}iterator size(){return m_hb.size();}iterator find(const V& val){return m_hb.find(val);}size_t count(const K& key){return m_hb.count(key);}void clear(){return m_hb.clear();}bool empty(){return m_hb.empty();}pair<iterator, bool> insert(const pair<K, V> val){return m_hb.insert(val);}V& operator[](const K& key){pair<iterator, bool> ptmp = m_hb.insert(pair<K, V>(key, V()));iterator itmp = ptmp.first;return (*itmp).second;}const V& operator[](const K& key) const{return (*(m_hb.insert(pair<K, V>(key, V()))).first).second;} };};哈希桶頭文件
#pragma once #include <vector> using namespace std;namespace dg {template<class T>class HashBucketNode{T m_val;HashBucketNode<T>* m_next;HashBucketNode(const T& val = T()) :m_val(val),m_next(nullptr){}template<class K, class V, class KeyofValue, class HF>friend class HashBucket;};class dealInt{public:int operator()(int n){return n;}};template<class K, class V, class KeyofValue, class HF = dealInt>class HashBucket{vector<HashBucketNode<V>*> m_data;size_t m_size;static long long s_m_primeTable[30];int m_primePos;public:HashBucket(size_t capacity = s_m_primeTable[0]) :m_data(capacity, nullptr),m_size(0),m_primePos(0){}~HashBucket(){clear();}class iterator{public:HashBucket<K, V, KeyofValue, HF>* m_hb;HashBucketNode<V>* m_node;iterator(HashBucketNode<V>* node = nullptr,HashBucket<K, V, KeyofValue, HF>* hbpos = nullptr) :m_node(node),m_hb(hbpos){}iterator(const iterator& it) :m_node(it.m_node),m_hb(it.m_hb){}V& operator*(){return m_node->m_val;}V* operator->(){return &m_node->m_val;}// 前置++iterator operator++(){V val = m_node->m_val;m_node = m_node->m_next;if (!m_node){int bucketno = m_hb->hashFunc(KeyofValue()(val)) + 1;for (; bucketno < m_hb->capacity(); bucketno++){if (m_hb->m_data[bucketno]){m_node = m_hb->m_data[bucketno];break;}}}return *this;}iterator operator++(int){HashBucket<K, V, KeyofValue, HF> tmp = *this;++(*this);return tmp;}bool operator==(const iterator& data) const{return m_node == data.m_node && m_hb == data.m_hb;}bool operator!=(const iterator& data) const{return m_node != data.m_node || m_hb != data.m_hb;}};private:int hashFunc(const K& key){HF func;return func(key) % capacity();}void checkCapacity(){if (m_size == capacity()){int mcapa = capacity();vector<HashBucketNode<V>*> tmp(s_m_primeTable[++m_primePos], nullptr);m_data.swap(tmp);m_size = 0;int i;HashBucketNode<V>* cur;for (i = 0; i < mcapa; i++){for (cur = tmp[i]; cur; cur = cur->m_next){insert(cur->m_val);}}}}public:iterator begin(){int bucketno = 0;for (; bucketno < capacity(); bucketno++){if (m_data[bucketno]){return iterator(m_data[bucketno], this);}}return iterator(nullptr, this);}iterator end(){return iterator(nullptr, this);}pair<iterator, bool> insert(const V& val){checkCapacity();int hashnum = hashFunc(KeyofValue()(val));HashBucketNode<V>* tmp;if (m_data[hashnum]){for (tmp = m_data[hashnum]; tmp; tmp = tmp->m_next){if (tmp->m_val == val){pair<iterator, bool> pairtmp;pairtmp.first = end();pairtmp.second = false;return pairtmp;}}}tmp = new HashBucketNode<V>(val);tmp->m_next = m_data[hashnum];m_data[hashnum] = tmp;m_size++;pair<iterator, bool> pairtmp;iterator it(m_data[hashnum], this);pairtmp.first = it;pairtmp.second = true;return pairtmp;}iterator erase(const V& val){int hashnum = hashFunc(KeyofValue()(val));HashBucketNode<V>* tmp;if (!m_data[hashnum]){return end();}if (m_data[hashnum]->m_val == val){iterator res(m_data[hashnum], this);++res;tmp = m_data[hashnum];m_data[hashnum] = tmp->m_next;delete tmp;m_size--;return res;}else{for (tmp = m_data[hashnum]; tmp->m_next; tmp = tmp->m_next){if (tmp->m_next->m_val == val){iterator res(tmp->m_next, this);++res;HashBucketNode<V>* cur;cur = tmp->m_next;tmp->m_next = cur->m_next;delete cur;m_size--;return res;}}return end();}}iterator find(const V& val){int hashnum = hashFunc(KeyofValue()(val));HashBucketNode<V>* cur;for (cur = m_data[hashnum]; cur; cur = cur->m_next){if (cur->m_val == val){return iterator(cur, this);}}return iterator(nullptr, this);}void clear(){HashBucketNode<V>* tmp;for (auto& head : m_data){while (head){tmp = head;head = head->m_next;delete tmp;}}m_size = 0;}size_t capacity(){return s_m_primeTable[m_primePos];}size_t size(){return m_size;}bool empty(){return m_size == 0;}size_t count(const K& kv){int bucketno = hashFunc(kv);HashBucketNode<V>* cur;for (cur = m_data[bucketno]; cur; cur = cur->m_next){if (KeyofValue()(cur->m_val) == kv){return 1;}}return 0;}size_t bucketCount(){return capacity();}size_t bucketSize(size_t bucketno){HashBucketNode<V>* cur;int count = 0;for (cur = m_data[bucketno]; cur; cur = cur->next){count++;}return count;}};template<class K, class V, class KeyofValue, class HF>long long HashBucket<K, V, KeyofValue, HF>::s_m_primeTable[30] = {11, 23, 47, 89, 179,353, 709, 1409, 2819, 5639,11273, 22531, 45061, 90121, 180233,360457, 720899, 1441807, 2883593, 5767169,11534351, 23068673, 46137359, 92274737, 184549429,369098771, 738197549, 1476395029, 2952790016u, 4294967291u};};代碼生成圖
知識點習題:
A. 哈希表不可以用數組實現
B. 隊列可以用數組實現
C. 二叉樹可以用數組來實現
D. 棧可以用單項鏈表來實現
正確答案: A
A. jbk1.7中,在多線程環境下,擴容時會造成數據丟失
B. jbk1.7中,在多線程環境下,擴容時會造成環形鏈
C. jbk1.8中,在多線程環境下,擴容時會造成環形鏈
D. jbk1.8中,在多線程環境下,會發生數據覆蓋的情況
正確答案: C
答案解析:
HashMap的線程不安全體現在會造成死循環、數據丟失、數據覆蓋這些問題。其中死循環和數據丟失是在JDK1.7中出現的問題,在JDK1.8中已經得到解決,然而1.8中仍會有數據覆蓋這樣的問題。(Java 方面)
如有不同見解,歡迎留言討論
總結
以上是生活随笔為你收集整理的C++:哈希(闭散列、开散列)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构-栈(Stack)
- 下一篇: S5PV210 LCD屏