> 文章列表 > 哈希->模拟实现+位图应用

哈希->模拟实现+位图应用

哈希->模拟实现+位图应用

致前行路上的人:

                要努力,但不要着急,繁花锦簇,硕果累累都需要过程!

 

目录

1. unordered系列关联式容器

1.1 unordered_map

1.1.1概念介绍:

1.1.2 unordered_map的接口说明

1.2unordered_set

1.3常见面试题oj

2.底层结构

2.1 哈希概念

2.2 哈希冲突

2.3哈希函数

2.4哈希冲突解决

2.4.1闭散列

2.4.2开散列

3.模拟实现

3.1哈希表的改造:

3.2unordered_map模拟实现

3.3unordered_set模拟实现

4.哈希的应用

4.1位图

4.1.1 位图概念

 4.1.2 位图的实现

 4.1.3 位图的应用

4.2哈希切割

4.3布隆过滤器

4.3.1布隆过滤器的提出

4.3.2布隆过滤的概念

 4.3.3布隆过滤器的插入

4.2.4 布隆过滤器的查找

4.2.5 布隆过滤器删除

4.2.6如何选择哈希函数个数和布隆过滤器长度

4.2.6布隆过滤器优点

4.2.7 布隆过滤器缺陷

4.2.8布隆过滤器的应用场景

4.2.9布隆过滤器代码实现

4.2.10布隆过滤器的面试题


1. unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中对unordered_map和unordered_set进行介绍,unordered_multimap和unordered_multiset使用方式类型类似,后面两个相较于前两个,不同的是允许数据重复。

1.1 unordered_map

1.1.1概念介绍:

1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
2. 在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5. unordered_map实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
6. 它的迭代器至少是前向迭代器。

1.1.2 unordered_map的接口说明

1. unordered_map的构造

unordered_map                                构造不同格式的unordered_map对象

2. unordered_map的容量

bool empty() const 检测unordered_map是否为空
size_t size() const  获取unordered_map的有效元素个数

3. unordered_map的迭代器

begin 返回unordered_map第一个元素的迭代器
end  返回unordered_map最后一个元素下一个位置的迭代器
cbegin 返回unordered_map第一个元素的const迭代器
cend  返回unordered_map最后一个元素下一个位置的const迭代器

4. unordered_map的元素访问

operator[]  返回与key对应的value,没有一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶
中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,
将key对应的value返回。

#include <iostream>
#include <string>
#include <unordered_map>int main()
{std::unordered_map<std::string, std::string> mymap;mymap["Bakery"] = "Barbara";  // new element insertedmymap["Seafood"] = "Lisa";    // new element insertedmymap["Produce"] = "John";    // new element insertedstd::string name = mymap["Bakery"];   // existing element accessed (read)mymap["Seafood"] = name;              // existing element accessed (written)mymap["Bakery"] = mymap["Produce"];   // existing elements accessed (read/written)name = mymap["Deli"];      // non-existing element: new element "Deli" inserted!mymap["Produce"] = mymap["Gifts"];    // new element "Gifts" inserted, "Produce" writtenfor (auto& x : mymap) {std::cout << x.first << ": " << x.second << std::endl;}return 0;
}

运行结果:

5. unordered_map的查询

iterator find(const K& key) 返回key在哈希桶中的位置
size_t count(const K& key) 返回哈希桶中关键码为key的键值对的个数

6. unordered_map的修改操作

insert 向容器中插入键值对
erase 删除容器中的键值对
clear 清空容器中有效元素个数
void swap(unordered_map&)  交换两个容器中的元素

7. unordered_map的桶操作

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

1.2unordered_set

unordered_set文档说明

1.3常见面试题oj

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

class Solution {
public:int repeatedNTimes(vector<int>& nums) {size_t N = nums.size() / 2;unordered_map<int,int>m;//统计每个元素出现的次数for(auto& e : nums){m[e]++;}//找到出现N次的元素:for(auto& e : m){if(e.second == N)return e.first;}return -1;}
};

349. 两个数组的交集

class Solution {
public:vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {unordered_set<int>s1(nums1.begin(),nums1.end());unordered_set<int>s2(nums2.begin(),nums2.end());vector<int> result;for(auto& e : s1){if(s2.find(e) != s2.end())result.push_back(e);}return result;}
};

 

 

2.底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

2.1 哈希概念

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

插入元素
        根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
        对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
哈希表(Hash Table)(或者称散列表

例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题 

2.2 哈希冲突

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

发生哈希冲突该如何处理呢?

2.3哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
        ~哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
        域必须在0到m-1之间
        ~哈希函数计算出来的地址能均匀分布在整个空间中
        ~哈希函数应该比较简单

常见哈希函数

1. 直接定址法--(常用)
取关键字的某个线性函数为散列地址:
Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:
Hash(key) = key% p(p<=m),将关键码转换成哈希地址、

2.4哈希冲突解决

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

2.4.1闭散列

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

1. 线性探测

现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

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

        插入
                1.通过哈希函数获取待插入元素在哈希表中的位置
                2.如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,
                使用线性探测找到下一个空位置,插入新元素

        扩容:

         散列表中有一个载荷因子(负载因子)= 散列表元素的个数 / 散列表的长度,载荷因子越大,产生冲突的可能性就越大,载荷因子越小,产生冲突的可能性就越小,但是可能会造成空间浪费,因此,一般建议将载荷因子控制在0.7~0.8之间

        删除
                采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
                会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影
                响。因此线性探测采用标记的伪删除法来删除一个元素。 

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};

线性探测的实现

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& e : key){hash = hash * 131 + e;}return hash;}
};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 ha;size_t hashi = ha(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 ha;size_t hashi = ha(key) % _tables.size();size_t starti = hashi;while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key){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;}return false;}
private:vector<data> _tables;size_t _n = 0;
};

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

2.4.2开散列

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

 

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。 

2. 开散列实现

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& e : key){hash = hash * 131 + e;}return hash;}
};
template<class K,class V>
struct HashNode
{pair<K, V> _kv;HashNode* 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(10);}~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){//负载因子控制在1,超过就扩容if (_n == _tables.size()){/*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*> newTable;newTable.resize(2 * _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) % newTable.size();//头插到新表cur->next = newTable[hashi];newTable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTable);}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;delete  cur;}else{prev->next = cur->next;delete cur;}--_n;return true;}else{prev = cur;cur = cur->next;}}return false;}
private:vector<node*> _tables;size_t _n = 0;
};

3.模拟实现

3.1哈希表的改造:

1. 模板参数列表的改造:

K:关键码类型
T: 不同容器V的类型不同,如果是unordered_map,T代表一个键值对,如果是
unordered_set,T为 K
KeyOfT: 因为V的类型不同,通过value取key的方式就不同

Hash:哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能
取模

2.代码实现:

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& e : key){hash = hash * 131 + e;}return hash;}
};
template<class T>
struct HashNode
{T _data;HashNode<T>* next;HashNode(const T& data):_data(data),next(nullptr){}
};
template<class K,class T,class Hash,class KeyOfT>
class HashTable;
template<class K,class T,class Hash,class KeyOfT>
struct __HTiterator
{typedef HashNode<T> node;typedef __HTiterator<K, T, Hash, KeyOfT> Self;typedef HashTable<K, T, Hash, KeyOfT> HT;node* _node;HT* ht;__HTiterator(node* node, HT* ht):_node(node), ht(ht) {}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}bool operator !=(const Self& s){return _node != s._node;}Self& operator++(){if (_node->next){_node = _node->next;}else{//当前桶走完了,走桶的下一个:KeyOfT kot;size_t hashi = Hash()(kot(_node->_data)) % ht->_tables.size();++hashi;while (hashi < ht->_tables.size()){if (ht->_tables[hashi]){_node = ht->_tables[hashi];break;}else{++hashi;}}if (hashi == ht->_tables.size())_node = nullptr;}return *this;}
};
template<class K,class T,class Hash,class KeyOfT>
class HashTable
{typedef HashNode<T> node;template<class K, class T, class Hash, class KeyOfT>friend struct __HTiterator;
public:typedef __HTiterator<K, T, Hash, KeyOfT> iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]){return iterator(_tables[i], this);}}return iterator(nullptr, this);}iterator end(){return iterator(nullptr, this);}HashTable():_n(0){_tables.resize(10);}~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;}}pair<iterator,bool> Insert(const T& data){KeyOfT kot;iterator it = Find(kot(data));if (it != end())return make_pair(it, false);//负载因子控制在1,超过就扩容if (_n == _tables.size()){/*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*> newTable;newTable.resize(2 * _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()(kot(cur->_data)) % newTable.size();//头插到新表cur->next = newTable[hashi];newTable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTable);}size_t hashi = Hash()(kot(data)) % _tables.size();node* newNode = new node(data);newNode->next = _tables[hashi];_tables[hashi] = newNode;++_n;return make_pair(iterator(newNode, this), true);}iterator Find(const K& key){KeyOfT kot;size_t hashi = Hash()(key) % _tables.size();node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){return iterator(cur, this);}else{cur = cur->next;}}return end();}bool Erase(const K& key){KeyOfT kot;size_t hashi = Hash()(key) % _tables.size();node* prev = nullptr;node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){if (cur == _tables[hashi]){_tables[hashi] = cur->next;delete  cur;}else{prev->next = cur->next;delete cur;}--_n;return true;}else{prev = cur;cur = cur->next;}}return false;}
private:vector<node*> _tables;size_t _n = 0;
};

3.2unordered_map模拟实现

namespace ns
{template<class K, class V,class Hash = HashFunc<K>>class unordered_map{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename HashTable<K, pair<const K, V>, Hash, MapKeyOfT>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator,bool> Insert(const pair<const K, V>& kv){return _ht.Insert(kv);}V& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}iterator Find(const K& key){return _ht.Find(key);}iterator Erase(const K& key){return _ht.Erase(key);}private:HashTable<K, pair<const K,V>, Hash, MapKeyOfT>_ht;};

3.3unordered_set模拟实现

namespace ns
{template<class K,class Hash = HashFunc<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator,bool> Insert(const K& key){return _ht.Insert(key);}iterator Find(const K& key){return _ht.Find(key);}iterator Erase(const K& key){return _ht.Erase(key);}private:HashTable<K, K, Hash, SetKeyOfT>_ht;};

4.哈希的应用

4.1位图

4.1.1 位图概念

关于位图的概念,下面通过一道面试题来进行介绍:

1. 面试题

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。【腾讯】

这道题直观的思路是使用排序+二分的方法,或者是使用红黑树,或者是使用哈希表,但是这三种方法对于40亿个整数,不合适,因为40个整数大概占16GB的空间,而普通计算机提供不了16GB的空间,所以这三种方式都被否决了!

下面介绍一种新的思路,使用位图的方式进行判断。

数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一
个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0
代表不存在。比如:

 4.1.2 位图的实现

namespace ns
{template<size_t N>class bit_set{public:bit_set(){_bits.resize(N / 8 + 1, 0);}void set(size_t x){size_t i = x / 8; //表示在第几个char上size_t j = x % 8; //表示在几个char的bit位上_bits[i] |= (1 << j);}void reset(size_t x){size_t i = x / 8;size_t j = x % 8;_bits[i] &= (~(1 << j));}bool test(size_t x){size_t i = x / 8;size_t j = x % 8;return _bits[i] & (1 << j);}private:vector<char> _bits;};void test_bit_set(){bit_set<-1> bs1;bs1.set(10);bs1.set(30);bs1.set(40);bs1.set(20);bs1.set(1000);bs1.set(1090);bs1.set(4);cout << bs1.test(10) << endl;cout << bs1.test(1) << endl;cout << bs1.test(40) << endl;bs1.reset(40);cout << bs1.test(40) << endl;}
}

 4.1.3 位图的应用

1. 给定100亿个整数,设计算法找到只出现一次的整数?

实现思路:定义两个bit_set的对象,00表示一次都没有出现,01表示出现一次,10表示出现一次以上

template<size_t N>class two_bit_set{public:void set(size_t x){if (!_bs1.test(x) && !_bs2.test(x)) // 00{_bs2.set(x);}else if (!_bs1.test(x) && _bs2.test(x)) //01{_bs1.set(x);_bs2.reset(x);}//10}void PrintOnce(){for (size_t i = 0; i < N; i++){if (!_bs1.test(i) && _bs2.test(i)){cout << i << " ";}}cout << endl;}private:bit_set<N> _bs1;bit_set<N> _bs2;};void test_two_bit_set(){two_bit_set<100> tbs;int a[] = { 3, 5, 6, 7, 8, 9, 33, 55, 67, 3, 3, 3, 5, 9, 33 };for (auto e : a){tbs.set(e);}tbs.PrintOnce();}

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

实现思路:定义两个bit_set的对象,将100一个整数分别设置到两个对象中,然后遍历找,如果同时在两个对象中则说明就是交集

3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整

实现思路:定义两个bit_set的对象,将100一个整数分别设置到两个对象中,00表示一次都没有出现,01表示出现一次,10表示出现2次,11表示出现3次及以上,然后遍历查找出现次数不超过两次的所有整数

4.2哈希切割

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?

实现思路:可以将100G大小的文件,按照哈希切分的方式,分别切到大小为1G的小文件,然后通过map来一个一个统计次数,统计完一个,clear掉map,再统计下一个,当Ai小文件超过1个G的时候,分两种情况,第一种是这个小文件冲突的ip很多,都是不同的ip,大多数都是不重复的,map统计不下,可以换一个字符串哈希函数,递归再切分,进行统计,第二种情况是这个小文件冲突的ip很多,大多数都是重复的map可以统计

如图所示:

直接用map统计,如果是第二种情况,可以统计出来,不会报错,如果是第一种情况,map insert插入失败,相当于new结点失败,可以捕获异常

4.3布隆过滤器

4.3.1布隆过滤器的提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉
那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用
户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那
些已经存在的记录。 如何快速查找呢?
1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
3. 将哈希与位图结合,即布隆过滤器

4.3.2布隆过滤的概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构的多个位置中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

存在是不准确的,因为可能不存在,但是这个位置可能和别人冲突,出现误判

不存在是准确的

 4.3.3布隆过滤器的插入

布隆过滤器是一个bit数组,如图所示:

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为: 

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

4.2.4 布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特
位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为
零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可
能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其
他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

4.2.5 布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也
被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计
数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储
空间的代价来增加删除操作。

注:上面的方法也不能确保一定就能够删除,因为要删除的元素,无法确保是否真正存在在布隆过滤器中

4.2.6如何选择哈希函数个数和布隆过滤器长度

很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

                     k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率

如何选择适合业务的 k 和 m 值呢,这里直接贴一个公式:

4.2.6布隆过滤器优点

1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无

2. 哈希函数相互之间没有关系,方便硬件并行运算
3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

4.2.7 布隆过滤器缺陷

1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再
建立一个白名单,存储可能会误判的数据)
2. 不能获取元素本身
3. 一般情况下不能从布隆过滤器中删除元素
4. 如果采用计数方式删除,可能会存在计数回绕问题

4.2.8布隆过滤器的应用场景

1.不需要一定准确的场景

2.可以在注册昵称的时候判重

如图所示:

当客户端要查询某个id是否存在的时候,首先在布隆过滤器中查找,查找结果为不在就可以直接返回了,如果查找的结果在,就到数据库外设中查找,这种方式就大大提高了查询效率

4.2.9布隆过滤器代码实现

#include<iostream>
#include<string>
#include<vector>
#include<bitset>
#include<stdlib.h>
using namespace std;
namespace ns
{struct BKDRHash{size_t operator()(const string& s){// BKDRsize_t value = 0;for (auto ch : s){value *= 31;value += ch;}return value;}};struct APHash{size_t operator()(const string& s){size_t hash = 0;for (long i = 0; i < s.size(); i++){if ((i & 1) == 0){hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));}}return hash;}};struct DJBHash{size_t operator()(const string& s){size_t hash = 5381;for (auto ch : s){hash += (hash << 5) + ch;}return hash;}};//N是最多存储的元素个数,X:平均存储一个值,开辟X个位template<size_t N,size_t X,class K = string,class HashFunc1 = BKDRHash,class HashFunc2 = APHash,class HashFunc3 = DJBHash>class BloomFilter{public:void set(const K& key){size_t hashi1 = HashFunc1()(key) % (N * X);size_t hashi2 = HashFunc2()(key) % (N * X);size_t hashi3 = HashFunc3()(key) % (N * X);_bs.set(hashi1);_bs.set(hashi2);_bs.set(hashi3);}bool test(const K& key){size_t hashi1 = HashFunc1()(key) % (N * X);if (!_bs.test(hashi1))return false;size_t hashi2 = HashFunc1()(key) % (N * X);if (!_bs.test(hashi2))return false;size_t hashi3 = HashFunc1()(key) % (N * X);if (!_bs.test(hashi3))return false;//前面不存在判读都是准确的,不存在误判return true; //可能会存在误判,映射的几个位置冲突}private:bitset<N* X> _bs;};void test_bloomfilter1(){string str[] = { "猪八戒", "孙悟空", "沙悟净", "唐三藏", \\"白龙马1","1白龙马","白1龙马","白11龙马","1白龙马1" };BloomFilter<10,5> bf;for (auto& str : str){bf.set(str);}for (auto& s : str){cout << bf.test(s) << endl;}cout << endl;srand(time(0));for (const auto& s : str){cout << bf.test(s + to_string(rand())) << endl;}}void test_bloomfilter2(){srand(time(0));const size_t N = 100000;BloomFilter<N,6> bf;std::vector<std::string> v1;std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";for (size_t i = 0; i < N; ++i){v1.push_back(url + std::to_string(i));}for (auto& str : v1){bf.set(str);}// v2跟v1是相似字符串集,但是不一样std::vector<std::string> v2;for (size_t i = 0; i < N; ++i){std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";url += std::to_string(999999 + i);v2.push_back(url);}size_t n2 = 0;for (auto& str : v2){if (bf.test(str)){++n2;}}cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;// 不相似字符串集std::vector<std::string> v3;for (size_t i = 0; i < N; ++i){string url = "zhihu.com";url += std::to_string(i + rand());v3.push_back(url);}size_t n3 = 0;for (auto& str : v3){if (bf.test(str)){++n3;}}cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;}
}

4.2.10布隆过滤器的面试题

1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出
精确算法和近似算法

近似算法:

可以使用布隆过滤器,将一个文件的内容都set到布隆过滤器当中,然后遍历另一个文件依次查找,如果存在就说明是交集。

精确算法:

将两个文件的内容,分别通过哈希切分的方式,切分成一个一个的小文件,然后将小文件放到map中,进行找交集。

切分的时候存在的两个问题:

1.切分完之后的文件大小超过1G,大多数都是不重复的,可以换一个哈希切分函数,递归在切分

2.切分完之后的文件大小超过1G

拔智齿网