当前位置: 首页 > news >正文

哈希表的实现

1. 哈希概念
哈希(hash)⼜称散列,是⼀种组织数据的⽅式。从译名来看,有散乱排列的意思。本质就是通过希
函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的置进⾏快速查找.
1.2 哈希冲突
这⾥存在的⼀个问题就是,两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的方案。
1.3 负载因⼦
假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么 ,负载因⼦有些地⽅
也翻译为载荷因⼦/装载因⼦等,他的英⽂为load factor。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;
1.4 将关键字转为整数
我们将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换整
数,这个细节我们后⾯代码实现中再进⾏细节展⽰。下⾯哈希函数部分我们讨论时,如果关键字不是整数,那么我们讨论的Key是关键字转换成的整数。
2.开放定址法
在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按 照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于的。这⾥的规则有三种:线性探测、⼆次探测、双重探测。
2.1开放定址法,即线性探测解决冲突
2.2 开放定址法的哈希表结构
enum State
{
EXIST,
EMPTY,
DELETE
};
template < class K , class V >
struct HashData
{
pair<K, V> _kv;
  State _state = EMPTY;
};
  template < class K , class V >
  class HashTable
  {
  private :
  vector<HashData<K, V>> _tables;
  size_t _n = 0 ; // 表中存储数据个数
  };

 2.3key不能取模的问题

当key是string/Date等类型时,key不能取模,那么我们需要给HashTable增加⼀个仿函数,这个仿函 数⽀持把key转换成⼀个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数就⽤默认参数即可,如果这个Key不能转换为整形,我们就需要⾃⼰实现⼀个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整形值不同。string做哈希表的key⾮常常⻅,所以我们可以考虑把string特化⼀下

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 e : key)
        {
            hash *= 31;
            hash += e;
        }

        return hash;
    }
};

2.4插入操作

插入的时候可能面临需要扩容的情况,所以需要判断负载因子后再进行插入,下面是插入的代码

 

bool Insert(const pair<K, V>& kv){if (Find(kv.first))//下面会实现查找功能return false;size_t size = _tables.size();if (_n * 10 / size >= 7)//判断当前储存的数据个数是否超过负载因子{size *= 2;HashTable<K,V> hs;hs._tables.resize(size);for (auto &e : _tables){if (e._state == EXIST)//存在才会从旧表迁移到新表hs.Insert(e._kv);}_tables.swap(hs._tables);}HashFunc<K> hf; size_t hashi = hf(kv.first) % size;while (_tables[hashi]._state == EXIST)//找到空或者被删除的地方{hashi++;}HashData<K, V> newdata;//插入节点并设置状态和增加数据个数newdata._kv = kv;newdata._state = EXIST;_tables[hashi] = newdata;_n++;return true;}

2.5查找操作

HashData<K, V>* Find(const K& key){HashFunc<K> hf;size_t size = _tables.size();size_t hashi = hf(key) % size;size_t hash0 = hashi;size_t i = 1;while (_tables[hashi]._state != EMPTY)//遍历哈希表{if (hf(_tables[hashi]._kv.first) == hf(key) && _tables[hashi]._state == EXIST)//数据存在且状态为存在return &_tables[hashi];hashi = (hash0 + i) % size;//超过size会从0继续遍历++i;}return nullptr;}

2.6删除操作

bool Erase(const K& key){HashData<K, V>* hd= Find(key);if (hd){hd->_state = DELETE;//将状态修改为删除--_n;return true;}return false;}
1.6.3 链地址法
解决冲突的思路
开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。
下⾯演⽰ {19,30,5,36,13,20,21,12,24,96} 等这⼀组值映射到M=11的表中。

 3.1扩容

开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;stl中unordered_xxx的最⼤负载因⼦基本控制在1,⼤于1就扩容,我们下⾯实现也使⽤这个⽅式。
3.2代码实现
template<class T>struct HashNode{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){}};// K 为 T 中key的类型// T 可能是键值对,也可能是K// KeyOfT: 从T中提取key// Hash将key转化为整形,因为哈市函数使用除留余数法template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>class HashTable{typedef HashNode<T> Node;public:HashTable(){_tables.resize(10, nullptr);}// 哈希桶的销毁/*~HashTable();*/// 插入值为data的元素,如果data存在则不插入bool Insert(const T& data){size_t size = _tables.size();KeyOfT kof;Hash hs;size_t key=kof(data);//将数据提取,如int 和pair<k,T>就需要使用不同的仿函数提取数据if (Find(key))return false;if (_n / size == 1){HashTable newHT;size_t newsize = 2 * size;newHT._tables.resize(newsize);for (int i = 0; i < size; i++){Node* cur = _tables[i];//遍历当前桶的数据while(cur){Node* next = cur->_next;cur->_next = nullptr;//旧表中节点,挪动新表重新映射的位置if (newHT._tables[hs(kof(cur->_data)) % newsize] == nullptr){newHT._tables[hs(kof(cur->_data)) % newsize] = cur;//如果是第一个映射的数据则直接插入}else{newHT._tables[hs(kof(cur->_data)) % newsize]->_next = cur;//不是第一个映射进行尾插}cur = next;}_tables[i] = nullptr;}_tables.swap(newHT._tables);//交换表}Node* newnode = new Node(data);//头插数据Node* prev = _tables[hs(kof(data) % size)];_tables[hs(kof(data) % size)] = newnode;newnode->_next = prev;_n++;return true;}// 在哈希桶中查找值为key的元素,存在返回true否则返回falsebool Find(const K& key){size_t size = _tables.size();KeyOfT kof;Hash hs;Node* node = _tables[hs(key) % size];//指定数据是哪一个桶while (node)//遍历当前桶{if (hs(kof(node->_data)) == hs(key)){return true;}node = node->_next;}return false;}// 哈希桶中删除key的元素,删除成功返回true,否则返回falsebool Erase(const K& key){size_t size = _tables.size();KeyOfT kof;Hash hs;Node* node = _tables[hs(key) % size];Node* prev = nullptr;while (node)//遍历数据所在桶{if (hs(kof(node->_data)) == hs(key))//找到改数据{if (prev){prev->_next = node->_next;delete node;--_n;return true;}else//如果是该桶第一个数据{if (node->_next)//后面有数据{_tables[hs(key) % size] = node->_next;}else//后面没有数据{_tables[hs(key) % size] = nullptr;}delete node;--_n;return true;}}prev = node;//保留前节点node = node->_next;//往下一个节点走}return false;}private:vector<Node*> _tables;  // 指针数组size_t _n = 0;			// 表中存储数据个数};

4.测试以上代码

void test1()
{open_address1::HashTable<int, int> h1;vector<pair<int, int>> v;v.push_back({ 1,1 });v.push_back({ 11,11 });v.push_back({ 2,2 });v.push_back({ 4,4 });v.push_back({ 6,6 });v.push_back({ 16,16 });v.push_back({ 8,8 });v.push_back({ 9,9 });for (auto e : v){h1.Insert(e);}h1.Insert({ 26,26 });h1.Insert({ 36,36 });if (h1.Find(1)){cout << "找到了" << endl;}elsecout << "没找到了" << endl;h1.Erase(1);if (h1.Find(1)){cout << "找到了" << endl;}elsecout << "没找到了" << endl;h1.Insert({ 41,41 });h1.Print();return;
}void test2()
{open_address1::HashTable<string, string> h2;h2.Insert({ "insert","插入" });h2.Insert({ "find","查找" });h2.Insert({ "aabb","aabb" });h2.Insert({ "bbaa","bbaa" });h2.Print();
}void test3()
{hash_bucket::HashTable<int,pair<int,int>, PairKeofT<int,int>> h1;vector<pair<int, int>> v;v.push_back({ 1,1 });v.push_back({ 11,11 });v.push_back({ 2,2 });v.push_back({ 4,4 });v.push_back({ 6,6 });v.push_back({ 16,16 });v.push_back({ 8,8 });v.push_back({ 9,9 });for (auto e : v){h1.Erase(e.first);}h1.Insert({ 26,26 });h1.Insert({ 36,36 });h1.Insert({ 46,46 });if(h1.Erase(46))cout<<"delete 46"<<endl;if (h1.Erase(36))cout << "delete 36"<<endl;if (h1.Erase(26))cout << "delete 26"<<endl;return;
}

4.1测试结果

相关文章:

  • 大模型AI的“双刃剑“:数据安全与可靠性挑战与破局之道
  • 高精度并行2D圆弧拟合(C++)
  • ORACLE RAC环境使用ASM机制零宕机时间更换存储的实践
  • 安宝特案例 | AR技术在院外心脏骤停急救中的革命性应用
  • 4.4 记忆机制与上下文管理:短期与长期记忆的设计与应用
  • 新时代质量管理体系-端到端流程通俗演义,什么是端到端流程?
  • 3D高斯个人笔记
  • 如何实现客户端热部署能力方案
  • 写一个esp开发SPI的链接吗,
  • 机器人新革命:Pi 0.5如何让智能走进千家万户
  • 【踩坑记录】stm32 jlink程序烧录不进去
  • 电力作业安全工器具全解析:分类、配置与检查要点
  • 解决高德地图AMapUtilCoreApi、NetProxy类冲突
  • 最小生成树-prim、kruskal算法
  • 配置 C/C++ 语言智能感知(IntelliSense)的 c_cpp_properties.json 文件内容
  • Redis Cluster 使用 CRC16 算法实现 Slot 槽位分片的核心细节
  • git Http改用户下载
  • 直接偏好优化(Direct Preference Optimization,DPO):论文与源码解析
  • 3. pandas笔记之:创建
  • 如何在Spring Boot中配置自定义端口运行应用程序
  • 解放日报头版:外资汽车产业链布局上海步伐明显加快
  • 上海4-6月文博美展、剧目演出不断,将开设直播推出文旅优惠套餐
  • 正荣地产旗下“H20正荣2”债未能于宽限期内支付分期偿付款,尚未就新兑付方案达成一致
  • 三博脑科跌超10%:董事长遭留置立案,称控制权未变化,经营秩序正常
  • 大连万达商业管理集团提前兑付“22大连万达MTN001” ,本息2.64亿元
  • 洛阳白马寺存争议的狄仁杰墓挂牌,当地文物部门:已确认