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

【C++】哈希表实现 - 链地址法/哈希桶

目录

    • 前言
  • 一、链地址法
    • 1.1 解决哈希冲突的思路
    • 1.2 关于扩容
    • 1.3 关于极端场景
  • 二、代码实现
    • 2.1 节点结构框架
    • 2.2 insert 函数
      • 2.2.1 插入逻辑
      • 2.2.2 扩容逻辑
      • 2.2.3 插入函数测试
    • 2.3 find 和 erase 函数
        • 测试代码
    • 2.4 质数表
    • 2.5 析构函数
    • 2.5 解决 key 不能取模的问题
      • 测试 string 类型

在这里插入图片描述
个人主页<—请点击
C++专栏<—请点击

前言

上期博客,我们讲解了哈希表的概念,并使用开放定址法实现了哈希表,本期博客我们将使用链地址法来实现哈希表。跳转上期博客:【C++】哈希表实现 - 开放定址法

一、链地址法

1.1 解决哈希冲突的思路

开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,链地址法也叫做哈希桶或者拉链法

下面演示 {19,30,5,36,13,20,21,12,24,96} 等这一组值映射到M=11的表中。
在这里插入图片描述

1.2 关于扩容

开放定址法负载因子必须小于1,链地址法的负载因子就没有限制了,可以大于1。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低;

stlunordered系列最大负载因子基本控制在1,大于1就扩容,我们下面实现也使用这个方式。

1.3 关于极端场景

如果极端场景下,某个桶特别长怎么办?其实可以考虑使用全域散列法,这样就不容易被针对了。但是假设不是被针对了,用了全域散列法,但是偶然情况下,某个桶很长,查找效率很低怎么办?

这里在Java8HashMap中当桶的长度超过一定阀值(8)时就把链表转换成红黑树。当然,一般情况下,不断扩容,单个桶很长的场景还是比较少的,C++没有考虑使用这种方式,还是使用的下面挂链表的方式。 我们在实现的时候也就直接挂链表了。

二、代码实现

2.1 节点结构框架

template<class K, class V>
struct HashNode
{// 单链表足以满足需求pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K, V>& kv):_kv(kv),_next(nullptr){ }
};template<class K, class V>
class HashTable
{
public:typedef HashNode<K, V> Node;HashTable():_tables(11),_n(0){ }
private:vector<Node*> _tables;size_t _n; // 实际存储的数据个数
};

这里我们没有使用stl库中的list链表,而选择使用了原生链表,这样比较容易获取链表中的成员。

2.2 insert 函数

2.2.1 插入逻辑

bool insert(const pair<K, V>& kv)
{int hashi = kv.first % _tables.size();Node* newNode = new Node(kv);// 头插,尾插还要找尾// 第一个节点的地址在表里面newNode->_next = _tables[hashi];_tables[hashi] = newNode;++_n;return true;
}

插入逻辑,我们选择头插,那就是将申请的新节点的_next指针指向旧的第一个节点,然后新节点成为新的第一个节点。不要忘了增加有效数据个数。

2.2.2 扩容逻辑

这里扩容时,就和前面的开放定址法不太一样,开放定址法是在insert函数中重新定义了一个类对象,这个类对象再调用insert函数,将数据重新定址,最后把类对象交换过来。但是放在链地址法这里,使用这样的方法会重新定义一遍节点,会造成空间的浪费,所以我们考虑把节点摘下来,重新挂。

bool insert(const pair<K, V>& kv)
{// 负载因子 == 1,就扩容if (_n == _tables.size()){vector<Node*> newtables(_tables.size() * 2);for (int i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){// 存储 cur 的下一个节点Node* next = cur->_next;int hashi = cur->_kv.first % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next; // 走到原链表的下一个节点}// 清空原链表节点数据_tables[i] = nullptr;}// 交换新旧表_tables.swap(newtables);}int hashi = kv.first % _tables.size();Node* newNode = new Node(kv);// 头插,尾插还要找尾// 第一个节点的地址在表里面newNode->_next = _tables[hashi];_tables[hashi] = newNode;++_n;return true;
}

2.2.3 插入函数测试

测试代码

void test_hash1()
{int a[] = { 19, 30, 5, 36, 13, 20, 21, 12, 24, 96, 23, 31 };HashTable<int, int> hash;for (auto& e : a){hash.insert({ e, e });}
}

测试结果
在这里插入图片描述
在这里插入图片描述

2.3 find 和 erase 函数

Node* find(const K& key)
{int hashi = key % _tables.size();Node* cur = _tables[hashi];while (cur){// 找到返回节点地址if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}bool erase(const K& key)
{int hashi = key % _tables.size();Node* cur = _tables[hashi];Node* pre = nullptr; // 保存 cur 的前一个节点while (cur){// 找到返回节点地址if (cur->_kv.first == key){// 要找的节点是表头if (pre == nullptr){_tables[hashi] = cur->_next;}else{pre->_next = cur->_next;}delete cur;return true;}pre = cur;cur = cur->_next;}
测试代码

我们依旧按照之前的代码进行测试,我们发现96, 30, 19在一个桶中,我们把这三个全部删掉。

void test_hash2()
{int a[] = { 19, 30, 5, 36, 13, 20, 21, 12, 24, 96 };HashTable<int, int> hash;for (auto& e : a){hash.insert({ e, e });}cout << hash.find(96) << endl;cout << hash.erase(96) << endl;cout << hash.find(30) << endl;cout << hash.erase(30) << endl;cout << hash.find(19) << endl;cout << hash.erase(19) << endl;
}

测试结果
在这里插入图片描述
执行后
在这里插入图片描述

有了find之后,我们就可以再次完善insert了,完善不能插入相同元素的功能。

bool insert(const pair<K, V>& kv)
{if (find(kv.first)){return false;}// ...
}

2.4 质数表

和之前一样,我们这里的扩容更改为依旧使用stl源码里面给的近乎二倍增长的质数表。

template<class K, class V>
class HashTable
{
public:typedef HashNode<K, V> Node;HashTable():_tables(__stl_next_prime(1)) // 开初始空间, 函数返回的是比 1 大且最接近 1 的值,_n(0){ }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;}bool insert(const pair<K, V>& kv){// 负载因子 == 1,就扩容if (_n == _tables.size()){vector<Node*> newtables(__stl_next_prime(_tables.size() + 1));// ...}// ...}// ...
private:vector<Node*> _tables;size_t _n; // 实际存储的数据个数
};

测试代码

void test_hash3()
{HashTable<int, int> hash;for (int i = 1; i < 200; i++){hash.insert({ i, i });if (i == 100) // 可控断点{cout << endl;}}
}

测试结果
在这里插入图片描述
没有出现问题。

2.5 析构函数

~HashTable()
{for (int i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}
}

2.5 解决 key 不能取模的问题

这里的解决方法和我们的开放定址法相同,依旧使用仿函数把key转化成可以取模的整型值。

template<class K>
struct HashOfKey
{size_t operator()(const K& k){return (size_t)k;}
};template<>
struct HashOfKey<string>
{size_t operator()(const string& k){size_t hs = 0;for (auto& e : k){hs += e;hs *= 131; // 能够有效防止 "abcd" "bcda"的整型值一样的情况}return hs;}
};
template<class K, class V, class Hash = HashOfKey<K>>
class HashTable
{
public:typedef HashNode<K, V> Node;HashTable():_tables(__stl_next_prime(1)) // 开初始空间, 函数返回的是比 1 大且最接近 1 的值,_n(0){ }bool insert(const pair<K, V>& kv){Hash kot;// ...// 负载因子 == 1,就扩容if (_n == _tables.size()){vector<Node*> newtables(__stl_next_prime(_tables.size() + 1));for (int i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){// 存储 cur 的下一个节点Node* next = cur->_next;int hashi = kot(cur->_kv.first) % newtables.size();// ...}// ...}int hashi = kot(kv.first) % _tables.size();// ...return true;}Node* find(const K& key){Hash kot;size_t hashi = kot(key) % _tables.size();// ...while (cur){// 找到返回节点地址if (kot(cur->_kv.first) == kot(key))// ...}return nullptr;}bool erase(const K& key){Hash kot;size_t hashi = kot(key) % _tables.size();// ...while (cur){// 找到返回节点地址if (kot(cur->_kv.first) == kot(key)){// ...}// ...}return false;}private:vector<Node*> _tables;size_t _n; // 实际存储的数据个数
};

测试 string 类型

测试代码

void test_hash4()
{HashTable<string, int> hash;hash.insert({ "one", 1 });hash.insert({ "two", 2 });hash.insert({ "five", 5 });cout << hash.find("five") << endl;cout << hash.erase("five") << endl;cout << hash.find("five") << endl;
}

测试结果

在这里插入图片描述

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

http://www.dtcms.com/a/545082.html

相关文章:

  • 建设一个教程视频网站需要什么资质龙岗网络推广公司
  • 后端日志框架
  • 服务器在企业中的作用与价值
  • 《搭建属于自己的网站之网页前端学习》基础入门
  • 拿网站做商标童装网站建设
  • 金融投资网站毕设做网站是不是太low
  • 【pandas】pandas apply 方法详解
  • 散户如何运用券商手机智能T0算法
  • CRMEB-PHP订单删除机制详解
  • 分数阶微分方程谱方法求解
  • 经典“绿叶”算法——SVM回归预测(SVR)算法及MATLAB实现
  • 南漳网站开发wordpress flash加载插件
  • 过度依赖单一工具会带来哪些风险
  • 132-Spring AI Alibaba Vector Neo4j 示例
  • 杜集网站建设免费做网站公司ydwzjs
  • 中心网站设计建筑工程总承包合同范本
  • AWS ECS 健康检查与部署配置最佳实践指南
  • leetcode 205. 同构字符串 python
  • wordpress 纯静态插件wordpress 文章seo
  • [无人机sdk] `Linker` | 设置加密密钥
  • 淘宝网发布网站建设wordpress 取消做这
  • 【数字逻辑】24进制LED综合控制实战!10灯精准执行(74HC161+138+139完整方案)
  • 范式重构:可逆计算如何颠覆DDD的经典模式
  • 【win11】ffmpeg 去除mp4文件的音频
  • 东莞建设公司网站打开网站代码
  • 瑞萨SDK编译linux时,make menuconfig报错
  • 自助公益网站建设如何进行网站开发
  • 仓颉语言相机拍照功能实现深度解析
  • **发散创新:AI绘画编程探索与实践**随着人工智能技术的飞速发展,AI绘
  • linux命令-文件目录操作-1