在c++中,hanshtable是一个重要的底层数据结构,无序容器(unordered_map \ unordered_set)底层都是hashtable实现的。
一、什么是 Hash Table,为什么它如此重要?
在正式开始实现之前,咱们得先搞清楚 Hash Table 到底是个啥。想象一下,你有一个超级大的图书馆,里面摆满了各种各样的书籍。如果每次找一本书都要从第一排书架开始一本一本地找,那得耗费相当长的时间。这时候,Hash Table 就像是图书馆的智能索引系统,它能让你快速地找到你想要的书。
Hash Table 是一种根据键(key)直接访问内存存储位置的数据结构。它通过一个哈希函数(hash function),将键映射到一个固定大小的数组中的某个位置,这个位置就像是图书馆里书的具体书架编号。这样一来,当你要查找某个键对应的值时,就可以直接通过哈希函数计算出位置,然后快速找到它,而不需要像在链表中那样逐个元素地查找。
Hash Table 的重要性不言而喻。在很多实际应用场景中,比如数据库的索引、缓存系统、编译器的符号表等等,都广泛地使用了 Hash Table。因为它能提供非常高效的查找、插入和删除操作,平均时间复杂度可以达到 O (1),这在处理大量数据时能大大提高程序的性能。
二、哈希表工作原理剖析
哈希表的工作流程主要涉及哈希函数、哈希冲突处理以及存储结构三个关键环节。
(一)哈希函数
哈希函数是哈希表实现的核心组件,它负责将任意类型的键转换为一个固定范围的整数值,该整数值用于确定键值对在哈希表中的存储位置。一个优秀的哈希函数需要满足以下特性:
- 确定性:相同的键经过哈希函数计算,必须得到相同的哈希值。
- 均匀性:能够将键尽可能均匀地分布到哈希表的各个位置,减少哈希冲突的发生概率。
- 高效性:计算过程应尽量简洁快速,避免因复杂计算影响哈希表整体性能。
常见的哈希函数实现方式有直接定址法、数字分析法、折叠法等。以简单的整数键为例,可使用取模运算作为哈希函数:index = key % table_size
,其中table_size
为哈希表的大小,通过此运算将键映射到哈希表的有效索引范围内。
(二)哈希冲突处理
尽管哈希函数追求均匀分布,但由于键的数量可能远超哈希表的存储容量,哈希冲突(即不同键映射到同一存储位置)难以完全避免。目前,解决哈希冲突的方法主要有两类:
- 开放地址法:当发生冲突时,按照某种规则在哈希表中寻找下一个可用位置。常见的寻找策略包括线性探测(每次冲突后顺序查找下一个位置)、二次探测(通过二次函数计算下一个位置)等。
- 链地址法:哈希表的每个存储位置不再存储单个元素,而是维护一个链表(或其他数据结构)。当冲突发生时,将冲突元素插入到对应位置的链表中。这种方式在处理大量冲突时具有较好的扩展性。
(三)存储结构
哈希表的存储结构通常基于数组实现,数组的每个元素作为哈希表的一个 “桶”(bucket)。在开放地址法中,“桶” 直接存储键值对;在链地址法中,“桶” 存储指向链表头节点的指针,链表中存储实际的键值对。通过哈希函数计算得到的索引,可快速定位到对应的 “桶”,进而进行数据操作。
三、哈希表实现步骤规划
实现哈希表需要遵循严谨的步骤,确保功能的完整性与性能的可靠性。
- 定义哈希表结构:确定哈希表的基本组成部分,包括存储数组、哈希表大小、元素类型等,并设计合适的数据结构来承载这些信息。
- 实现哈希函数:根据实际应用场景,选择或设计合适的哈希函数,并进行代码实现与测试。
- 冲突处理机制实现:依据需求选择开放地址法或链地址法,实现相应的冲突处理逻辑,保证哈希表在冲突情况下仍能正常工作。
- 操作接口实现:完成插入、查找、删除等核心操作接口的实现,确保各操作逻辑正确且高效。
- 测试:编写全面的测试用例,验证哈希表功能的正确性。
四、代码实现
使用std::vector构造哈希表以提升其查找效率,std::list构建列表,哈希表的增删改查操作流程都是先计算键值对应索引,通过索引取出链表,再对链表进行搜索,具体看代码注释。
#include <algorithm>
#include <iostream>
#include <list>
#include <vector>
#include <string>
template<typename Key, typename Value, typename Hash = std::hash<Key>>
class MyHashTable
{
private:
class Node
{
public:
Key key;
Value value;
//显式构造
explicit Node(const Key &key) : key(key), value() {}
Node(const Key &key, const Value &value) : key(key), value(value) {}
//重载比较操作,按照key比较,为后边对list<Node>进行std::find时提供标准
bool operator==(const Node &other) const { return key == other.key; }
bool operator!=(const Node &other) const { return key != other.key; }
bool operator<(const Node &other) const { return key < other.key; }
bool operator>(const Node &other) const { return key > other.key; }
bool operator==(const Key &key_) const { return key == key_; }
};
//用vector存储list,此时vector是哈希表,内部的list是应对哈希冲突采用的链表,每个被映射为同一个哈希值的键值对通过链表连接
std::vector<std::list<Node>> lists_;
Hash hashfunc_;
size_t vecsize_;//哈希表大小(索引个数)
size_t elementsize_;//元素总数
float load_factor_ = 0.75;//负载因子,元素总数大于等于哈希表大小*负载因子时对哈希表进行扩容并重哈希
size_t hash(const Key &key) const {return hashfunc_(key) % vecsize_;}
//重哈希函数,遍历原始哈希表内部每一个元素,计算每一个元素新的哈希表索引并加入到新哈希表内
void rehash(size_t size)
{
std::vector<std::list<Node>> newlists(size);//注意申请新空间是一定要初始化大小,否则插入时越界
for(std::list<Node> &list : lists_)
{
for(Node &node : list)
{
size_t newidx = hashfunc_(node.key) % size;
newlists[newidx].push_back(node);
}
}
lists_ = std::move(newlists);
vecsize_ = size;
}
public:
MyHashTable(size_t size = 0, const Hash &hashFunc = Hash()): lists_(size), hashfunc_(hashFunc), vecsize_(size), elementsize_(0) {}
~MyHashTable()
{
clear();
}
void insert(const Key &key, const Value &value)
{
//向vector一样,再插入时先判断是否需要扩容
if(elementsize_ + 1 > load_factor_ * vecsize_)
{
if(vecsize_ == 0) vecsize_ = 1;
rehash(vecsize_ * 2);
}
//计算新元素哈希索引
size_t idx = hash(key);
//找到哈希索引对应链表并查找新元素键值在列表中是否存在,若存在丢弃新元素,只有在不存在时新元素才会插入哈希表
std::list<Node> &list = lists_[idx];
if(std::find(list.begin(),list.end(), key) == list.end())
{
list.push_back(Node(key, value));
elementsize_ ++ ;
}
}
void insert(const Key &key) { insert(key, Value{}); }
//删除元素
void erase(const Key &key)
{
size_t idx = hash(key);
std::list<Node> &list = lists_[idx];
auto it = std::find(list.begin(),list.end(), key);
if(it == list.end())
{
return;
}
else
{
list.erase(it);
elementsize_ --;
}
}
Value* find(const Key &key)
{
size_t idx = hash(key);
std::list<Node> &list = lists_[idx];
auto it = std::find(list.begin(), list.end(), key);
if(it == list.end())
{
return nullptr;
}
else
{
return &it->value;
}
}
size_t size() {return elementsize_;}
void clear()
{
this->lists_.clear();
this->vecsize_ = 0;
this->elementsize_ = 0;
}
};
int main()
{
MyHashTable<std::string,int> myhash(0);
myhash.insert("1",1);
myhash.insert("2",2);
myhash.insert("3",3);
myhash.insert("4",4);
myhash.insert("5",5);
myhash.insert("6",6);
myhash.insert("7",7);
std::cout << *(myhash.find("2")) << std::endl;
std::cout << *(myhash.find("3")) << std::endl;
std::cout << *(myhash.find("4")) << std::endl;
return 0;
}