哈希表的设计
1. 哈希表的基本原理
哈希表是一种通过 哈希函数 将元素的键(Key)映射到存储位置的数据结构。
-
哈希函数 的作用:通过键值计算存储位置,公式一般为
index = hash(key) % capacity
。 -
哈希冲突:不同的键可能被映射到同一个位置(例如
hash("abc") % 10 == hash("xyz") % 10
),这种情况称为 哈希冲突 或 哈希碰撞。
2. 解决哈希冲突的两种方法
(1) 闭散列(开放定址法,Open Addressing)
核心思想:如果目标位置已被占用,就按照某种探测方法在哈希表内 寻找下一个可用位置。
常用探测方法:
-
线性探测(Linear Probing)
-
冲突时,依次检查下一个位置:
index = (hash(key) + i) % capacity, i=1, 2, 3...
-
-
二次探测(平方探测)
-
冲突时,按平方步长跳跃检查:
index = (hash(key) + i²) % capacity, i=1, 2, 3...
-
-
双重哈希(Double Hashing)
-
第一个哈希函数计算基础地址,使用第二个哈希函数计算步长:
index = (hash1(key) + i * hash2(key)) % capacity, i=1,2,3...
-
开放定址法的特点:
-
所有数据都存在哈希表内,没有额外资源。
-
删除元素需特殊处理(标记为“逻辑删除”),否则会破坏探测链。
-
负载因子(Load Factor)通常需较低(如 ≤0.7),否则性能急剧下降。
(2) 开散列(链地址法,Separate Chaining)
核心思想:将哈希表的每个槽作为 链表头节点,冲突的元素直接追加到链表中。
优化:
-
当链表过长时(如 Java 8 的
HashMap
),可能转换为 红黑树,将查找时间从 O(n) 优化到 O(log n)。
链地址法的特点:
-
实现简单,允许负载因子 >1(即元素数量可以超过哈希表容量)。
-
删除操作直接(直接移除链表节点即可)。
-
内存开销较大,除了哈希表外,还有额外资源节点。
#pragma once
#include<vector>//只包头文件不展开命名空间白搭
#include<utility>//
#include<iostream>using namespace std;namespace OpenAdress//开放定址法
{enum State{EMPTY,DELETE,EXIST};template<class K,class V>struct HashData//为了能直接访问hashdata设为struct{pair<K, V> _kv;State _state=EMPTY;//使用默认构造,初始化列表使用缺省值};template<class K,class V>class HashTable{public:bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}if (_tables.size() == 0 || (double)_n / _tables.size() >= 0.7)//负载因子设为0.7{size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<HashData<K, V>> newtable;newtable.resize(newsize);for (HashData<K, V>& e : _tables)//已经扩容过了必定可以插入{if (e._state == EXIST){size_t hashi = e._kv.first % newtable.size();size_t i = 1;size_t index = hashi;while (newtable[index]._state == EXIST){index = hashi + i;index %= newtable.size();i++;}newtable[index]._kv = e._kv;newtable[index]._state = EXIST;}}_tables.swap(newtable);}size_t hashi = kv.first % _tables.size();size_t i = 1;size_t index = hashi;while (_tables[index]._state == EXIST){index = hashi + i;index %= _tables.size();i++;}_tables[index]._kv = kv;_tables[index]._state = EXIST;_n++;return true;}HashData<K,V>* Find(const K& key){if (_tables.size() == 0){return nullptr;}size_t hashi = key % _tables.size();size_t i = 1;size_t index = hashi;while (_tables[index]._state!=EMPTY)//插入是从hashi开始向后找不EXIST的位置,按插入方式找的时候如果遇到空说明没有该值,找了一圈没有找到那也是没有{if (_tables[index]._state == EXIST && _tables[index]._kv.first == key){return &_tables[index];}index =hashi+ i;index %= _tables.size();i++;if (index == hashi)//找了一圈{break;//直接返回报警不是所有路径有返回值}}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;_n--;return true;}else{return false;}}private:vector<HashData<K, V>> _tables;//全部建立在栈区上,无需释放size_t _n = 0;//有效数据个数};}//在全局作用域定义Hash仿函数,包了头文件就能直接用
template<class K>
struct HashFunc//可作为Hash的缺省值
{size_t operator()(const K& key){return key;//可隐式类型转换}
};template<>//特化 仿函数
struct HashFunc<string>
{size_t operator()(const string& str){size_t i= 0;for (auto e : str){i += e;i *= 31;}return i;}
};namespace HashBucket
{template<class T>struct HashNode{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}};template<class K, class T, class KeyOfT, class Hash>//前置声明class HashTable;template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>struct __HashIterator{typedef HashNode<T> Node;typedef __HashIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;typedef __HashIterator< K, T, T&, T*, KeyOfT, Hash> iterator;typedef HashTable<K, T, KeyOfT, Hash> HT;//不前置声明的话会报<前缺少;的错误Node* _node;HT* _ht;__HashIterator(Node* node,HT* ht):_node(node),_ht(ht){}__HashIterator(const iterator& it):_node(it._node),_ht(it._ht){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& it){return _node != it._node;}//哈希表为单向迭代器Self& operator++(){if (_node->_next){_node = _node->_next;break;}else{KeyOfT kot;Hash hash;size_t hashi = hash(kot(_node->_data)) % _ht->GetSize();hashi++;while (hashi < _ht->GetSize()){if (_ht->_tables[hashi]){_node = _ht->_tables[hashi];break;}else{hashi++;}}if (hashi == _ht->GetSize()){_node = nullptr;}}return *this;}};template<class K,class T,class KeyOfT,class Hash>//Hash仿函数将key转换为整形class HashTable{//需要访问_tablestemplate<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>friend struct __HashIterator;typedef HashNode<T> Node;public:typedef __HashIterator<K, T, T&, T*, KeyOfT, Hash> iterator;typedef __HashIterator<K, T, const T&,const T*, KeyOfT, Hash> const_iterator;iterator begin(){Node* cur = nullptr;for (int i = 0;i < _tables.size();i++){if (_tables[i]){cur = _tables[i];break;}}return iterator(cur, this);}iterator end(){return iterator(nullptr, this);}const_iterator begin()const{Node* cur = nullptr;for (int i = 0;i < _tables.size();i++){if (_tables[i]){break;}}return const_iterator(cur, this);}const_iterator end()const{return const_iterator(nullptr, this);}~HashTable(){for (Node* cur : _tables){while (cur){Node* tmp = cur;cur = cur->_next;delete tmp;}}}size_t GetSize(){return _tables.size();}iterator Find(const K& key){if (_tables.size() == 0)return iterator(nullptr,this);KeyOfT kot;Hash hash;size_t hashi = hash(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){return iterator(cur,this);}cur = cur->_next;}return iterator(nullptr, this);}bool Erase(const K& key){KeyOfT kot;Hash hash;size_t hashi = hash(key) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (kot(cur->_data) != key){prev = cur;cur = cur->_next;}else{if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}}return false;}pair<iterator,bool> Insert(const T& data){KeyOfT kot;iterator it = Find(kot(data));if ( it!=end()){return make_pair(it,false);}Hash hash;if (_n == _tables.size())//先判断是否要扩容,且该代码包含为空的情况{size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newtable(newsize,nullptr);for (Node* cur : _tables){while (cur){Node* tmp = cur;cur = cur->_next;size_t hashi = hash(kot(tmp->_data)) % newtable.size();tmp->_next = newtable[hashi];newtable[hashi] = tmp;}}_tables.swap(newtable);}size_t hashi = hash(kot(data)) % _tables.size();Node* tmp = new Node(data);tmp->_next = _tables[hashi];_tables[hashi] = tmp;return make_pair(iterator(tmp,this),true);}private:vector<Node*> _tables;size_t _n;};}
1.闭散列也就是发放定址法(线性探测)实现哈希表
a.哈希表节点设计
两个模板参数一个键类型K,一个值类型V,有一个数据,然后数据类型是pair<K,V>,一个状态值是枚举类型,不去设置泛型的数据类型T,而是设计成这种数据格式,这个设计和AVL树节点设计很像
b.哈希表设计
哈希表的成员有一个vector,vector的元素就是哈希表节点,还有一个size_t,是节点个数
哈希表模板参数本应由三个,第一个是键的类型K,第二个是值类型V,第三个是将K转换成size_t的仿函数类,但这里偷懒了没有加,测试直接用的K是int,而且因为节点数据类型不是T,所以也不用加把T变成K的仿函数类
c.哈希表节点为什么有三种状态而不是只有empty和exist两种
插入一个元素,如果键映射的位置已经是exist那就往后探测,如果是delete或empty,那直接存就是了。
查找一个元素,如果是exist那就看看键是不是我要的,如果不是那就下一个,如果是delete,那说明这个已经被删了,但后面可能还有,所以继续往后探测,如果是empty那真到头了,直接结束吧,没有对应键的节点
也就是说delete这种状态是因为我们要用empty来表示查找结束,所以删除一个元素不能用empty来表示,所以加一个delete状态
d.哈希表中存储节点,没有额外资源
2.开散列也就是链地址法实现哈希表
a.哈希表节点设计
一个模板参数T表示数据类型,然后节点里有一个T类型数据,一个指针,因为节点是用单链表串起来的
b.哈希表设计
模板参数是,键的类型K,哈希表节点数据类型T,把数据类型T变成K的仿函数类keyOfT,把键类型K变成size_t的仿函数类类型Hash
成员是,一个vector,vector的元素是节点指针,存的是一个哈希桶单链表的头结点指针,还有一个size_t成员,表示节点个数
c.哈希表的插入
插入就是头插节点
d.哈希表的迭代器设计
哈希表的迭代器就是封装了结点指针的类
成员有结点指针,这个自然。节点是以单链表的形式存在的,我们为了支持迭代器++能够找到下一个节点,要在迭代器对象里加一个哈希表对象的指针,不然下一个节点不在这个单链表我们就没法弄
迭代器的模板参数K,T,keyOfT,Hash都是为了有结点类型和哈希表类型,剩下俩ref和ptr是为了实现迭代器和const迭代器
ps:迭代器的解引用*返回的是节点里数据的引用,迭代器的成员访问->,返回的是节点里数据的指针。编译器在编 迭代器->成员 时,先是 迭代器.operator->(),返回节点里数据的指针,然后 编译器自动 指针->成员,最终实现访问节点里数据的成员的效果
扩容触发条件
哈希表通常在负载因子(Load Factor)达到某个预设阈值时进行扩容。最常见的触发条件是:
-
当插入新元素后,当前负载因子超过预设的最大负载因子阈值时
负载因子(Load Factor)
负载因子是衡量哈希表填充程度的一个指标,计算公式为:
负载因子 = 哈希表中当前元素个数 / 哈希表的总容量