文章目录

  • 一、unordered系列关联式容器
  • 二、哈希概念
  • 三、哈希冲突
  • 四、哈希函数
  • 五、解决哈希冲突
    • 1.闭散列——开放定址法
    • 2.代码实现
    • 3.开散列——开链法
    • 4.代码实现
  • 六、结语

一、unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同 :unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构

unordered_set:

与set的区别在于不支持方向迭代器,只是单向迭代器,其他接口基本与set相似

int main()
{unordered_set<int> us;us.insert(10);us.insert(1);us.insert(10);us.insert(3);us.insert(4);us.insert(4);auto it = us.begin();while (it != us.end()){cout << *it << " ";it++;}cout << endl;return 0;
}

无序+去重

unordered_map:

迭代器也是单向迭代器,其他也基本与map相似

int main()
{unordered_map<string, int> countMap;string arr[] = { "苹果","香蕉","苹果" };for (auto& e : arr){auto it = countMap.find(e);/*if (it == countMap.end()){countMap.insert(make_pair(e, 1));}else{it->second++;}*/countMap[e]++;}for (auto& kv : countMap){cout << kv.first << ":" << kv.second << endl;}return 0;
}

unordered_map的桶操作

函数声明 功能介绍
size_t bucket_count()const 返回哈希桶中桶的总个数
size_t bucket_size(size_t n)const 返回n号桶中有效元素的总个数
size_t bucket(const K& key) 返回元素key所在的桶号

实际运用:

在长度 2N 的数组中找出重复 N 次的元素

给你一个整数数组 nums ,该数组具有以下属性:

nums.length == 2 * n.
nums 包含 n + 1 个 不同的 元素
nums 中恰有一个元素重复 n 次
找出并返回重复了 n 次的那个元素。

class Solution {
public:int repeatedNTimes(vector<int>& nums) {unordered_map<int,int> CountMap;//统计次数for(auto& e:nums){CountMap[e]++;}//符合条件for(auto&kv:CountMap){if(kv.second==nums.size()/2)return kv.first;}return -1;}
};

insert\find\erase性能比较:随机数下unordered系列效率更高,但是有序数的情况下就不行了

int main()
{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());}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();for (auto e : v){us.insert(e);}size_t end2 = clock();cout << "unordered_set insert:" << end2 - begin2 << endl;size_t begin3 = clock();for (auto e : v){s.find(e);}size_t end3 = clock();cout << "set find:" << end3 - begin3 << endl;size_t begin4 = clock();for (auto e : v){us.find(e);}size_t end4 = clock();cout << "unordered_set find:" << end4 - begin4 << endl;cout << s.size() << endl;cout << us.size() << 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;return 0;
}


二、哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

哈希映射:key值跟存储位置建立关联关系

当向该结构中插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小:比如数据集合{1,7,6,4,5,9}

三、哈希冲突

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快问题。但是当插入元素44,会出现哈希冲突

哈希冲突:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 ,比如44%10=4,但是4的位置已经被占用了。


四、哈希函数

如果哈希函数设计的不够合理就会引发哈希冲突。

哈希函数设计原则:

哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单

常见哈希函数

  1. 直接定制法–(常用)
    取关键字的某个线性函数为散列地址:HashKey= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况
  2. 除留余数法–(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:**Hash(key) = key% p(p<=m),**将关键码转换成哈希地址
  3. 平方取中法–(了解)
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
  4. 折叠法–(了解)
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
  5. 随机数法–(了解)
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。通常应用于关键字长度不等时采用此法
  6. 数学分析法–(了解)
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突


五、解决哈希冲突

解决哈希冲突两种常见的方法是:闭散列和开散列

1.闭散列——开放定址法

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

  • 线性探测

从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

插入:通过哈希函数获取待插入元素在哈希表中的位置

删除 :采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索 ,比如上述例子中,如果删除27,此时要在找38,会发现在搜索过程就遇到了空,影响到了38的查找。解决方案:线性探测采用标记的伪删除法来删除一个元素 ,给每个位置加一个状态标识

在有限的空间内,随着我们插入的数据越来越多,冲突的概率也越来越大,查找效率越来越低,所以闭散列的冲突表不可能让它满了,所以引入了负载因子:

负载因子/载荷因子:等于表中的有效数据个数/表的大小,衡量表的满程度,在闭散列中负载因子不可能超过1(1代表满了)。一般情况下,负载因子一般在0.7左右。负载因子越小,冲突概率也越小,但是消耗的空间越大,负载因子越大,冲突概率越大,空间的利用率越高。

当负载因子大于0.7的时候就需要进行扩容了:扩容不能进行直接拷贝,映射的位置会随空间大小发生变化,所以需要重新计算映射的位置.

线性探测优点:逻辑简单,实现也简单
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,直到找到空为止,导致搜索效率降低

  • 二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找(start+i),二次探测为了避免该问题,找下一个空位置的方法为:以i的2次方去进行探测(start+i^2):

但是本质上还是没有解决问题,占用别人的空间

2.代码实现

#include <vector>
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 ch : key){hash *= 131;//顺序?abc,cbahash += ch;}return hash;}
};
//闭散列
namespace closehash
{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{typedef HashData<K, V> Data;public:HashTable():_n(0){_tables.resize(10);}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;if (_n * 10 / _tables.size() >= 7){HashTable<K, V, Hash> newHT;newHT._tables.resize(_tables.size() * 2);for (auto& e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}_tables.swap(newHT._tables);}Hash hf;size_t hashi = hf(kv.first)% _tables.size();while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}Data* Find(const K& key){Hash hf;size_t hashi = hf(key) % _tables.size();size_t starti = hashi;while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST && key == _tables[hashi]._kv.first){return &_tables[hashi];}++hashi;hashi %= _tables.size();//if (hashi == starti){break;}}return nullptr;}bool Erase(const K& key){Data* ret = Find(key);if (ret){ret->_state = DELETE;--_n;return true;}else{return false;}}private:vector<Data> _tables;size_t _n = 0;};void TestHT1(){HashTable<int, int> ht;int a[] = { 18, 8, 7, 27, 57, 3, 38, 18 };for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(17, 17));ht.Insert(make_pair(5, 5));cout << ht.Find(7) << endl;cout << ht.Find(8) << endl;ht.Erase(7);cout << ht.Find(7) << endl;cout << ht.Find(8) << endl;}void TestHT2(){string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };//HashTable<string, int, HashFuncString> countHT;HashTable<string, int> countHT;for (auto& e : arr){HashData<string, int>* ret = countHT.Find(e);if (ret){ret->_kv.second++;}else{countHT.Insert(make_pair(e, 1));}}HashFunc<string> hf;cout << hf("abc") << endl;cout << hf("bac") << endl;cout << hf("cba") << endl;cout << hf("aad") << endl;}
}

代码需注意的点:

1.仿函数:考虑到统计出现次数:因为字符串不能够取模,所以我们可以给HashTable增加一个仿函数Hash,其可以将不能取模的类型转成可以取模的类型,同时把string特化出来解决字符串不能取模的问题

2.字符串哈希求法:考虑到顺序问题,比如abc,cba,如果只乘以131则结果是相同的,所以我们可以加上ch在乘以131

3.开散列——开链法

开散列:开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素,不一定要有序

开散列增容问题:

由于桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容。

开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突。

所以在元素个数刚好等于桶的个数时,可以给哈希表增容 。研究分析表明:素数作为哈希表的长度可以尽可能减小哈希冲突。所以可提前定义一个素数表。

4.代码实现

//开散列
namespace buckethash
{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():_n(0){_tables.resize(__stl_next_prime(0));}~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;}}bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}if (_tables.size() == _n){//消耗?/*HashTable<K, V, Hash> newHT;newHT._tables.resize(_tables.size() * 2);for (auto cur : _tables){while (cur){newHT.Insert(cur->_kv);cur = cur->_next;}}_tables.swap(newHT._tables);*/vector<Node*> newTables;newTables.resize(__stl_next_prime(_tables.size()), nullptr);for (size_t i = 0; i < _tables.size(); i++){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;++_n;return true;}Node* Find(const K& key){size_t hashi = Hash()(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}else{cur = cur->_next;}}return nullptr;}bool Erase(const K& key){size_t hashi = Hash()(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (cur == _tables[hashi]){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_n;return true;}else{prev = cur;cur = cur->_next;}}return false;}inline unsigned long __stl_next_prime(unsigned long n){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};for (int i = 0; i < __stl_num_primes; ++i){if (__stl_prime_list[i] > n){return __stl_prime_list[i];}}return __stl_prime_list[__stl_num_primes - 1];}private:vector<Node*> _tables;size_t _n = 0;};void TestHT1(){HashTable<int, int> ht;int a[] = { 18, 8, 7, 27, 57, 3, 38, 18,17,88,38,28 };for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(5, 5));ht.Erase(17);ht.Erase(57);}void TestHT2(){string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };//HashTable<string, int, HashFuncString> countHT;HashTable<string, int> countHT;for (auto& e : arr){auto ret = countHT.Find(e);if (ret){ret->_kv.second++;}else{countHT.Insert(make_pair(e, 1));}}}}


六、结语

开散列与闭散列比较
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间

【C++】哈希——unordered系列容器|哈希冲突|闭散列|开散列相关推荐

  1. 【c++】哈希---unordered容器+闭散列+开散列

    1.unordered系列关联式容器 在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也 ...

  2. python leetcode_七十五、Python | Leetcode哈希表系列

    @Author:Runsen @Date:2020/7/3 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  3. 【C++】哈希(闭散列,开散列)

    哈希 unordered系列关联式容器 unordered_map&& unordered_set 关联性容器介绍 介绍 底层结构 哈希概念 哈希冲突 闭散列 闭散列介绍 实现 开散列 ...

  4. C++:哈希(闭散列、开散列)

    文章目录 哈希概念 哈希冲突 哈希函数 哈希冲突解决 闭散列 什么时机增容,如何增容? 线性探测的实现 开散列 开散列增容 开散列的实现 开散列与闭散列比较 unordered_map模拟实现(应用开 ...

  5. C++---哈希闭散列与开散列

    产生原因 在顺序结构或树形结构的数据集合中,我们想要查询一个元素时,必须进行遍历,所以顺序结构的查询时间复杂度为O(N),树形结构查询的时间复杂度为O(log2^N),但是我们想要一种不用遍历就知道其 ...

  6. 七十五、Python | Leetcode哈希表系列

    @Author:Runsen @Date:2020/7/3 人生最重要的不是所站的位置,而是内心所朝的方向.只要我在每篇博文中写得自己体会,修炼身心:在每天的不断重复学习中,耐住寂寞,练就真功,不畏艰 ...

  7. IPFS系列 - 分布式哈希表(DHT)

    分布式账本技术(DLT)的分支 有向无环图(DAG) 哈希图(Hashgraph) 分布式散列表(DHT) 区块链(Blockchain) DHT DHT的全称是Distributed Hash Ta ...

  8. SHA3系列(KECCAK)哈希算法原理及实现(附源码)

    相关文章: (本文持续更新中) SHA3系列(KECCAK)哈希算法原理及实现(附源码) SHA512系列哈希算法原理及实现(附源码) SHA224和SHA256哈希算法原理及实现(附源码) 国密SM ...

  9. Comunion 区块链深度学习系列|哈希碰撞原理

    Comunion 是一个去中心化的(DAO) 组织协作网络,提供面向数字时代的全新商业基础设施和价值转化机制,致力于让劳动价值 像 资本一样自由流通.交易和积累. 本系列内容包含:基本概念及原理.密码 ...

最新文章

  1. JSP中的重定向和请求转发以及它们的区别
  2. oracle date类型,向Oracle中插入date数据类型
  3. 浪潮存储双活方案:新疆道路运输管理局的大数据应用不再是梦
  4. 在.net 2.0 中执行分布式事务:隐式事务篇(SQL Server 与 Oracle)
  5. C语言——选择法排序_数组
  6. 漏洞payload 靶机_学生会私房菜【20200707期】Wordpress 4.6 任意命令执行漏洞
  7. http下载异常_百度网站抓取异常的原因有哪些?有什么影响和解决方法?
  8. 关于vite2.0和vue/cli建立项目的区别
  9. 永中集成Office:既是创新产品,也是战略储备
  10. 牛顿插值法C语言实现
  11. postgresql查看锁表的进程
  12. 概率论和数理统计知识点总结
  13. flv转mp4,电脑视频flv怎么批量转mp4格式
  14. 郑捷《机器学习算法原理与编程实践》学习笔记(第四章 推荐系统原理)(一)推荐系统概述...
  15. awk OFS问题说明
  16. vuejs开发H5页面总结
  17. jmeter调试取样器
  18. react 生命周期详解
  19. React的React.FC与React.Component
  20. 计算机cad考试试题及答案,2017职称计算机CAD练习试题及答案

热门文章

  1. IIS服务器的安全保护措施
  2. 查看电脑连接的WiFi密码的两种方法
  3. LruCache缓存方法
  4. LRUCache简单实现
  5. HFM深入技术学习系列之二--规则
  6. matlab多元回归分析怎么计算,matlab在多元线性回归分析中的相关计算
  7. Swift的一次函数式之旅
  8. SS00027.algorithm——|ArithmeticMachine.v27|——|Machine:项目实战.v04|竞争分析|
  9. JavaScript 中的强制类型转换
  10. CrashLoopBackOff