比如说一个论坛要实现注册功能,每个人都有昵称(主码)。现在新用户输入昵称或者修改昵称,提交的时候要如何快速检测一下昵称是否被用过?有人使用,则提示一下,没人使用,则提交成功。
此时就是一个key的模型。比如说此时已经有100亿个用户昵称了。
目前有两种方式:
因此针对字符串等更加复杂的类型,能否设计出一个位图一样的key在不在模型且节省空间的数据结构呢?
字符串可以通过BKDR算法变成整数,但是存在哈希冲突。但是位图只有一个位,也没法存指针形成单链表。
布隆解决方式:这个问题不能解决,那能不能降低误判概率?每个值映射一个位容易冲突,那把一个值映射成多个位。
映射多个位,还是可能存在误判,但是误判的概率低了,因为要映射的多个位都被占用冲突,才会误判。(类似字符串的双哈希、多哈希)
布隆过滤器,出发点是节省空间,效率高,一个值映射的位越多,消耗的空间越大。
将哈希与位图结合,即布隆过滤器,布隆过滤器是位图变形和延伸。
重点:
判断昵称用过:存在误判。当前昵称映射的位置可能被其他给占用了。
判断昵称没用过:准确的。
k为哈希函数个数,m为布隆过滤器长度,n为插入的元素的个数,p为误报率。
k = m n l n ( 2 ) k=\frac{m}{n}ln(2) k=nmln(2)
当冲突概率高的时候可以类似控制平衡因子那样对布隆过滤器进行增容。
#pragma once #include"BitSet.h" struct HashBKDR { size_t operator()(const string& s) { size_t value = 0; for (auto ch : s) { value = value * 131 + ch; } return value; } }; struct HashAP { size_t operator()(const std::string& s) { register size_t hash = 0; size_t ch; for (long i = 0; i < s.size(); i++) { ch = s[i]; if ((i & 1) == 0) { hash ^= ((hash << 7) ^ ch ^ (hash >> 3)); } else { hash ^= (~((hash << 11) ^ ch ^ (hash >> 5))); } } return hash; } }; struct HashDJB { size_t operator()(const std::string& s) { register size_t hash = 5381; for (auto ch : s) { hash += (hash << 5) + ch; } return hash; } }; template<size_t N,class K = string ,class Hash1=HashBKDR,class Hash2 = HashAP,class Hash3 = HashDJB> class BloomFilter { public: void Set(const K& key) { size_t i1 = Hash1()(key) % N;//匿名对象 size_t i2 = Hash2()(key) % N; size_t i3 = Hash3()(key) % N; cout << i1 << " " << i2 << " " << i3 << endl; _bitset.Set(i1); _bitset.Set(i2); _bitset.Set(i3); } bool Test(const K& key) { size_t i1 = Hash1()(key) % N;//匿名对象 if (_bitset.Test(i1) == false) { return false; } size_t i2 = Hash2()(key) % N; if (_bitset.Test(i2) == false) { return false; } size_t i3 = Hash3()(key) % N; if (_bitset.Test(i3) == false) { return false; } return true;//这里三个位都在,有可能是其他key占的,是不准确的。 } private: Y::BitSet<N> _bitset; }; void TestBloomFilter() { BloomFilter<100> bf; bf.Set("张三"); bf.Set("李四"); bf.Set("王五"); bf.Set("赵六"); cout << bf.Test("张三") << endl; cout << bf.Test("李四") << endl; cout << bf.Test("王五") << endl; cout << bf.Test("赵六") << endl; cout << bf.Test("苹果") << endl; }
一般情况下布隆过滤器是不能删除的。
但是有些场景可能只能妥协。
布隆过滤器删除的办法是引用计数。
使用多个位标识一个位置,多个值映射时,++计数;删除时,–计数。比如说4位,那么有16个值同时映射这个位置就不行了。整体还是比哈希好。
真的要实现的时候可以简单点,就实现8bit的,vector<char> _bitset
,找位置如果存在就对该位置++,删除就–。
布隆过滤器优点:节省空间(相比于平衡搜索树和哈希表),效率高。
布隆过滤器缺点:存在误判,判断在是不准确的,判断不在是准确的。布隆过滤器一般不支持删除。
布隆过滤器使用的前提:能够容许他的误判。
实际场景中比如危险网站的检测,可以用布隆过滤器判断是否在危险网站集合中。
数据库是存在硬盘上的,假如系统所有用户的电话号码都存储在数据库的用户表。场景:注册新账号的时候在你没提交的时候提示一下这个手机号是否注册过。然而IO是比较慢的,这种场景可以在前端放一个布隆过滤器,里面放数据库的手机号,先布隆里面判在不在,布隆的判断不存在是准确的,直接就可以了。如果手机号在布隆过滤器中,这时候就要去数据库中找。
给一个超过100G大小的log file,log中存着IP地址,设计算法找到出现次数最多的IP地址?
与上题条件相同,如何找到top K的IP?
如何直接用Linux系统命令实现?
对于大文件的思路是类似的,大文件不好处理就用哈希切分成小文件。
哈希切分的特点:相同的记录会进入下标相同的文件中。所以我们直接统计小文件中的次数即可。
假设生成A0~A99的100个文件,依次读取大文件中ip,计算每个ip映射的文件号,i=HashBKDR()(ip)%100,这个ip就进入 A i A_i Ai号小文件。
再依次读取A0~A99,读取Ai文件,如果Ai大于2G,可以类似上面切分一下。如果小于2G,可以直接统计每个小文件map<string,int>统计次数获得最多的。最后多个文件比较出最大的那个。
出现次数最多k个ip,用大堆还是小堆?建k个数的小堆。后面来的比top()大就进堆。
linux系统命令可以直接对文件sort然后count。
位图应用的题也可以哈希切分。
以后简单扩展:一致性哈希,哈希与加密
关于一致性哈希:
在实际开发应用中,实际场景大致是这样的:
这样就要考虑一个问题,找数据的时候服务器怎么知道要找的数据在哪一台机器。每个服务器相当于一个桶,进行编号,那么对于访问的ip进行取模来知道在哪个机器。这样就会同步产生一个问题,当扩容机器或者减少机器的时候,模的关系就发生变化了,怎么处理数据的问题。这就是一致性哈希处理的问题。
关于哈希与加密:密码学方向。