【C++】哈希表的实现
目录
1. 哈希表基本概念
1.1 负载因子
1.2 哈希冲突
2. 哈希函数
3. 开放地址法
4. 链地址法
5. 开放地址法代码实现
5.1 表结构
5.2 扩容
5.3 key不能取模
5.4 完整代码
6. 链地址法代码实现
6.1 扩容
6.2 完整代码
1. 哈希表基本概念
哈希表(Hash Table)是计算机科学中最重要且最常用的数据结构之一,它提供了时间复杂度O(1)的插入、删除和查找操作。
哈希表是一种通过"键"(key)直接访问内存存储位置的数据结构。它使用哈希函数将键映射到表中一个位置来访问记录,从而加快查找速度。
1.1 负载因子
负载因子(Load Factor)是衡量哈希表空间利用程度的重要指标,
定义为:负载因子 = 哈希表中元素数量 / 哈希表容量
当负载因子超过某个阈值(通常为0.7-0.8,下面代码实现用的是0.7)时,哈希表的性能会显著下降,此时需要进行扩容操作。
1.2 哈希冲突
哈希冲突是指不同的键通过哈希函数计算后得到相同的哈希值的情况。解决哈希冲突是哈希表设计的核心问题,主要有两种方法:开放地址法和链地址法。
2. 哈希函数
一个好的哈希函数应具备以下特点:
-
计算速度快
-
能够均匀分布键值(减少冲突)
-
确定性(相同键总是产生相同哈希值)
常见的哈希函数构造方法包括:
-
直接寻址法
-
乘法散列法
-
全域散列法
-
除留余数法(最常用,下面实现的也是这个方法)
3. 开放地址法
开放地址法处理冲突的方式是:当发生冲突时,按照某种探测序列在哈希表中寻找下一个空槽位。
常见的探测方法:
-
线性探测:顺序查找下一个空位
-
二次探测:使用二次方程计算下一个位置
-
双重哈希:使用第二个哈希函数计算步长
4. 链地址法
链地址法(又称拉链法)将哈希到同一位置的元素存储在同一个链表中。这种方法简单直接,能够有效处理冲突,但需要额外的空间存储指针。
5. 开放地址法代码实现
5.1 表结构
enum State
{EMPTY, //空EXIST, //非空DELETE //删除过节点
};
template<class K, class V>
struct HashNode
{pair<K, V> _kv;State _state = EMPTY;//初始默认为空
};template<class K,class V>
class HashTable
{
public:private:vector<pair<K,V>> _tables;size_t _size = 0;
};
5.2 扩容
inline unsigned long __stl_next_prime(unsigned long n)
{// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] ={53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}
5.3 key不能取模
当key是string/Date等类型时,key不能取模,那么我们需要给哈希表增加⼀个仿函数,这个仿函数支持把key转换成⼀个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数就用默认参数即可,如果这个key不能转换为整形,我们就需要自己实现⼀个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每个值都参与到计算中,让不同的key转换出的整形值不同。因为string做哈希表的key比较常见,所以我们可以这里把string特化⼀下。
这里介绍一个小知识点:
//解决有些key不能取模的问题
template<class K>
struct HashKey
{//能转化成size_t的,就直接转换size_t operator()(const K& k){return size_t(k);}
};//特化
template<>
struct HashKey<string>
{//要将字符串转换成整型,我们可能会想到把所有字符ASCII值相加//但这样可能字符ads和sad就是一样的了//所以这里我们使用BKDR哈希的思路size_t operator()(const string& s){size_t ret = 0;for (auto e : s){ret *= 31;ret += e;}return ret;}
};
5.4 完整代码
//解决有些key不能取模的问题
template<class K>
struct HashKey
{//能转化成size_t的,就直接转换size_t operator()(const K& k){return size_t(k);}
};//特化
template<>
struct HashKey<string>
{//要将字符串转换成整型,我们可能会想到把所有字符ASCII值相加//但这样可能字符ads和sad就是一样的了//所以这里我们使用BKDR哈希的思路size_t operator()(const string& s){size_t ret = 0;for (auto e : s){ret *= 31;ret += e;}return ret;}};enum State
{EMPTY, //空EXIST, //非空DELETE //删除过节点
};
template<class K, class V>
struct HashNode
{pair<K, V> _kv;State _state;
};template<class K,class V,class Hash = HashKey<K>>
class HashTable
{
public:HashTable(){_tables.resize(__stl_next_prime(0));}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;//负载因子大于0.7——扩容if ((double)_size / (double)_tables.size() > 0.7){HashTable<K, V> newHT;newHT._tables.resize(__stl_next_prime((unsigned long)_tables.size() + 1));for (int i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}Hash h;size_t hash0 = h(kv.first) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state == EXIST){//线性探测hashi = (hashi + i) % _tables.size();i++;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_size++;return true;}HashNode<K, V>* Find(const K& key){/*for (int i = 0; i < _tables.size(); i++){if (_tables[i]._kv.first == k){return &_tables[i];}}return nullptr;*/Hash h;size_t hash0 = h(key) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state == EXIST){if (_tables[hashi]._kv.first == key)return &_tables[hashi];hashi = (hashi + i) % _tables.size();i++;}return nullptr;}bool Erase(const K& key){if (Find(key)){HashNode<K, V>* ret = Find(key);ret->_state = DELETE;--_size;return true;}return false;}
private:inline unsigned long __stl_next_prime(unsigned long n){// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] ={53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}vector<HashNode<K,V>> _tables;size_t _size = 0;
};
6. 链地址法代码实现
6.1 扩容
扩容方法和开放地址法相同
6.2 完整代码
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 = HashKey<K>>class HashTable {typedef HashNode<K, V> Node;public:HashTable(){_tables.resize(__stl_next_prime(0),nullptr);}~HashTable(){for (int i = 0; i < _tables.size(); i++){while (_tables[i]){Node* del = _tables[i];_tables[i] = _tables[i]->_next;delete del;del = nullptr;}}_size = 0;}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;Hash h;//负载因子大于0.7——扩容if ((double)_size / (double)_tables.size() > 0.7){//可以使用和开放地址法类似的方法,但这样的方法消耗的空间很大,还要释放很麻烦/*HashTable<K, V> newHT;newHT._tables.resize(__stl_next_prime((unsigned long)_tables.size() + 1),nullptr);for (int i = 0; i < _tables.size(); i++){if(_tables[i]){newHT.Insert(_tables[i]->_kv);}}_tables.swap(newHT._tables);*///我们可以直接使用原节点vector<Node*> newHT;newHT.resize(__stl_next_prime((unsigned long)_tables.size() + 1), nullptr);for (int i = 0; i < _tables.size(); i++){while(_tables[i]){size_t hash0 = h(_tables[i]->_kv.first) % newHT.size();Node* newnode = _tables[i];_tables[i] = newnode->_next;newnode->_next = newHT[hash0];newHT[hash0] = newnode;}}newHT.swap(_tables);}Node* newnode = new Node(kv);size_t hash0 = h(kv.first) % _tables.size();//头插newnode->_next = _tables[hash0];_tables[hash0] = newnode;_size++;return true;}Node* Find(const K& key){Hash h;size_t hash0 = h(key) % _tables.size();for (int i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}}return nullptr;}bool Erase(const K& key){Hash h;size_t hash0 = h(key) % _tables.size();for (int i = 0; i < _tables.size(); i++){Node* cur = _tables[i];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr)_tables[i] = cur->_next;else{prev->_next = cur->_next;}delete cur;cur = nullptr;_size--;return true;}prev = cur;cur = cur->_next;}}return false;}private:inline unsigned long __stl_next_prime(unsigned long n){// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] ={53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}vector<Node*> _tables;size_t _size = 0;};