14 哈希表和哈希桶
文章目錄
- 一、哈希表
- 二、哈希函數
- 2.1. 直接定址法(常用)
- 2.2. 除留余數法(常用)
- 2.3. 幾種不常用的方法
- 三、哈希沖突
- 四、閉散列
- 4.1. 線性探測
- 4.2. 負載因子
- 4.3. 二次探測
- 4.4. 插入和刪除操作
- 4.5. 擴容操作
- 4.6. 代碼實現
- 五、開散列(哈希桶)
- 5.1. 開散列擴容
- 5.2. 代碼實現
- 六、閉散列和開散列的比較
一、哈希表
哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,通過這種方式,可以不經過任何比較,一次直接從表中得到要搜索的元素,即查找的時間復雜度為 O(1)。
這個映射函數叫做哈希(散列)函數,存放記錄的數組叫做哈希(散列)表。
最典型的例子是計數排序,將要排序的數組每個元素和新開辟的數組的下標進行映射。
向哈希表中插入和搜索元素的過程如下:
先用哈希函數將被要插入或查找的鍵值轉化為數組的一個索引。
- 插入元素: 根據索引位置,將元素存放到此位置。
- 搜索元素: 根據索引,找到存儲在該索引位置的元素。
在理想情況下,不同的鍵都能轉化為不同的索引值。當然,這只是理想情況,所以我們需要面對兩個或者多個鍵都會散列到相同的索引值的情況。這就是哈希沖突的問題。
二、哈希函數
2.1. 直接定址法(常用)
取關鍵字的某個線性函數作為散列地址:hash(key)=A*key + B
優點:簡單、均勻
缺點:實現需要知道關鍵字的分布情況,并且只適合查找比較小,且連續分布的情況
適用場景:查找字符串中,第一次出現的單詞:構建一個數組 hash[ch-‘a’] 即為對應的地址
不適用場景:給一批數據, 1 5 8 100000 像這數據跨度大,數據元素不連續,很容易造成空間浪費
2.2. 除留余數法(常用)
設散列表中允許的地址數為m,通常是取一個不大于m,但是最接近或者等于m的質數num,作為除數,按照哈希函數進行計算hash(key)= key%num, 將關鍵碼轉換成哈希地址
優點:使用場景廣泛,不受限制。
缺點:存在哈希沖突,需要解決哈希沖突,哈希沖突越多,效率下降越厲害。
2.3. 幾種不常用的方法
hash(key)=key*key 然后取函數返回值的中間的幾位,作為哈希地址
比如 25^2 = 625 取中間的一位 2 作為哈希地址
比較適合不知道關鍵字的分布,而位數又不是很大的情況
將關鍵字從左到右分割成位數相等的幾部分(最后一部分可以短些),然后將這幾部分疊加求和,并且按照散列表長度,取最后幾位作為散列地址
適用于不知道關鍵字分布,關鍵字位數比較多的情況
選取一個隨機函數,取關鍵字的隨機函數值,作為它的哈希地址,hash(key) = random(key),random為隨機函數
通常用于關鍵字長度不等的情況
通過實現分析關鍵字,來獲取哈希地址
比如用每個人的手機號碼充當關鍵字,如果采用前三位作為哈希地址,那么沖突的概率是非常大的。如果采用的是中間3位那么沖突的概率要小很多
常用于處理關鍵字位數比較大的情況,且事前知道關鍵字的分布和關鍵字的若干位的分布情況
三、哈希沖突
不同關鍵字通過相同哈希函數計算出相同的哈希映射地址,這種現象稱為哈希沖突或哈希碰撞。
解決哈希沖突通常有兩種方法:閉散列(開放地址發)和開散列(鏈地址法)。
四、閉散列
閉散列也叫做開放地址法,當發生哈希沖突的時候,如果哈希表未被填滿,說明在哈希表中必然還有空位置,那么可以把key存放到沖突位置的"下一個"空位置中去,尋找下一個空位置的方法有線性探測法和二次探測法。
4.1. 線性探測
從發生沖突的位置開始,依次向后探測,直到尋找到下一個位置為止
優點:實現非常簡單
缺點:一旦發生哈希沖突,所有的沖突連在一起,容易產生數據"堆積",即不同關鍵碼占據了可利用的空位置,使得尋找某關鍵碼的位置需要進行多次比較,導致搜索效率降低。
4.2. 負載因子
隨著哈希表中數據的增多,產生哈希沖突的可能性也隨著增加。
我們將數據插入到有限的空間,那么空間中的元素越多,插入元素時產生沖突的概率也就越大,沖突多次后插入哈希表的元素,在查找時的效率必然也會降低。介于此,哈希表當中引入了負載因子(載荷因子):
散列表的載荷因子定義為 α = 填入表中的元素 / 散列表的長度
α是散列表裝滿程度的標志因子,α越大表明裝入表中的元素越多,產生沖突的可能性也就越大,反之填入表中的元素越少,沖突可能性越低,空間利用率也就越低
閉散列:一般將載荷因子控制在 0.7-0.8以下,超過0.8查表時候的緩存不中率會按照指數曲線上升(哈希可能性沖突越大),因此一般hash庫中,都將α設置在0.8以下。 閉散列,千萬不能為滿,否則在插入的時候會陷入死循環
開散列/哈希桶:一般將載荷因子控制在1。超過1,那么鏈表就掛得越長,效率也就越低
4.3. 二次探測
線性探測的缺陷是產生哈希沖突,容易導致沖突的數據堆積在一起,這是因為線性探測是逐個的找下一個空位置.
二次探測為了緩解這種問題(不是解決),對下一個空位置的查找進行了改進(跳躍式查找):
POS = (H+i2)%m
其中:i=1、2、3…
H是發生哈希沖突的位置
m是哈希表的大小
4.4. 插入和刪除操作
插入操作比較簡單:通過哈希函數插入元素在哈希表中的位置,如果發生了哈希沖突,則使用線性探測或二次探測尋找下一個空位置插入元素。
但是刪除操作比較麻煩,采用閉散列處理哈希沖突時,不能隨便刪除哈希表中已有的元素,如果直接刪除元素,會影響其他元素的搜索(比如原來下標為40的元素因為前面刪除了一個元素下標變成了39)。
因此線性探測采用標記的偽刪除法來刪除下一個元素。
4.5. 擴容操作
為了減少沖突,哈希表的大小最好是素數。為了能夠獲取每次增容后的大小,將需要用到的素數序列提前用一個數組存儲起來,當我們需要增容時就從該數組當中進行獲取。
4.6. 代碼實現
namespace CloseHash//閉散列 {enum State{EMPTY,EXIT,DELETE};template<class K,class V>struct HashData{pair<K, V> _kv;State _state=EMPTY;//節點的狀態默認為空};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 value = 0;for (auto ch : s){value += ch;value *= 131;}return value;}};template<class K, class V,class HashFunc=Hash<K>>struct HashTable{public:size_t GetNextPrime(size_t prime){static const int PRIMECOUNT = 28;//給成靜態,不用重復生成static 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, 429496729ul};size_t i = 0;for (; i < PRIMECOUNT; ++i) {if (primeList[i] > prime)return primeList[i];}return primeList[i];}bool Insert(const pair<K, V>& kv){HashData<K, V>* ret = Find(kv.first);if (ret) //哈希表中已經存在該鍵值的鍵值對(不允許數據冗余){return false; //插入失敗}//進行擴容檢測if (_n == 0 || (_n / _table.size() * 10 > 7))//當前個數為0或者載荷因子超過了,則進行擴容{//size_t newsize = _size == 0 ? 10 : 2 * _tables.size();//初始化給10,后續擴容兩倍//選取素數size_t newsize = GetNextPrime(_table.size());//擴容之后,需要重新計算元素的位置HashTable<K, V, HashFunc> newHT;newHT._table.resize(newsize);for (auto& e : _table){if (e._state == EXIT)newHT.Insert(e._kv);}_table.swap(newHT._table);//進行交換}HashFunc hf;size_t start= hf(kv.first) % _table.size();size_t index = start;//探測后面的位置,線性探測或二次探測size_t i = 1;while (_table[index]._state == EXIT){index =start+i;index %= _table.size();++i;}_table[index]._kv = kv;_table[index]._state = EXIT;++_n;return true;}HashData<K,V>* Find(const K& key){if (_table.size() == 0) return nullptr;HashFunc hf;size_t start = hf(key) % _table.size();size_t index = start;size_t i = 1;while (_table[index]._state != EMPTY){if (_table[index]._state== EXIT&&_table[index]._kv.first == key){return &_table[index];}index = start + i;index %= _table.size();++i;}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret == nullptr){return false;}else{ret->_state = DELETE;return true;}}private:vector<HashData<K,V>> _table;size_t _n = 0;}; }五、開散列(哈希桶)
開散列又名哈希桶/開鏈法,首先對關鍵碼集合采用散列函數計算散列地址,具有相同地址的關鍵碼歸于同一子集合,每一個子集合稱為一個桶,各個桶中的元素通過一個單鏈表串聯起來,各個鏈表的頭節點存儲在哈希表中
5.1. 開散列擴容
與閉散列不同,開散列需要負載因子達到1的時候才進行擴容。
因為在理想的情況下每個桶下面只有一個節點。哈希桶的載荷因子控制在1,當大于1的時候就進行擴容,這樣平均下來,每個桶下面只有一個節點;
與閉散列進行比較: 看起來哈希桶之中存儲節點的指針開銷比較大,其實不然。閉散列的負載因子需要保證小于0.7,來確保有足夠的空間降低哈希沖突的概率,而表項的空間消耗遠遠高于指針所占的空間效率,因此哈希桶更能節省空間。
5.2. 代碼實現
namespace OpenHash//開散列 {template<class K,class V>struct HashNode{HashNode<K, V>*_next;pair<K, V>_kv;HashNode(const pair<K, V>& kv):_next(nullptr),_kv(kv){} };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 value = 0;for (auto ch : s){value += ch;value *= 131;}return value;}};template<class K, class V, class HashFunc = Hash<K>>struct HashTable{public:typedef HashNode<K, V> Node;Node* Find(const K& key){HashFunc hf;if (_table.size() == 0) return nullptr;size_t index = hf(key) % _table.size();Node* cur = _table[index];//在桶中進行查找while (cur){if (cur->_kv.first == key){return cur;}else{cur = cur->_next;}}return nullptr;}bool Insert(const pair<K, V>& kv){HashFunc hf;if (Find(kv.first)) return false;//列表里已經存在kv//負載因子到1時進行增容if (_n == _table.size()){vector<Node*>newtable;size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;newtable.resize(newSize);//遍歷舊表中的節點,重新計算映射位置,掛到新表中for (size_t i = 0; i < _table.size(); ++i){if (_table[i]){Node* cur = _table[i];while (cur){//記錄原來cur后面的節點Node* next = cur->_next;size_t index = hf(cur->_kv.first) % newtable.size();//頭插cur->_next = _table[index];_table[index] = cur;cur = next;}_table[i] = nullptr;}}_table.swap(newtable);}size_t index = hf(kv.first) % _table.size();Node* newnode = new Node(kv);newnode->_next = _table[index];_table[index] = newnode;++_n;}bool Erase(const K& key){HashFunc hf;size_t index = hf(key) % _table.size();Node* prev = nullptr;Node* cur = _table[index];//在桶中找到對應的節點進行刪除while (cur){if (cur->_kv.first == key){//頭結點的情況if (_table[index] == cur){_table[index] = cur->_next;}else{prev->_next = cur->_next;}--_n;delete cur;return true;}prev = cur;cur = cur->_next;}//要刪的節點不存在return false;}private:vector<Node*> _table;size_t _n=0;//有效數據的個數}; }六、閉散列和開散列的比較
資料參考:
哈希表、哈希桶的實現
哈希表底層探索
數據結構之(一)Hash(散列)
哈希之開散列,閉散列
總結
以上是生活随笔為你收集整理的14 哈希表和哈希桶的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: html实现海浪
- 下一篇: 2022年度大数据服务公司TOP50