目錄
一、前言
1.1 閉散列
1.2 開散列
1.3 string 與 非 string?
二、哈希桶的構成
2.1 哈希桶的節點
2.2 哈希桶類
三、 Insert 函數
3.1 無需擴容時
3.2 擴容
復用 Insert:
逐個插入:
優缺點比對:
第一種寫法優點
第一種寫法缺點
第二種寫法優點
第二種寫法缺點
3.3 完整代碼
四、 Erase 函數
4.1 析構函數
4.2 Erase 函數
五、 Find 函數
六、完整代碼?
一、前言
上一篇文章講的哈希表,屬于閉散列。可以解決哈希沖突有兩種方式:閉散列和開散列。現在要學習的哈希桶就是開散列。
1.1 閉散列
閉散列:也叫開放定址法,當發生哈希沖突時,如果哈希表未被裝滿,說明在哈希表中必然還有
空位置,那么可以把key存放到沖突位置中的“下一個” 空位置中去。
1.2 開散列
開散列法又叫鏈地址法(開鏈法),首先對關鍵碼集合用散列函數計算散列地址,具有相同地
址的關鍵碼歸于同一子集合,每一個子集合稱為一個桶,各個桶中的元素通過一個單鏈表鏈
接起來,各鏈表的頭結點存儲在哈希表中。
下面則是即將學習的哈希桶的簡易圖:
類似于一個數組中存了若干個鏈表頭,每個頭所代表的鏈表成為一個桶。
1.3 string 與 非 string?
在上一篇博客的最后:哈希表HashTable-CSDN博客
探討過當 Key 為負數、浮點數、字符串...時,類函數邏輯中有關 Key 取模的問題,當 Key 為負數、浮點數、字符、字符串時,顯然這幾個內置類型無法完成取模的操作,這時就用到了仿函數,這里不再多說,直接來看仿函數的代碼,下面會直接使用仿函數來完成 HashBucket
template<class T>
class HashFunc<>
{size_t operator()(const T& Key){return (size_t)Key;}
}
template<>
class HashFunc<string>
{size_t operator()(const string& Key){size_t hash = 0;for (auto ch : Key){hash *= 131;hash += ch;}return hash;}
}
二、哈希桶的構成
2.1 哈希桶的節點
由上圖就可以看出來,每個結點必要存一個 pair 和一個指向下一個節點的指針 _next。
template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode* _next;
}
2.2 哈希桶類
哈希桶類的構成和哈希表類似,都是一個由一個 vector 存放每個節點,但是這里與 HashTable 不同的是需要存放的是節點的指針。還有一個 _n 代表有效數據的個數:
template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{typedef HashNode Node;
private:vector<Node*> _bucket;size_t _n;
};
三、 Insert 函數
3.1 無需擴容時
下面要介紹的是不需要擴容時的插入邏輯:
此時只需要使用 Key 模數組的大小來計算出該節點需要連接在 vector 上的位置,然后使用 new 得到儲存 kv 的新節點,當 new 一個新節點時,節點的構造函數必不可少,下面先來看一下單個節點的構造函數以及類的構造函數:
template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode* _next;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{ typedef HashNode<K, V> Node;HashBucket(){_bucket.resize(10, nullptr);_n = 0;}
private:vector<Node*> _bucket;size_t _n;
}
此時需要思考的是,既然 vector 每個節點都要存放一個鏈表,那么鏈表頭插還是尾插的效率更高呢?
顯然是頭插,所以這個新結點就需要以類似頭插的方式添加到 vector 的這個位置上,
bool Insert(const pair<K, V>& kv)
{Hash hs;size_t index = hs(kv.first) % _bucket.size(); Node* newnode = new Node(kv);newnode->_next = _bucket[index];_buckte[index] = newnode;++_n;return true;
}
3.2 擴容
這里的載荷因子可以直接設為1,至于載荷因子是什么,可以查看上一篇博客哈希表HashTable-CSDN博客,在擴容中的何時擴容標題下,介紹了載荷因子的概念。
在擴容中,既可以使用 HashTable 中類似的寫法直接復用 Insert ,也可以直接挨個讓節點插入,下面先介紹每種方式,再進行優缺點的處理:
復用 Insert:
bool Insert(const pair<K, V>& kv)
{Hash hs;if (_n == _bucket.size()){HashBucket newHB = new HashBucket;newHB._bucket.resize(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while(cur){Node* next = cur->_next; // 保存下一個節點指針newHB.Insert(cur->_kv); // 插入當前節點的鍵值對到新哈希表cur = next; // 移動到下一個節點}_bucket[i] = nullptr;}_bucket.swap(newHB);}
}
逐個插入:
bool Insert(const pair<K, V>& kv)
{Hash hs;if (_n == _bucket.size()){vector<Node*> newBucket(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;size_t index = hs(cur->_kv.first) % newBucket.size();cur->_next = newBucket[index];newBucket[index] = cur;cur = next;}_bucket[i] = nullptr;}_bucket.swap(newBucket);}
}
優缺點比對:
第一種寫法優點
- 代碼復用:通過調用
newHB.Insert(cur->_kv)
來重新插入節點,重用了Insert
方法,減少了代碼重復。 - 邏輯清晰:將舊節點遷移到新桶中,然后交換桶,邏輯分離清晰。
第一種寫法缺點
- 性能:因為每次擴容時調用
Insert
,可能會多次計算哈希值和處理沖突,性能可能稍差。
第二種寫法優點
- 性能:直接處理節點遷移,無需調用
Insert
方法,減少了函數調用和重復計算,提高了性能。 - 直接操作:直接操作指針,代碼簡潔,性能高效。
第二種寫法缺點
- 代碼重復:需要手動處理節點遷移邏輯,代碼重復。
- 復雜性:直接操作指針可能增加代碼的復雜性,增加錯誤的可能性。
3.3 完整代碼
bool Insert(const pair<K, V>& kv){if (Find(kv.first)) return false;Hash hs;if (_n == _bucket.size()){HashBucket newHB;newHB._bucket.resize(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next; // 保存下一個節點指針newHB.Insert(cur->_kv); // 插入當前節點的鍵值對到新哈希表cur = next; // 移動到下一個節點}_bucket[i] = nullptr;}_bucket.swap(newHB._bucket);}size_t index = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);newnode->_next = _bucket[index];_bucket[index] = newnode;++_n;return true;}bool Insert(const pair<K, V>& kv){if (Find(kv.first)) return false;Hash hs;if (_n == _bucket.size()){vector<Node*> newBucket(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;size_t index = hs(cur->_kv.first) % newBucket.size();cur->_next = newBucket[index];newBucket[index] = cur;cur = next;}_bucket[i] = nullptr;}_bucket.swap(newBucket);}size_t index = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);newnode->_next = _bucket[index];_bucket[index] = newnode;++_n;return true;}
四、 Erase 函數
4.1 析構函數
根據 Insert 函數中,可以得知, HashBucket 的每個節點都是 new 出來的,那刪除的時候就要使用 delete ,又因為每個節點都是自定義類型,所以要為 HashBucket 寫一個析構函數。
對類的析構就是遍歷 vector 的每個節點,再從每個節點遍歷每個鏈表,以此遍歷全部節點。
~HashBucket(){for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_bucket[i] = nullptr;}}
4.2 Erase 函數
下面介紹一下Erase函數的步驟:
-
計算哈希值:使用哈希函數
hs
計算給定鍵Key
的哈希值,并確定它在桶中的索引index
。 -
遍歷鏈表:從索引
index
開始,遍歷鏈表中的每個節點。 -
查找節點:檢查當前節點的鍵是否等于
Key
。- 如果找到匹配節點:
- 如果該節點是鏈表的第一個節點,將桶的頭指針
_bucket[index]
指向下一個節點。 - 否則,將前一個節點的
_next
指針指向當前節點的下一個節點。 - 刪除當前節點
cur
,釋放內存。 - 返回
true
,表示刪除成功。
- 如果該節點是鏈表的第一個節點,將桶的頭指針
- 如果沒有找到匹配節點,繼續遍歷鏈表,更新
prev
和cur
。
- 如果找到匹配節點:
-
返回結果:如果遍歷完整個鏈表未找到匹配節點,返回
false
,表示刪除失敗。
bool Erase(const K& Key){Hash hs;size_t index = hs(Key) % _bucket.size();Node* cur = _bucket[index];Node* prev = nullptr;while (cur){if (cur->_kv.first == Key){//刪除的是第一個節點if (prev == nullptr){_bucket[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}
五、 Find 函數
這里的 Find 函數與 HashTable 中的 Find 函數邏輯略有不同,因為這里如果發送哈希沖突,那么存儲的位置還是 vector 中取模后的位置,不需要像 HashTable 那樣進行線性探測,只需要取一個指針挨個遍歷位于 _bucket[index] 鏈表上的節點即可。
Node* Find(const K& Key){if (_bucket.empty()) return nullptr;Hash hs;size_t index = hs(Key) % _bucket.size();Node* cur = _bucket[index];while (cur){if (cur->_kv.first == Key)return cur;else cur = cur->_next;}return nullptr;}
寫完 Find 后,還可以進一步改進 Insert 函數,在插入時可以先進行查找,如果存在,那就插入失敗;如果不存在,再繼續插入。這樣避免了哈希桶中數據冗余的結果。
六、完整代碼?
#pragma once
#include <iostream>
#include <vector>
#include <string>using namespace std;template<class K>
struct HashFunc
{size_t operator()(const K& Key){return (size_t)Key;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& Key){size_t hash = 0;for (auto ch : Key){hash *= 131;hash += ch;}return hash;}
};template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode* _next;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{typedef HashNode<K, V> Node;
public:HashBucket(){_bucket.resize(10, nullptr);_n = 0;}~HashBucket(){for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_bucket[i] = nullptr;}}bool Insert(const pair<K, V>& kv){if (Find(kv.first)) return false;Hash hs;if (_n == _bucket.size()){vector<Node*> newBucket(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;size_t index = hs(cur->_kv.first) % newBucket.size();cur->_next = newBucket[index];newBucket[index] = cur;cur = next;}_bucket[i] = nullptr;}_bucket.swap(newBucket);}size_t index = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);newnode->_next = _bucket[index];_bucket[index] = newnode;++_n;return true;}bool Insert(const pair<K, V>& kv){if (Find(kv.first)) return false;Hash hs;if (_n == _bucket.size()){HashBucket newHB;newHB._bucket.resize(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next; // 保存下一個節點指針newHB.Insert(cur->_kv); // 插入當前節點的鍵值對到新哈希表cur = next; // 移動到下一個節點}_bucket[i] = nullptr;}_bucket.swap(newHB._bucket);}size_t index = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);newnode->_next = _bucket[index];_bucket[index] = newnode;++_n;return true;}bool Erase(const K& Key){Hash hs;size_t index = hs(Key) % _bucket.size();Node* cur = _bucket[index];Node* prev = nullptr;while (cur){if (cur->_kv.first == Key){//刪除的是第一個節點if (prev == nullptr){_bucket[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}Node* Find(const K& Key){if (_bucket.empty()) return nullptr;Hash hs;size_t index = hs(Key) % _bucket.size();Node* cur = _bucket[index];while (cur){if (cur->_kv.first == Key)return cur;else cur = cur->_next;}return nullptr;}
private:vector<Node*> _bucket;size_t _n;
};
void TestHB1()
{int ret[] = {5, 15, 3, 12, 13, 31};HashBucket<int, int> hb;for (auto e : ret){hb.Insert(make_pair(e, e));}cout << hb.Erase(0) << endl;cout << hb.Erase(5) << endl;
}
void TestHB2()
{HashBucket<string, int> hb;hb.Insert(make_pair("sort", 1));hb.Insert(make_pair("left", 1));hb.Insert(make_pair("insert", 1));}