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

【C++】哈希表:除留余散法和哈希桶的实现

本文是小编巩固自身而作,如有错误,欢迎指出!

目录

一、unordered map and unordered set

二、哈希表

1.直接定址法

2.哈希冲突

3.负载因子

4.1除法散列法/除留余数法

三、哈希冲突的处理

1.开放定址法

2.链地址法

四、完整代码实现及测试结果

.h文件

.cpp文件



一、unordered map and unordered set

在前文我们已经学过了map和set而unordered map 和 unordered set又是什么呢?首先,很明显unordered的意思是“无序的”,和寻常的map和set在表达上最大的区别就map和set是有序的,而后者是无序的,在底层上最大的区别就是map和set是由红黑树组成,而后者是由哈希表组成

二、哈希表

1.直接定址法

当关键字的范围⽐较集中时,直接定址法就是⾮常简单⾼效的⽅法,⽐如⼀组关键字都在[0,99]之间, 那么我们开⼀个100个数的数组,每个关键字的值直接就是存储位置的下标。

而我们今天学习的哈希表的实现就是类似这样的实现方法,采用一一映射的方法使数据被储存在不同的空间中。

2.哈希冲突

在上述的直接定址法,存在一个很大的缺陷:

当关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤。假设我 们只有数据范围是[0,9999]的N个值,我们要映射到⼀个M个空间的数组中(⼀般情况下M>=N),那么 就要借助哈希函数  (hashfunction)hf,关键字key被放到数组的h(key)位置,这⾥要注意的是h(key)计 算出的值必须在[0,M)之间。

而在这种情况下,就会出现一种情况是什么呢?就是两个不同的key会映射到相同的位置上去,那么此刻该怎么处理呢?就是我们今天要学习的哈希表的核心问题。

3.负载因子

假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么负载因子=N/M,负载因⼦有些地⽅ 也翻译为载荷因⼦/装载因⼦等,他的英⽂为load factor。负载因⼦越⼤,哈希冲突的概率越⾼,空间 利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;

4.1除法散列法/除留余数法

除法散列法也叫做除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为 映射位置的下标,也就是哈希函数为:h(key)=key%M

 当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是2^x ,那么key %2^x本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如:{63 , 31}看起来没有关联的值,如果M是16,也就是 ,那么计算出的哈希值都是15,因为63的⼆进制后8位是 00111111,31的⼆进制后8位是 00011111。如果是 10^x,就更明显了,保留的都是10进值的后x位,如:{112, 12312},如果M是100,也就是10^2 ,那么计算出的哈希值都是12。

三、哈希冲突的处理

实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了 冲突,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法

1.开放定址法

开放定址法其中一个主要的方法就是用我们上面所述的除法散列法。

然后使用线性探测:从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛走到哈希表尾,则回绕到哈希表头的位置

下⾯演⽰ {19,30,5,36,13,20,21,12} 等这⼀组值映射到M=11的表中

namespace open_way
{template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:HashTable():_tables(11), _n(0){}bool Insert(const pair<K, V>& kv){if ((double)_n / (double)_tables.size() >= 0.7)//扩容{//std::vector<HashData<K, V>> newtables(_tables.size()*2);////遍历旧表将其映射到新表//for (auto& data : _tables)//{//	if (data._status == EXIST)//	{//		//...//	}//}//_tables.swap(newtables);HashTable<K, V, Hash> newHT;newHT._tables.resize(_tables.size() * 2);  // 新表大小为原表2倍//遍历旧表将其映射到新表for (auto& data : _tables){if (data._status == EXIST){newHT.Insert(data._kv);}}_tables.swap(newHT._tables);_n = newHT._n;  // 同步新表的有效元素数}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();size_t hashi = hash0;size_t i = 1;//线性探测while (_tables[hashi]._status == EXIST)//当前位置已经存在时{hashi = (hash0 + i) % _tables.size();++i;}_tables[hashi]._kv = kv;_tables[hashi]._status = EXIST;++_n;return true;}HashData<K, V>* Find(const K& key){Hash hs;size_t hash0 = hs(key) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._status != EMPTY){if (_tables[hashi]._status == EXIST && _tables[hashi]._kv.first == key){return &_tables[hashi];}hashi = (hash0 + i) % _tables.size();++i;}return nullptr;}bool Erase(const K& key){auto* ptr = Find(key);if (ptr){ptr->_status = DELETE;--_n;return true;}else{return false;}}private:std::vector<HashData<K, V>> _tables;size_t _n = 0;//有效数据的个数};void testhash1(){HashTable<int, int> ht;int a[] = { 19,30,5,36,13,20,21,12,58 };for (auto e : a){ht.Insert({ e, e });}cout << ht.Find(5) << endl;cout << ht.Find(58) << endl;ht.Erase(5);cout << ht.Find(5) << endl;cout << ht.Find(58) << endl;for (size_t i = 100; i < 200; i++){ht.Insert({ i, i });}}struct StringHashFunc{// BKDRsize_t operator()(const string& str){size_t hash = 0;for (auto ch : str){hash += ch;hash *= 131;}return hash;}};void testhash2(){HashTable<string, string, StringHashFunc> dict;dict.Insert({ "insert", "插入" });auto ptr = dict.Find("insert");if (ptr){cout << ptr->_kv.second << endl;}StringHashFunc hf;cout << hf("abcd") << endl;cout << hf("bcad") << endl;cout << hf("aadd") << endl;}
}

上述的代码就是我们通过除法散列法实现的哈希表,除了上面我们讲到的关于如何处理哈希冲突,我们这里还额外强调一点就是key不是整数而是string类的情况

template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};
template<>  // 模板特化标记
struct HashFunc<string>  // 特化string类型
{size_t operator()(const string& str)  // 复用你的BKDR算法{size_t hash = 0;for (auto ch : str){hash += ch;hash *= 131;}return hash;}
};

这里我们专门实现了一个模版用来将我们的key转化为整数。

2.链地址法

所谓链地址法,很好理解,就是解决哈希冲突的方法是通过链表来实现的,在前面我们学习的线性探测法,是出现冲突就将其向后挪,而链地址法,就是出现了冲突,我们直接将其制作成一个链表,将其链接在下面

像这种这种方法解决哈希冲突我们一般将其称作哈希桶。

与上面的开放定址法的一个很大区别就在于链地址法的负载因子可以达到一再进行扩容。

四、完整代码实现及测试结果

.h文件

#pragma once
#include<vector>
#include<utility>using namespace std;enum Status
{EXIST,EMPTY,DELETE
};
template<class K, class V>
struct HashData
{pair<K, V> _kv;Status _status=EMPTY;
};
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};
template<>  // 模板特化标记
struct HashFunc<string>  // 特化string类型
{size_t operator()(const string& str)  // 复用你的BKDR算法{size_t hash = 0;for (auto ch : str){hash += ch;hash *= 131;}return hash;}
};namespace open_way
{template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:HashTable():_tables(11), _n(0){}bool Insert(const pair<K, V>& kv){if ((double)_n / (double)_tables.size() >= 0.7)//扩容{//std::vector<HashData<K, V>> newtables(_tables.size()*2);////遍历旧表将其映射到新表//for (auto& data : _tables)//{//	if (data._status == EXIST)//	{//		//...//	}//}//_tables.swap(newtables);HashTable<K, V, Hash> newHT;newHT._tables.resize(_tables.size() * 2);  // 新表大小为原表2倍//遍历旧表将其映射到新表for (auto& data : _tables){if (data._status == EXIST){newHT.Insert(data._kv);}}_tables.swap(newHT._tables);_n = newHT._n;  // 同步新表的有效元素数}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();size_t hashi = hash0;size_t i = 1;//线性探测while (_tables[hashi]._status == EXIST)//当前位置已经存在时{hashi = (hash0 + i) % _tables.size();++i;}_tables[hashi]._kv = kv;_tables[hashi]._status = EXIST;++_n;return true;}HashData<K, V>* Find(const K& key){Hash hs;size_t hash0 = hs(key) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._status != EMPTY){if (_tables[hashi]._status == EXIST && _tables[hashi]._kv.first == key){return &_tables[hashi];}hashi = (hash0 + i) % _tables.size();++i;}return nullptr;}bool Erase(const K& key){auto* ptr = Find(key);if (ptr){ptr->_status = DELETE;--_n;return true;}else{return false;}}private:std::vector<HashData<K, V>> _tables;size_t _n = 0;//有效数据的个数};void testhash1(){HashTable<int, int> ht;int a[] = { 19,30,5,36,13,20,21,12,58 };for (auto e : a){ht.Insert({ e, e });}cout << ht.Find(5) << endl;cout << ht.Find(58) << endl;ht.Erase(5);cout << ht.Find(5) << endl;cout << ht.Find(58) << endl;for (size_t i = 100; i < 200; i++){ht.Insert({ i, i });}}struct StringHashFunc{// BKDRsize_t operator()(const string& str){size_t hash = 0;for (auto ch : str){hash += ch;hash *= 131;}return hash;}};void testhash2(){HashTable<string, string, StringHashFunc> dict;dict.Insert({ "insert", "插入" });auto ptr = dict.Find("insert");if (ptr){cout << ptr->_kv.second << endl;}StringHashFunc hf;cout << hf("abcd") << endl;cout << hf("bcad") << endl;cout << hf("aadd") << endl;}
}namespace Hash_bucket
{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{typedef HashNode<K, V> Node;public:HashTable():_tables(11),_n(0){}bool Insert(const pair<K, V>& kv){//扩容if (_n == _tables.size()){//HashTable<K, V> newHT;//newHT._tables.resize(_tables.size() * 2);//for (auto cur : _tables)//{//	newHT.Insert(cur->_kv);//	cur = cur->_next; //}//_tables.swap(newHT._tables);vector<Node*> newtables(_tables.size() * 2);for (size_t i = 0;i < _tables.size();i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = cur->_KV.first % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}size_t hashi = kv.first % _tables.size();//头插Node* newNode = new Node(kv);newNode->_next = _tables[hashi];_tables[hashi] = newNode;++_n;return true;}Node* Find(const K& key){size_t hashi = key % _tables.size();Node* cur = _tables[hashi];whiel(cur){if (cur->_kv.first == key){return cur;}cur = cur->next;}return nullptr;}bool Erase(const K& key){size_t hashi = key % _tables.size();Node* prev=nullptr;Node* cur = _tables[hashi];while(cur){if (cur->_kv.first == key){if (prev == nullptr)//第一个节点{_tables[hashi] = cur->next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->next;}}private:vector<Node*> _tables;size_t _n = 0;};void testhash1(){HashTable<int, int> ht;int a[] = { 19,30,5,36,13,20,21,12,58 };for (auto e : a){ht.Insert({ e, e });}//cout << ht.Find(5) << endl;//cout << ht.Find(58) << endl;//ht.Erase(5);//cout << ht.Find(5) << endl;//cout << ht.Find(58) << endl;//	for (size_t i = 100; i < 200; i++)//	{//		ht.Insert({ i, i });//	}}
}

.cpp文件

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<unordered_set>
#include<unordered_map>
#include<set>
#include"HashTable.h"
using namespace std;void test01()
{unordered_set<int> s1 = { 1,3,5,6,7,2 };s1.insert(11);auto it = s1.begin();while (it != s1.end()){cout << *it << " ";it++;}cout << endl;unordered_map<string, string> dict;dict.insert({ "apple","苹果" });dict.insert({ "orange","橘子" });dict.insert({ "banana","香蕉" }); for (auto& [k, v] : dict){cout << k << ":" << v << endl;}}
int test02()
{const size_t N = 1000000;unordered_set<int> us;set<int> s;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){//v.push_back(rand()); // N比较大时,重复值比较多v.push_back(rand() + i); // 重复值相对少//v.push_back(i); // 没有重复,有序}size_t begin1 = clock();for (auto e : v){s.insert(e);}size_t end1 = clock();cout << "set insert:" << end1 - begin1 << endl;size_t begin2 = clock();us.reserve(N);for (auto e : v){us.insert(e);}size_t end2 = clock();cout << "unordered_set insert:" << end2 - begin2 << endl;int m1 = 0;size_t begin3 = clock();for (auto e : v){auto ret = s.find(e);if (ret != s.end()){++m1;}}size_t end3 = clock();cout << "set find:" << end3 - begin3 << "->" << m1 << endl;int m2 = 0;size_t begin4 = clock();for (auto e : v){auto ret = us.find(e);if (ret != us.end()){++m2;}}size_t end4 = clock();cout << "unorered_set find:" << end4 - begin4 << "->" << m2 << endl;cout << "插入数据个数:" << s.size() << endl;cout << "插入数据个数:" << us.size() << endl << endl;size_t begin5 = clock();for (auto e : v){s.erase(e);}size_t end5 = clock();cout << "set erase:" << end5 - begin5 << endl;size_t begin6 = clock();for (auto e : v){us.erase(e);}size_t end6 = clock();cout << "unordered_set erase:" << end6 - begin6 << endl << endl;return 0;}int main()
{//test01();//test02();//testhash1();//testhash2();Hash_bucket::testhash1();return 0;
}

本次分享就到这里结束了,后续会继续更新,感谢阅读!

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

相关文章:

  • 沧州网站运营自己做网站需要什么材料
  • PostgreSQL查不动?分区表+覆盖索引+物化视图的优化魔法了解下?
  • 多相CFD中的模型转换:Ansys Fluent中的从DPM到VOF和欧拉壁膜
  • 关于学校的网站模板免费下载高端网站建设磐石网络好
  • 在半导体制造中如何选择最佳的刻蚀方法?
  • 构建Django的Web镜像
  • 历史数据分析——锦江酒店
  • 做网站站怎么赚钱吗企业网站推广的收获与启示
  • 大厂硬件岗位笔试题库-卷11
  • 【操作系统】408操作系统核心考点精讲:第二章——进程的概念、组成与特征​
  • 基于脉冲神经网络的语音识别系统实现:识别“将榴弹从位置幺搬到位置两“命令
  • 破茧成蝶:全方位解析Java学习难点与征服之路
  • 江门网页建站模板aws 搭建wordpress
  • C语言:使用顺序表实现通讯录
  • 手机网站与app免费的网站平台有哪些
  • SQL186 对试卷得分做min-max归一化
  • 哪家开发app好有南昌网站优化公司
  • vue3 npm run dev局域网可以访问,vue启动设置局域网访问,
  • 网站建设续费催款通知书哈尔滨微信网站开发
  • NLP之Embedding:Youtu-Embedding的简介、安装和使用方法、案例应用之详细攻略
  • 做网站需要学哪些语言wordpress sina
  • Redis常见指令
  • 机器学习02——环境安装
  • 网站可以用中国二字做抬头吗WordPress 评论框表情
  • 随笔——记一次常见的浮点数精度问题到Grisu3初识
  • 【git】rebase 和 merge 区别及使用建议
  • 机器学习催化剂设计!
  • Agent Zero:重新定义AI Agent的有机生长框架——从“预设工具“到“自我进化“的范式革命
  • 脚本更新--CosMx、Xenium的邻域通讯分析(R版本)
  • VS Code搭建C/C++开发调试环境-Windows