从零开始手写STL库–HashTable的实现
Gihub链接:miniSTL
文章目录
- 从零开始手写STL库–HashTable的实现
- HashTable是什么
- HashTable需要包含什么函数
- 基础成员部分
- 基础函数部分
- 可用函数部分
- 其他函数
- 总结
HashTable是什么
HashTable在STL中直接出现的情况并不多,但却是很重要的底层结构。
STL库中的unordered_set和unordered_map均由它构成。
实际上也就是大家所熟悉的哈希表,是通过哈希函数将键映射到索引的一种数据结构,一般是不允许重复键值存在的,
所以一般都用来做搜索工作。
HashTable需要包含什么函数
通常来讲HashTable需要解决哈希函数映射
,冲突解决
,迭代器
等等功能。本文提供一种简化的HashTable实现。
基础成员部分
首先需要知道的是,STL库中HashTable这一数据结构的实现方式为桶数组+链表
当输入一个键值时,通过哈希函数计算它的索引,寻找到对应的桶,再插入该桶后面的链表中,这就是一次插入过程
所以首先需要构建基础元素,HashNode,也就是上图中每一行组成的基础元素,多个基础元素拼起来也就成了一个HashTable
template <typename Key, typename Value, typename Hash = std::hash<Key>>
class HashTable {
class HashNode {
public:
Key key;
Value value;
explicit HashNode(const Key &key) : key(key), value() {}
// 从Key和Value构造节点
HashNode(const Key &key, const Value &value) : key(key), value(value) {}
// 比较算符重载,只按照key进行比较
bool operator==(const HashNode &other) const { return key == other.key; }
bool operator!=(const HashNode &other) const { return key != other.key; }
bool operator<(const HashNode &other) const { return key < other.key; }
bool operator>(const HashNode &other) const { return key > other.key; }
bool operator==(const Key &key_) const { return key == key_; }
void print() const {
std::cout << key << " "<< value << " ";
}
};
};
当有了基础元素,就可以将这些单独元素统一起来成为一个HashTable了
private:
using Bucket = std::list<HashNode>; // 定义桶的类型为存储键的链表
std::vector<Bucket> buckets; // 存储所有桶的动态数组
Hash hashFunction; // 哈希函数对象
size_t tableSize; // 哈希表的大小,即桶的数量
size_t numElements; // 哈希表中元素的数量
需要注意的是,哈希表需要动态地调整桶数组的大小来保持较好的性能,不然一个桶后面跟了太多的数字,会降低搜索效率
所以定义一个负载因子,当链表数量与桶数量之比大于这个因子,就进行rehash工作
float maxLoadFactor = 0.75; // 默认的最大负载因子
基础函数部分
当结构完整了,就可以开始定义函数了。
首先就是哈希映射函数
size_t hash(const Key &key) const { return hashFunction(key) % tableSize; }
用来接收键值(Key),并返回一个索引值(Index),来储存该键值
接着就是上文提到的rehash函数,与vector、deque的resize函数相同,这个函数也需要放在private中,不能随意让外界调用
void rehash(size_t newSize) {
std::vector<Bucket> newBuckets(newSize); // 创建新的桶数组
for (Bucket &bucket : buckets) { // 遍历旧桶
for (HashNode &hashNode : bucket) { // 遍历桶中的每个键
size_t newIndex =
hashFunction(hashNode.key) % newSize; // 为键计算新的索引
newBuckets[newIndex].push_back(hashNode); // 将键添加到新桶中
}
}
buckets = std::move(newBuckets); // 使用move来更新,从而省去复制操作,优化性能
tableSize = newSize; // 更新哈希表大小
}
接下来就是可以为外界使用的函数了
可用函数部分
1、构造函数:
HashTable(size_t size = 10, const Hash &hashFunc = Hash())
: buckets(size), hashFunction(hashFunc), tableSize(size), numElements(0) {
}
这里的hash函数如果用户不指定,那就用STL库中自带的hash函数来初始化
2、插入函数
通过该函数,将键插入到哈希表中
void insert(const Key &key, const Value &value) {
if ((numElements + 1) > maxLoadFactor * tableSize) { // 检查是否需要重哈希
if (tableSize == 0) tableSize = 1;// 防止之前进行了 clear 导致的 tableSize = 0 的情况
rehash(tableSize * 2); // 重哈希,桶数量翻倍
}
size_t index = hash(key); // 计算键的索引
std::list<HashNode> &bucket = buckets[index]; // 获取对应的桶
// 如果键不在桶中,则添加到桶中
if (std::find(bucket.begin(), bucket.end(), key) == bucket.end()) {
bucket.push_back(HashNode(key, value));
++numElements; // 增加元素数量
}
}
void insertKey(const Key &key) { insert(key, Value{}); } // 只插入键值,没有value的情况
3、移除函数
通过该函数,将哈希表中存在的值删除,当然也需要区分存在和不存在的情况,不存在的话要注意不能内存溢出
void erase(const Key &key) {
size_t index = hash(key); // 计算键的索引
auto &bucket = buckets[index]; // 获取对应的桶
auto it = std::find(bucket.begin(), bucket.end(), key); // 查找键
if (it != bucket.end()) { // 如果找到键
bucket.erase(it); // 从桶中移除键
numElements--; // 减少元素数量
}
}
4、查找函数
通过给定的键来查找该元素是否存在,按照STL库的函数,本函数需要返回一个指针,指向找到的值,否则返回end()
Value *find(const Key &key) {
size_t index = hash(key); // 计算键的索引
auto &bucket = buckets[index]; // 获取对应的桶
// 返回键是否在桶中
auto ans = std::find(bucket.begin(), bucket.end(), key);
if (ans != bucket.end()) {
return &ans->value;
};
return nullptr;
}
其他函数
// 获取哈希表中元素的数量
size_t size() const { return numElements; }
// 打印哈希表中的所有元素
void print() const {
for (size_t i = 0; i < buckets.size(); ++i) {
for (const HashNode &element : buckets[i]) {
element.print();
}
}
std::cout << std::endl;
}
// 清空哈希表
void clear() {
this->buckets.clear();
this->numElements = 0;
this->tableSize = 0;
}
总结
哈希表需要注意底层实现是桶数组+链表,并且需要知道rehash的用处,什么时候要rehash。
另外哈希表解决冲突不止本文这种方式,桶+链表虽然实现简单,但是极端情况下查找时间会到达O(n),而且占用内存较多
还有些方法比如
线性探测:当前桶有数字了,就顺序向下,一直到空桶;
二次探测:线性探测中把线性函数变成平方项;
双重哈希:两个哈希表,一个计算出来冲突了,就用另一个计算
知道即可,一般还是用桶+链表