C++数据结构——哈希表

目录

一,unordered系列容器

1.1 关于unordered系列

1.2 unordered_set

1.4 unordered_map

1.5 性能对比

二,哈希

2.1 哈希概念

2.2 常用哈希函数

2.3 哈希冲突及解决

2.3.1 闭散列

2.3.2 开散列

2.4 哈希表扩容

2.4.1 闭散列扩容

2.4.2 开散列扩容

三,哈希表模拟实现

3.1 映射函数实现

3.2 闭散列哈希表

3.3 开散列哈希表


一,unordered系列容器

1.1 关于unordered系列

在C++98中,STL提供了以红黑树为底层的一系列关联式容器,查询时效率可达到logN,但是当树中节点非常多时,查询效率也不理想,所以在C++11中,STL提供了unordered系列的几个容器,使用哈希表作为底层,大大增加了查询效率

1.2 unordered_set

关于unordered_set的使用和之前介绍的set大体相同,如下代码:

void test_unordered_set()
{unordered_set<int> s;s.insert(2);s.insert(3);s.insert(1);s.insert(2);s.insert(5);unordered_set<int>::iterator it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;
}

 

1.4 unordered_map

void test_unordered_map()
{string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨" };map<string, int> countMap;for (auto& e : arr){countMap[e]++;}for (auto& kv : countMap){cout << kv.first << ":" << kv.second << endl;}
}

 

1.5 性能对比

void test_op() //测试性能
{//产生n个随机数int n = 100000;vector<int> v;v.reserve(n);srand(time(0));for (int i = 0; i < n; ++i){//把n个随机数放到vector里去//v.push_back(i); //有序插入// //v.push_back(rand());  // 重复多v.push_back(rand() + i);  // 重复少}size_t begin1 = clock();set<int> s;for (auto e : v){s.insert(e);//先往set插入}size_t end1 = clock(); size_t begin2 = clock();unordered_set<int> us;for (auto e : v){us.insert(e);//再往unordered_set插入}size_t end2 = clock();cout << "size:" << s.size() << endl;cout << "set insert:" << end1 - begin1 << endl; //算出set插入时间cout << "unordered_set insert:" << end2 - begin2 << endl; //算出unordered_set插入时间cout << endl;size_t begin3 = clock();for (auto e : v){s.find(e);}size_t end3 = clock();size_t begin4 = clock();for (auto e : v){us.find(e);}size_t end4 = clock();//对比查找效率cout << "set find:" << end3 - begin3 << endl;cout << "unordered_set find:" << end4 - begin4 << endl;cout << endl;size_t begin5 = clock();for (auto e : v){s.erase(e);}size_t end5 = clock();size_t begin6 = clock();for (auto e : v){us.erase(e);}size_t end6 = clock();//对比删除效率cout << "set erase:" << end5 - begin5 << endl;cout << "unordered_set erase:" << end6 - begin6 << endl;unordered_map<string, int> countMap;countMap.insert(make_pair("苹果", 1));//可以支持unordered_map<string, int> countmap;countmap.insert(make_pair("苹果", 1));//综合各种场景而言,unordered系列综合性能是更好的,尤其是find
}

 

二,哈希

2.1 哈希概念

哈希本质是一种设计思路。

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须经过多次关键码的比较。顺序查找时间复杂度为O(N),平衡树中为O(logN),查找效率取决于搜索过程中元素的比较次数。

所以,为了使查找效率更高,推出了一种理想的搜索方法,即不经过任何比较,一次直接从表中查到相关的数据。如果构造一种存储结构,通过某种函数(HashFunc)使元素的存储位置与它的关键码之间建立一一映射的关系,那么查找时能很快找到该数据。

向该结构中:

①插入元素:根据插入元素的key值,以此函数计算出该元素的存储位置进行存放

②查找元素:对要查找元素的key值进行相同的计算,得出存储位置,再对比关键码查看结构当中是否有该元素

该方法被称为哈希(散列)方法,哈希方法中使用的位置计算函数称为哈希(散列)函数,构造出来的结构称为哈希表

例如:数据集合{ 1,6,7,4,5,9 },哈希函数设置为hash(key) = key % capacity

用该方法进行搜索不必进行多次关键码比较,因此搜索速度比较快

2.2 常用哈希函数

1,直接定值法

取关键字的某个线性函数为散列地址:Hash(key) = A*key + B。

这种方法的优点是简单,缺点是需要提前直到关键字的分布情况,适合查找比较小且连续的数据

2,除留余数法

设哈希表允许的地址数为m,去一个不大于m,但最接近或者等于m的质数作为除数,按照哈希函数:Hash(key) = key%p (p<=m),将关键码转换成哈希地址

2.3 哈希冲突及解决

就上面的图而言,如果插入44时,会算出和4同样的位置。

不同关键字通过相同哈希函数计算出相同的地址,这种现象被称为哈希冲突或哈希碰撞。

2.3.1 闭散列

闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被填满,说明哈必然还有其他空位置,那么可以把key存放到冲突位置的“下一个”空位置中去,这里寻找下一个位置的方法称为“线性探测”

就上面的图,我们要插入44,会发生哈希冲突,所以我们从发生冲突的位置开始依次向后探测,直到寻找到下一个空位置为止,该方法应用在插入函数中,如下图:

对于删除,采用闭散列处理哈希冲突时,不能直接删除表中的数据否则会影响其他数据的搜索,所以采用标记的伪删除法来删除,给要删除的位置打上delete的标记,具体实现请看后面的模拟实现部分

2.3.2 开散列

开散列又叫链地址法,首先对关键码集合用哈希函数计算地址,具有相同地址的关键码用一个单链表集合起来,称每个单链表为一个桶,每个链表的头结点存在哈希表中,如下图:

2.4 哈希表扩容

2.4.1 闭散列扩容

哈希表的负载因子定义为:i = 表中现有数据个数/表的总长度

由于表长是定值,i与表中现有数据个数成正比,所以,负载因子越大,表面填入表中的数据越多,产生冲突的可能性越大,负载因子越小,产生冲突的可能性越小。

对于开放定址法,负载因子必须严格限制在0.7 -- 0.8以下,超过0.8,查表时的CPU的计算效率成指数上升。因此,一些采用开放定址法的hash库,如Java的库限制了负载因子为0.75,超过将resize哈希表。扩容具体实现请看下面哈希表模拟实现部分

2.4.2 开散列扩容

桶的个数是一定的,随着数据的不断插入,每个桶中元素不断增多,极端情况下可能会导致一个桶中的链表节点非常多,影响哈希表的查找效率,所以需要对哈希表进行增容。

最好的情况是,每个桶中刚好有一个节点,再插入数据时,都会发生哈希冲突,所以在数据个数等于桶的个数时,也就是负载因子等于1的适合进行扩容

注:如果实在没办法扩容,但是又有很多值经过哈希函数运算后插入同一个地址,那么可以将桶挂单链表改为挂红黑树。

三,哈希表模拟实现

3.1 映射函数实现

template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//特化 -- 如果是普通类型走上面的,如果是string类型走下面的
template<>
struct HashFunc<string>
{//将字符串变成整数以后非常常见size_t operator()(const string& key){//BKDR算法size_t val = 0;for (auto ch : key){val *= 131;val += ch;}return val;}
};

3.2 闭散列哈希表

enum State//标志位,解决删除带来的老六问题
{EMPTY,EXIST,DELETE
};template<class K, class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};template<class K, class V, class Hash = HashFunc<K>>//仿函数
class HashTable
{
public:bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)//使用负载因子控制扩容{//size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;//vector<HashData> newtables(newsize);遍历旧表,重新映射到新表//for (auto& data : _tables)//{//	if (data._state == EXIST)//	{//		// 重新算在新表的位置//		size_t i = 1;//		size_t index = hashi;//		while (newtables[index]._state == EXIST)//		{//			index = hashi + i;//			index %= newtables.size();//			++i;//		}//		newtables[index]._kv = data._kv;//		newtables[index]._state = EXIST;//	}//}//_tables.swap(newtables);size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;HashTable<K, V> newHT;newHT._tables.resize(newSize);//把空间开好for (auto& e : _tables)//遍历旧表{if (e._state == EXIST){newHT.Insert(e._kv);//直接复用插入}}_tables.swap(newHT._tables);}//线性探测Hash hash;size_t hashi = hash(kv.first) % _tables.size();while (_tables[hashi]._state == EXIST){//找空位置,如果走到结尾了,从头开始找,反正要找到一个空位置//注意不是从头开始找,从映射的值的那个位置开始找hashi++;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_size;线性探测问题:某个位置冲突很多的情况下,互相占用,冲突一篇,效率变低二次探测 -- 按2的i次方进行探测 hash+i^2 (i>=0)//while (_tables[hashi]._state == EXIST)//{//	++i;//	hashi = start + i * i;//	hashi %= _tables.size();//}//tables[hashi].kv = kv;//_tables[hashi].state = EXIST;//++_szie;return true;}HashData<K, V>* Find(const K& key){if (_tables.size() == 0)//表为空,返回空{return nullptr;}Hash hash;size_t start = hash(key) % _tables.size();size_t hashi = start;while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)//要同时判断状态和值{return &_tables[hashi];//找到了,返回该处的地址}else{hashi++;hashi %= _tables.size();if (hashi == start)//极端判断,删一点插入一点break;}}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_size;return true;}else{return false;//要删除的值不存在}}void Print(){for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){printf("[%d:%d]", i, _tables[i]._kv.first);}else{printf("[%d:*]", i);}}cout << endl;}
private:vector<HashData<K, V>> _tables;size_t _size;//表示已经存储了多少个有效数据
};

测试代码: 

void TestHashTable1()
{int a[] = { 1,11,4,15,26,7,44,9 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Print();ht.Erase(4);cout << ht.Find(44)->_kv.first << endl;cout << ht.Find(4) << endl;ht.Print();
}
void TestHashTable2()
{int a[] = { 3, 33, 2, 13, 5, 12, 1002 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(15, 15));if (ht.Find(13)){cout << "13在" << endl;}else{cout << "13不在" << endl;}ht.Erase(13);if (ht.Find(13)){cout << "13在" << endl;}else{cout << "13不在" << endl;}
}
void TestHashTable3()
{string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };HashTable<string, int> countHT;for (auto& str : arr){auto ptr = countHT.Find(str);if (ptr){ptr->_kv.second++;}else{countHT.Insert(make_pair(str, 1));}}
}
void TestHashTable4()
{HashFunc<string> hash;cout << hash("abcd") << endl;cout << hash("bcad") << endl;cout << hash("eat") << endl;cout << hash("ate") << endl;cout << hash("abcd") << endl;cout << hash("aadd") << endl << endl;cout << hash("abcd") << endl;cout << hash("bcad") << endl;cout << hash("eat") << endl;cout << hash("ate") << endl;cout << hash("abcd") << endl;cout << hash("aadd") << endl << endl;
}

3.3 开散列哈希表

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 Hash = HashFunc<K>>
class HashTable
{typedef HashNode<K, V> Node;
public:~HashTable(){/*for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete(cur);cur = next;}_tables[i] = nullptr;}*/for (auto& cur : _tables){while (cur){Node* next = cur->_next;delete cur;cur = next;}cur = nullptr;}}Node* Find(const K& key){if (_tables.size() == 0){return nullptr;}Hash hash;size_t hashi = hash(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){//不能直接用Find找这个节点if (_tables.size() == 0){return nullptr;}//现在这个数组是一个指针数组,里面存的都是指针,由hashi的数字代表位置,_tables[hashi]代表该位置的指针,该指针指向下面的单链表Hash hash;size_t hashi = hash(key) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key)//找到了{if (prev == nullptr)//头删{_tables[hashi] = cur->_next;}else//中间删{prev->_next = cur->_next;}delete cur;--_size;return true;}else//没找到{prev = cur;cur = cur->_next;}}return false;}inline size_t GetNextPrime(size_t prime){//SGIstatic const size_t __stl_num_primes = 28;static const size_t __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};//去上面的一堆素数中找第一个大于n的值for (size_t i = 0; i < __stl_num_primes; ++i){if (__stl_prime_list[i] > prime){return __stl_prime_list[i];}}return -1;}bool Insert(const pair<K, V>& kv){//去重if (Find(kv.first)){return false;}Hash hash;//负载因子到1就扩容if (_size == _tables.size()){//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newTables;//newTables.resize(newSize, nullptr);newTables.resize(GetNextPrime(_tables.size()), nullptr);//利用素数表来扩容//旧表中节点映射移动到新表for (size_t i = 0; i < _tables.size(); i++)//如果复用insert,调用insert,会生成新节点,然后拷贝再释放旧节点,代价太大,所以我们直接把旧节点废物利用,直接把旧节点搞过来{Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = hash(cur->_kv.first) % newTables.size();//通过映射找到新表中对应的位置cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;//往后走}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = hash(kv.first) % _tables.size();//头插Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_size;return true;}size_t Size(){return _size;}//表的长度 -- 有多少个桶的位置size_t TablesSize(){return _tables.size();}//表中已经有多少桶被使用了size_t BucketNum(){size_t num = 0;for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){++num;}}return num;}size_t MaxBucketLenth(){size_t maxLen = 0;for (size_t i = 0; i < _tables.size(); ++i){size_t len = 0;Node* cur = _tables[i];while (cur){++len;cur = cur->_next;}if (len > maxLen){maxLen = len;}}return maxLen;}private:vector<Node*> _tables;size_t _size = 0;//存储的有效数据
};

测试代码:

	void TestHashBucket1(){int a[] = { 3, 33, 2, 13, 5, 12, 1002 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(15, 15));ht.Insert(make_pair(25, 25));ht.Insert(make_pair(35, 35));ht.Insert(make_pair(45, 45));ht.Erase(12);ht.Erase(3);ht.Erase(33);}void TestHashBucket2(){string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };//HashTable<string, int, HashFuncString> countHT;HashTable<string, int> countHT;for (auto& str : arr){auto ptr = countHT.Find(str);if (ptr){ptr->_kv.second++;}else{countHT.Insert(make_pair(str, 1));}}}//字符串转整形算法测试函数void TestHashBucket3(){//HashTable<string, string, HashStr> ht;HashTable<string, string> ht;ht.Insert(make_pair("sort", "排序"));ht.Insert(make_pair("string", "字符串"));ht.Insert(make_pair("left", "左边"));ht.Insert(make_pair("right", "右边"));ht.Insert(make_pair("", "右边"));HashFunc<string> hashstr;cout << hashstr("abcd") << endl;cout << hashstr("bcda") << endl;cout << hashstr("aadd") << endl;cout << hashstr("eat") << endl;cout << hashstr("ate") << endl;}void TestHashBucket4(){int n = 1000000;vector<int> v;v.reserve(n);srand(time(0));for (int i = 0; i < n; ++i){//v.push_back(i);v.push_back(rand() + i);  // 重复少//v.push_back(rand());  // 重复多}size_t begin1 = clock();HashTable<int, int> ht;for (auto e : v){ht.Insert(make_pair(e, e));}size_t end1 = clock();cout << "数据个数:" << ht.Size() << endl;cout << "表的长度:" << ht.TablesSize() << endl;cout << "桶的个数:" << ht.BucketNum() << endl;cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/434911.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

贝拉国际医疗 | 赋能升级生态链,共创健康新格局

中国60岁及以上人口已达全国21.1%&#xff0c;衰老相关疾病正逐年攀升成为重大医疗挑战。人体细胞、组织及器官衰老造成各种疾病&#xff0c;例如&#xff1a;心血管病、糖尿病、骨关节疾病、组织纤维化、神经退行性疾病及各种老年病等。 为了推动健康中国建设&#xff0c;提升…

了解维特比算法:通信系统和自然语言处理中解码的基石

一、介绍 在数字通信和信号处理领域&#xff0c;维特比算法是一种革命性的纠错和解码方法。该算法以 1967 年推出的 Andrew Viterbi 的名字命名&#xff0c;已成为数字通信和自然语言处理领域的基础。本文旨在深入研究维特比算法的复杂性&#xff0c;探讨其理论基础、实际应用以…

linux 基于科大讯飞的文字转语音使用

官方文档地址&#xff1a;离线语音合成 Linux SDK 文档 | 讯飞开放平台文档中心 一、SDK下载 1、点击上面官方文档地址的链接&#xff0c;可以跳转到以下界面。 2、点击“普通版”&#xff0c;跳转到以下界面。 3、点击“下载”跳转到以下界面 4、最后&#xff0c;点击“SDK下…

Nodejs前端学习Day4_fs文件系统模块基础应用之成绩转换

君子应有龙蛇之变&#xff0c;处于木雁之间 文章目录 前言一、fs文件系统模块1.1 判断文件是否读取成功1.2 向指定的文件中写入内容1.2.1 fs.writeFile的语法格式1.2.2 fs.readFile和fs.writeFile的运用——成绩转换 总结 前言 Day3fs开了点头 一、fs文件系统模块 1.1 判断文…

使用sdbg执行smali简单片段解混淆

https://github.com/CalebFenton/simplify/releases/download/v1.3.0/sdbg-0.1.0.jar "C:\Program Files\Java\jre-1.8\bin\java.exe" -jar sdbg-0.1.0.jar smali "Lu/ad;->c()V"其中smali为文件夹名称。 ###### Class p124u.C12414ad (u.ad) .class …

[晓理紫]每日论文分享(有中文摘要,源码或项目地址)--大模型、扩散模型、视觉语言导航

专属领域论文订阅 关注{晓理紫|小李子}&#xff0c;每日更新论文&#xff0c;如感兴趣&#xff0c;请转发给有需要的同学&#xff0c;谢谢支持 如果你感觉对你有所帮助&#xff0c;请关注我&#xff0c;每日准时为你推送最新论文。 分类: 大语言模型LLM视觉模型VLM扩散模型视觉…

【数据分享】2015年泛第三极65国1km分辨率土壤侵蚀强度数据集(免费获取)

土壤数据是在环境、农业、生态等相关研究中都非常常用的数据&#xff01;我们之前发表过一篇介绍土壤数据来源的文章&#xff08;可查看之前推送的文章获悉详情&#xff09;&#xff01; 土壤侵蚀强度是土壤的重要属性&#xff01;本次我们给大家带来的是2015年泛第三极65国1k…

完美解决:Failed to connect to huggingface.co port 443

https://hf-mirror.com/ 域名 hf-mirror.com&#xff0c;用于镜像 huggingface.co 域名。 方法&#xff1a;使用huggingface 官方提供的 huggingface-cli 命令行工具。 pip install -U huggingface_hub export HF_ENDPOINThttps://hf-mirror.com然后是模型下载测试 huggingf…

Backtrader 文档学习-Bracket Orders

Backtrader 文档学习-Bracket Orders 1. 概述 组合订单类型是一个非常宽泛的订单类别&#xff0c;只要brokder支持的订单类型都可以&#xff0c; 包括(Market, Limit, Close, Stop, StopLimit, StopTrail, StopTrailLimit, OCO)。 该功能用于回测&#xff0c;交互broker Brac…

【安卓】不需要魔法使用AuthenticationApp解决Github报2FA双重验证警告的问题

如果你也收到了类似的警告信息&#xff0c;那就一起启用2FA吧​。 背景介绍 Github提供了四种2FA方式&#xff1a; AuthenticatorApp(今天要分享的就是这个)SMS/Text message: 由于SMS不支持国内手机号, 不可用Security keys: 由于该方式需要物理设备等&#xff0c;不好Githu…

《WebKit 技术内幕》学习之十五(6):Web前端的未来

6 Chromium OS和Chrome的Web应用 6.1 基本原理 HTML5技术已经不仅仅用来编写网页了&#xff0c;也可以用来实现Web应用。传统的操作系统支持本地应用&#xff0c;那么是否可以有专门的操作系统来支持Web应用呢&#xff1f;当然&#xff0c;现在已经有众多基于Web的操作系统&…

蓝桥杯——每日一练(简单题)

题目 153是一个非常特殊的数&#xff0c;它等于它的每位数字的立方和&#xff0c;即1531*1*15*5*53*3*3。编程求所有满足这种条件的三位十进制数。&#xff08;水仙花数&#xff09; 解析 一、for循环进行取值再进行判断 二、方法一使用字符串的思想进行各元素的三次方计算再…