目录

哈希的概念

哈希函数

哈希冲突和解决方法

闭散列

插入

查找

删除

开散列

插入

查找

删除

哈希表(开散列)整体代码

位图

位图模拟实现思路分析:

位图应用

布隆过滤器


本文介绍unordered系列的关联式容器,unordered_set和unordered_map。它们在使用和功能方面和set/map基本相同它们的底层使用了哈希结构。

哈希的概念

小提问:在红黑树的搜索效率已经很高的情况下为什么库中还要提供unordered_map和unordered_set这样的类似容器,这样不冗余吗?

答:在顺序结构和平衡树中,查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N)。平衡树中为树的高度,即O(logn),搜索的效率取决于搜索过程中元素的比较次数。

map和set的查找效率对于我们而言已经是很满足了,但是依然达不到大佬们的标准,于是它们想构造一种不需要比较,一次直接从表中获取元素的搜索方法。

哈希(散列)方法:元素的存储位置与它的关键码之间能够建立一一映射的关系。​​插入数据时根据key值计算出其存储位置并存放。找数据时对要查询的关键码进行计算,根据计算结果直接找到映射的位置。哈希方法中构造的结构叫做哈希表,使用的转换函数叫做哈希函数!

哈希函数

在哈希方法中,会用到某种函数将关键码和元素存储位置建立一个一一对应的映射,这种函数将其叫做哈希函数。
●哈希函数的定义域必须包括需要存储的全部关键码,如果哈希表中允许有n个地址时,其值域必须在0到n-1之间。

●哈希函数计算出来的地址需要的均匀的分布在整个空间中。

常见的哈希函数:
●直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。
●除留余数法

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

哈希冲突和解决方法

不同关键字通过哈希函数计算出了相同的哈希地址,这种现象叫做哈希冲突。闭散列和开散列是处理哈希冲突的两种常见方法!

闭散列

闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。这样的做法就好比我的“家园”被人占领了,为了“生活”我也要去占领别人的位置。

●将近似整型的类型强转成无符号整数。

template <class K>
struct HashFun
{size_t operator()(const K& key){return (size_t)key;}
};

●特化字符串类型,将字符串中每个字符的ASCII值加在一起。*=31是为了减少冲突。

template <>
struct HashFun<string>
{size_t operator()(const string& str){size_t  num = 0;for (auto ch : str){ch *= 31;num += ch;}return num;}
};

一块空间上的状态存在三种,空、被占用、占用过已删除、基于这三种情况首先定义一个枚举:

    enum State{EMPTY,EXIST,DELETE,};

节点结构:要存储的数据和存储位置状态的标记,标记初始状态为空。

 template <class K,class V>struct HashDate{//存储数据pair<K, V> _kv;//表状态State _state = EMPTY;};

HashTable结构:存储HashDate的容器和一个记录容器中存储有效数据的个数。

 template<class K,class V,class Hash = HashFunc<K>>class HashTable{typedef HashDate<K, V> Data;public:HashTable():_n(0){//有效数据个数初始为10_table.resize(10);}private:vector<Data> _table;size_t _n = 0;};

插入

插入逻辑解读:

●判断该数据是否已经存在于表中,以存在直接返回false。(复用了查找接口)

●新数据不在表中,插入前判断是否需要扩容:

1.当负载因子超标进行一次扩容,负载因子的计算公式:表中元素个数/总空间,当元素个数已经是总容量的70%甚至更多时,进行扩容。

2.按照习惯,通常会将容量扩至原来的二倍,那么扩容后又带来了新的问题,每个数据在新表中的映射位置可能和旧表不同。

3.处理方法,创建一个新的HashTable,将旧表中的数据全部插入到新建的HashTable中,最后来一个“偷梁换柱”,交换新旧表。

4.这里需要说明一下,HashTable的结构中有一个存储数据的vector和一个记录有效数据个数的变量,在局部对象生命周期结束调用其析构时,默认生成的析构就能够完成任务,因为vector会调用其自己的析构函数,内置类型_n销毁时不需要资源清理,最后系统直接将其内存回收即可

●插入部分,这里采用的插入方法是线性探测,如果映射位置为空或者删除状态直接进行插入,如果映射位置被占用,向后继续查找直至找到空的位置,如果查找到末尾还未找到合适位置,此时注意处理Hash_i从头进行查找。

     //插入数据bool insert(const pair<K,V>& kv){if (Find(kv.first)){return false;}//负载因子超标,需要扩容,元素个数/表总空间的个数if (_n * 10 / _table.size() >= 7){cout << "扩容成功" << endl;//需要扩容HashTable<K, V, Hash> newHT;newHT._table.resize(_table.size() * 2);//扩容后存在,数据的位置可能改变for (auto& e : _table){if (e._state == EXIST){newHT.insert(e._kv);}}//交换新旧表_table.swap(newHT._table);}Hash sh;size_t Hash_i = sh(kv.first) % _table.size();//size_t Hash_i = kv.first % _table.size();//如果映射位置的状态不是占用while(_table[Hash_i]._state == EXIST){//向后找空位置++Hash_i;//防止越界Hash_i = Hash_i % _table.size();}//走到这里该位置为空或者被占用过,插入_table[Hash_i]._kv = kv;_table[Hash_i]._state = EXIST;++_n;return true;}

查找

●根据Key值在_table中的映射直接查找。

●如果映射位置为空,说明表中没要要查找的值。

●映射位置被占且该位置的值不是我们要查找的,或者是删除状态,那么从此位置开始向后开始查找,就可以找到我们要查找的值了。

     //查找Data* Find(const K& key){Hash sh;size_t Hash_i = sh(key) % _table.size();while (_table[Hash_i]._state != EMPTY){if (_table[Hash_i]._state == EXIST &&_table[Hash_i]._kv.first == key){return &_table[Hash_i];}++Hash_i;Hash_i %= _table.size();}return nullptr;}

删除

●调用查找接口,判断要删除的数据表中是否存在。

●删除目标存在表中,进行删除:注意这里要做的处理并不是真的删除,而是改变该位置的状态为DELETE,这样一来如果有新的数据插就直接覆盖在这里。

     //删除bool earse(const K& key){Data* ret = Find(key);if (ret == nullptr){return false;}ret->_state = DELETE;_n--;cout << "删除成功" << endl;return true;}

开散列

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

插入

●第一步检查要插入数据是否已经在存在于表中。

●开散列的逻辑结构通过上图你一定有了一些了解,这里的扩容条件是通过对有效元素个数和容量的比较,当二者相等时进行一次扩容:

1.首先需要注意的是,哈希桶结构中表里存的是单链表的头节点,每个单链表可能有多个数据,这时的析构函数就需要显示定义,代码实现参考文章最后的整体实现!

2.按照闭散列的思路去获取旧节点的值,在申请节点插入到新表中也是可以的,但是考虑到旧表的节点没用直接销毁了,新表又需要新的节点,索性将旧表的节点拆下来拿给新表用。

3.节点插入时使用的方法是头插,新节点作为一个“桶”的头节点存储在哈希表中。

        bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}//扩容的情况if (_n == _tables.size()){vector<Node*> newTB;newTB.resize(_tables.size() * 2, 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) % newTB.size();cur->_next = newTB[Hashi];newTB[Hashi] = cur;cur = Next;}_tables[i] = nullptr;}}//节点插入,头插,挂节点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;}cur = cur->_next;}return nullptr;}

删除

删除数据要注意分析几种情况:

1.首先判断映射位置有没有桶。

2.找到对应的桶后,如果该值在头部,直接删除即可。

3.要删除的值在链表中间,需要一个记录节点前一节点的指针prev,找到要删除的节点后,prev->next指向cur->next;

     bool Earse(const K& key){size_t hashi = Hash()(key.first) % _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{Node* Next = cur->_next;prev->_next = Next;}--_n;delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}

哈希表(开散列)整体代码

template <class K>
struct HashFun
{size_t operator()(const K& key){return (size_t)key;}
};
template <>
struct HashFun<string>
{size_t operator()(const string& str){size_t  num = 0;for (auto ch : str){ch *= 31;num += ch;}return num;}
};template <class K, class V>struct HashBucketNode{pair<K, V> _kv;HashBucketNode<K, V>* _next;HashBucketNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}};template <class K, class V, class Hash = HashFun<K>>class HashBucket{typedef HashBucketNode<K, V> Node;public:HashBucket():_n(0){_tables.resize(10);}~HashBucket(){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;}cout << "调用析构函数" << endl;}bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}if (_n == _tables.size()){vector<Node*> newTB;newTB.resize(_tables.size() * 2, 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) % newTB.size();cur->_next = newTB[Hashi];newTB[Hashi] = cur;cur = Next;}_tables[i] = nullptr;}}//节点插入,头插,挂节点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;}cur = cur->_next;}return nullptr;}bool Earse(const K& key){size_t hashi = Hash()(key.first) % _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{Node* Next = cur->_next;prev->_next = Next;}--_n;delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}private://指针数组vector<Node*> _tables;size_t _n = 0;};

位图

位图概念:利用每个比特位来存放某种状态,使用于海量数据、无重复的场景,通常用来判断某个数据存不存在。

位图模拟实现思路分析:

●N表示要处理数据的最大范围,成员方面用一个存储char的容器模拟实现。容器初始大小设置为N/8+1,意义是要处理的数据范围需要多少个char才能够每个数据都有与之对应的映射(用一个比特位来表示一个数据是否存在)。N/8可能会有余数,多开一个char保证空间够用。

template<size_t N>
class BitSet
{
public:BitSet(){_bitset.resize(N/8+1,0);}
private:vector<char> _bitset;
};

●当新数据进入到位图中,i记录该数据x在容器中的哪一个char中,j计算x在这个char中的哪一个比特位。

    void insert(size_t x){size_t i = x / 8;size_t j = x % 8;_bitset[i] |= (1 << j);}

●当要将数据x在位图中去掉时,同样的要知道其在哪一个char中,在该char中的比特位的位置。如果该位置是1将其改为0。

  void earse(size_t x){size_t i = x / 8;size_t j = x % 8;_bitset[i] &= (~(1 << j));}

●查找一个数据是否在位图中有存在标记,首先还是要记录i,j两个位置和上述意义相同。这里的返回值是一个布尔值,如果不存在会返回0,存在会返回一个非0的数据。

   bool Find(size_t x){size_t i = x / 8;size_t j = x % 8;return _bitset[i] & (1 << j);}

整体代码:

​template<size_t N>
class BitSet
{
public:BitSet(){_bitset.resize(N/8+1,0);}void insert(size_t x){size_t i = x / 8;size_t j = x % 8;_bitset[i] |= (1 << j);}void earse(size_t x){size_t i = x / 8;size_t j = x % 8;_bitset[i] &= (~(1 << j));}bool Find(size_t x){size_t i = x / 8;size_t j = x % 8;return _bitset[i] & (1 << j);}
private:vector<char> _bitset;
};

位图应用

给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

分析:首先不要受到题目的迷惑,100亿整数中包括很多重复数据,实际范围是0到0xffffffff。

512M左右就可以表示数据是否存在了。

代码逻辑分析:

用两个位图来放两文件中的数据,用两个位图中的对应位置表示不同的状态:

00表示两文件中都没有某个数据,10表示在一个位图中出现过,01表示在两个位图中都出现了,这种状态也就是交集数据的状态。

代码实现:

template<size_t N>
class twobitset
{
public:void Insert(size_t x){if (!_bit1.Find(x) && !_bit2.Find(x))//00{_bit2.insert(x);}else if (!_bit1.Find(x) && _bit2.Find(x))//01 出现了一次{_bit1.insert(x);_bit2.earse(x);//10 表示出现2次及以上,两个表中都有}}void _Print_one(){//打印值出现一次的for (size_t i = 0; i < N; i++){if (!_bit1.Find(i) && _bit2.Find(i))//01{cout << i << endl;}}cout << endl;}void _Print_more(){//出现两次或以上for (size_t i = 0; i < N; i++){if (_bit1.Find(i) && !_bit2.Find(i))//10{cout << i << endl;}}cout << endl;}
private:BitSet<N> _bit1;BitSet<N> _bit2;
};void Testtwobit()
{twobitset<2000> tb;int a[] = { 520,43,132,454,43,43,1314,88,88,132};for (auto e : a){tb.Insert(e);}cout << "出现一次的数据有:>"<<endl;tb._Print_one();cout << "出现2次及以上的数据有:>"<<endl;tb._Print_more();
}

布隆过滤器

布隆过滤器可以认为是哈希和位图的结合体,当有海量数据需要查询的时候,哈希浪费空间,位图只能处理整型。布隆过滤器避免了它们的短板,即可节省空间,也可以处理字符串,特点也是高效的插入和查找。但是要说明的是,布隆过滤器是一种概率型数据结构,原理是用多个哈希函数,将一个数据映射到位图结构中,用次结构查找数据是,返回的结果如果是不在,那么该数据就一定不存在。如果查找结构是存在,则是可能存在!

●不同的哈希函数返回不同的映射位置:

//在Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得名
struct BKDRHash
{size_t operator()(const string& str){size_t num = 0;for (auto ch : str){ch *= 31;num += ch;}return num;}
};//Arash Partow发明的一种hash算法。
struct APHash
{size_t operator()(const string& key){size_t hash = 0;int i = 0;for (auto ch : key){if ((i & 1) == 0){hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));}++i;}return hash;}
};
//Daniel J.Bernstein教授发明的一种hash算法。
struct DJBHash
{size_t operator()(const string& key){size_t hash = 5381;for (auto ch : key){hash += (hash << 5) + ch;}return hash;}
};
// 由Justin Sobel发明的一种hash算法。
struct JSHash
{size_t operator()(const string& s){size_t hash = 1315423911;for (auto ch : s){hash ^= ((hash << 5) + ch + (hash >> 2));}return hash;}
};

N代表数据的最大范围,X表示存储一个数据需要的比特位数,K表示key的数据类型,不同的哈希函数返回不同的。

如上图所示,通过不同的哈希函数将“love”数据映射到该图中,但是这也埋下了布隆过滤器不能100%确定一个数据存在的原因(及时test返回的结果是肯定的)。看下图:

小总结:布隆过滤器告诉我们一个值不在,就是真的不在。如果它告诉我们一个数据存在,只能代表存在的可能性很大!

template <size_t N, size_t X = 5, class K = string,class HashFun1 = BKDRHash,class HashFun2 = APHash,class HashFun3 = DJBHash,class HashFun4 = JSHash>
class Bloom_Filter
{void set(const K& key){size_t Hash1 = BKDRHash()(key) % (N * X);size_t Hash2 = APHash()(key) % (N * X);size_t Hash3 = DJBHash()(key) % (N * X);size_t Hash4 = JSHash()(key) % (N * X);_bit.set(Hash1);_bit.set(Hash2);_bit.set(Hash3);_bit.set(Hash4);}bool test(const K& key){size_t Hash1 = BKDRHash()(key) % (N * X);size_t Hash2 = APHash()(key) % (N * X);size_t Hash3 = DJBHash()(key) % (N * X);size_t Hash4 = JSHash()(key) % (N * X);//Hash1位置不存在if (!_bit.test(Hash1)){return false;}if (!_bit.test(Hash2)){return false;}if (!_bit.test(Hash3)){return false;}if (!_bit.test(Hash4)){return false;}//上述返回不存在的结果是准确的//四个映射位置都存在,返回true,这个true是不准确的//可能一个数据的四个映射位置都与其它元素冲突return true;}
private:std::bitset<N* X> _bit;
};

布隆过滤器测试

假设你和你的舍友们去取了一些近似的游戏名称一起打“吃鸡游戏”,后台要确保名称不能重复。处理海量玩家昵称的时候就可以使用布隆过滤器进行处理,玩家起的名称布隆过滤器如果返回该昵称以不存在,可以创建,则一定是不存在。如果返回的结果是该昵称已经存在,这个结果是不能相信的,还需要进一步处理,因为是可能在。

void test_bloomfilter1()
{string str[] = { "特战尖刀", "李云龙", "楚云飞", "张大彪", "营长1","营长2","1营长","营11长","1营长1" };Bloom_Filter<10> bf;for (auto& str : str){bf.set(str);}//查找的数据都在位图中cout << "查找在位图中的数据:>"<<endl;for (auto& s : str){cout << bf.test(s) << " ";}cout << endl;//查找的数据不在位图中cout << "查找不在位图中的数据:>"<<endl;srand(time(0));for (const auto& s : str){cout << bf.test(s + to_string(rand())) << " ";}
}

哈希表、位图、布隆过滤器相关推荐

  1. 算法学习 (门徒计划)3-2 哈希表与布隆过滤器及经典问题 学习笔记

    算法学习 (门徒计划)3-2 哈希表与布隆过滤器及经典问题 学习笔记 前言 哈希表 哈希操作 冲突处理 开放定址法 再哈希法 公共溢出区 链式地址法 扩容哈希表 设计简易哈希表 总结 布隆过滤器 对比 ...

  2. 哈希表应用——布隆过滤器

    注:布隆过滤是用来处理海量数据且允许存在误判 目录 布隆过滤器提出 布隆过滤器概念 布隆过滤器的理论知识 布隆过滤器的实现 布隆过滤器的删除 布隆过滤器优点 布隆过滤器缺陷 布隆过滤器的应用场景 哈希 ...

  3. 算法练习day12——190331(哈希函数、哈希表、布隆过滤器、一致性哈希)

    1.哈希函数 1.1 特点: 经典的哈希函数输入域是无穷大的. 输出域是有穷尽的: 相同输入得到的输出肯定是一样的: 不同的输入得到的输出也可能一样(输入域>输出域); 哈希函数的离散型:给定多 ...

  4. 哈希(解决哈希冲突,封装map/set,哈希的应用{位图/布隆过滤器})

    文章目录 如何解决哈希冲突: ①闭散列:也叫开放定址法,如果发送哈希冲突了,就去找下一个空位置 ②开散列 -- 拉链法/哈希桶 封装map和set 哈希表的代码 封装的unordered_set 封装 ...

  5. 位图布隆过滤器海量数据处理

    位图&布隆过滤器&海量数据处理 文章目录 位图&布隆过滤器&海量数据处理 一.位图 二.布隆过滤器 三.海量数据处理 一.位图 1. 位图概念: 所谓位图,就是用每一位 ...

  6. 哈希的应用 -- 布隆过滤器

    作者:@小萌新 专栏:@C++进阶 作者简介:大二学生 希望能和大家一起进步! 本篇博客简介:介绍并模拟实现哈希的应用 – 布隆过滤器 布隆过滤器 布隆过滤器的提出 布隆过滤器的概念 布隆过滤器的实现 ...

  7. 哈希的应用 —— 布隆过滤器

    文章目录 布隆过滤器的提出 布隆过滤器的概念 布隆过滤器的实现 布隆过滤器的插入 布隆过滤器的查找 布隆过滤器的删除 布隆过滤器的优点 布隆过滤器的缺陷 布隆过滤器使用场景 布隆过滤器的提出 在注册账 ...

  8. 哈希的应用 -- 布隆过滤器与海量数据处理

    文章目录 布隆过滤器概念 布隆过滤器设计思路 布隆过滤器的应用 布隆过滤器模拟实现 布隆过滤器的基本框架 布隆过滤器的插入 布隆过滤器的探测 布隆过滤器的删除 布隆过滤器优点 布隆过滤器缺陷 布隆过滤 ...

  9. C++ 第十节 ——哈希 unordered_map/unordered_set的封装 位图 布隆过滤器 海量数据处理

    我们本节继续讲解STL里的内容. 哈希表又叫做散列表,它可以用来封装unordered_map和unordered_set. 我们还是按照老规矩来,先介绍其用法,再介绍其原理,并引到哈希上来. 目录 ...

  10. 高级数据结构——海量数据(位图,布隆过滤器)

    目录 位图 位图介绍 位图代码实现 布隆过滤器 布隆过滤器介绍 布隆过滤器代码实现 布隆过滤器的优缺点 海量数据相关题目 位图 位图介绍 位图(bitmap),适用于存储海量,非负整数,不重复的数据. ...

最新文章

  1. SLAM Scan-Matching/Registration
  2. iOS:quartz2D绘图(给图形绘制阴影)
  3. jQuery遇到问题的小记
  4. JSP简单练习-获取表单数据
  5. 学习ASP.NET-初探Application状态
  6. OpenCV FLANN在数据集中搜索查询图片的实例(附完整代码)
  7. 【NetApp】NetBoot的使用方法
  8. SAP CRM和Cloud for Customer里客户主数据的层级维护
  9. 分享个 之前写好的 android 文件流缓存类,专门处理 ArrayList、bean。
  10. SQL Server:查找周开始和结束日期时间
  11. 查看防火墙_在 Centos8 上使用 systemctl 防火墙服务
  12. 关于FLEX中找不到目标对象或通道未定义错误
  13. 抢占云安全管理高地 启明星辰先发云SOC
  14. docker阿里云加速器
  15. Express全系列教程之(十):渲染jade模板引擎
  16. C#【多线程篇】BackgroundWorker类使用总结
  17. 富士通推出区块链技术驱动的数据存储系统(GBCAX)
  18. 流式布局之京东首页案例
  19. linux上ftp上传文件失败
  20. 麻辣烫有几种类型?不同种麻辣烫怎么做

热门文章

  1. 从理发流程想到的组合设计模式
  2. 3 数字中台之企业中台
  3. 风电机组的预测性维护应该如何进行?
  4. android 2.6.32 SOUND 移植 OMAP3630
  5. 周立功: 我的25年嵌入式生涯(转)
  6. 2015校招求职之路
  7. 中国石油大学计算机专业论文,中国石油大学(华东) 本科毕业设计(论文)管理规定(修订)...
  8. 计算麦穗表面积示例图
  9. springboot工程修改名字
  10. 人品是可以转化为现金的。