之前我们讲了哈希思想,实际上就是一种映射,就是元素的值和它的位置的映射。这样我们只需要O(1)的时间复杂度就可以找到元素。
我们这里说的位图(bitset)和布隆过滤器也是基于哈希思想实现的,位图就是给一堆数,每个数都映射一个比特位,通过比特位是0还是1来判断这个数在不在,它就是比较快速并且节省空间。
布隆过滤器就是,比如字符串,通过多个不同的哈希函数来计算出多个哈希值,并且将哈希值与上面的位图联系起来,就是映射多个位,这样就会降低冲突的概率。以后在寻找一个字符串在不在时要判断多个位都为一才可以,当然这也是存在误判的,但是概率会大大减小
那我们就简单的来实现一下bitset
#include<iostream> #include<vector> #include<assert.h> using namespace std; template<size_t N=100>//非类型模板参数,我们在模板进阶说过 class bitset { public: bitset() { f.resize(N / 32 + 1, 0); } void set(size_t n) {//将比特位设置为一 assert(n <= N); size_t pos = n / 32; size_t pos1 = n % 32; f[pos] |= (1 << pos1); } void reset(size_t n) {//将比特位设置为零 assert(n <= N); size_t pos = n / 32; size_t pos1 = n % 32; f[pos] &= ~(1 << pos1); } bool testvalue(size_t n) {//检测这个数是否存在 assert(n <= N); size_t pos = n / 32; size_t pos1 = n % 32; return f[pos] & (1 << pos1); } private: vector<int>f; };
其中一些解释我就写在代码里了,我们这个一堆数中最大的数作为N值
那么接下来我就说一说它的一些应用
给四十亿个随机无符号整数,没排过序,然后给一个无符号整数,如何快速的判断这个数是否在这四十亿个数中呢?
我们如何解决这个问题呢?
首先四十亿个数大概占多少内存呢?2的十次方是1024,1000的三次方是十亿,所以2的三十次方就是十亿。从Byte到GB就是10的三十次方,所以1GB是十亿个字节。四十亿个数是160亿个字节,就是16个GB。
我们把这些数据都放到内存中显然是不可能的,所以就利用位图,一个比特位就表示一个数的特点,16GB/32就是512MB,显然占用了更少的内存。这样的话我们就可以解决问题了。
下一个问题是,有两个文件分别有一百亿个整数,我们只有1GB内存,如何找到两个文件的交集
我们size_t的最大值是42亿多,所以一百亿中肯定有重复的,所以我们一个位图占512MB,同时用两个位图,同时为一就是交集
一个文件中有100亿个int,1GB内存,如何找到出现次数不超过2次的所有整数
我们如果只用一个位的话只能表示有或无,两个比特位有四种状态,我们正好需要四种状态(0次,1次,2次,3次及以上),所以我们可以用两个位图,当然也可以再写一个类,让这个类里面有两个位图
template<size_t N=100> class two_bitset { public: void set(size_t n) { if (b2.testvalue(n) == 0)b2.set(n); else { if (b1.testvalue(n) == 0) { b1.set(n); b2.reset(n); } } } bool testvalue(size_t n) { if (b1.testvalue(n) == 1 && b2.testvalue(n) == 1)return false; return true; } private: bitset<N>b1; bitset<N>b2; };
我们这里的位图是只能处理整形的,遇到比如字符串的话也是先转为整形再利用位图,那么如果是字符串就可能导致不同的字符串转为同一个整形值,于是在这种情况下用布隆过滤器通过给定多个哈希函数让它多映射几个位置就可以减小冲突的概率
那我们先来简单的写一个布隆过滤器
#include"bitset.h"
#include<string>
struct HashFuncBKDR
{
// BKDR
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
struct HashFuncAP
{
// AP
size_t operator()(const string& s)
{
size_t hash = 0;
for (size_t 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 HashFuncDJB
{
// DJB
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash = hash * 33 ^ ch;
}
return hash;
}
};
template<size_t N=100,class K=string,class hash1= HashFuncBKDR,class hash2= HashFuncAP,class hash3= HashFuncDJB>
class Bloomfilter {
public:
void set(const K& key) {
size_t h1 = hash1()(key) % (N * 10);
size_t h2 = hash2()(key) % (N * 10);
size_t h3 = hash3()(key) % (N * 10);
_bt.set(h1);
_bt.set(h2);
_bt.set(h3);
}
bool test(const K& key) {
size_t h1 = hash1()(key) % (N * 10);
if (_bt.testvalue(h1) == false)return false;
size_t h2 = hash2()(key) % (N * 10);
if (_bt.testvalue(h2) == false)return false;
size_t h3 = hash3()(key) % (N * 10);
if (_bt.testvalue(h3) == false)return false;
return true;
}
private:
bitset<N * 10> _bt;
};
前面三个类是比较优秀的字符串哈希函数,后面的布隆过滤器是套用的之前写的位图,当然我们这里的检测函数肯定是存在误判的,如果一个字符串算出来的三个哈希值都是别人的,那么就算它不存在也会说它存在,但是有一个哈希值对应的位为0,那么它肯定是不存在的
布隆过滤器有哪些用途呢?比如说我们在游戏中,有的游戏昵称要求玩家之间不能相同,那么当一个玩家设置昵称时要判断这个昵称是否已经有玩家在用,这时就可以先用布隆过滤器看看是否存在,如果不存在,那就是真的不存在,如果存在,又可能是不存在的,所以就再去数据库中查一下,这样就可以快速且准确的进行判断了。
又比如:假设我们有一个网页爬虫系统,需要过滤已访问过的URL,以避免重复访问。这时可以使用布隆过滤器来快速判断一个URL是否已经被访问过。
那么下面还有一个题目:
给两个文件,分别给100亿个字符串,我们只有1GB内存,如何找到两个文件交集,分别给出近似算法和精确算法
我们的近似算法就是用布隆过滤器,先把一个文件放入布隆过滤器,100亿个字符串最多也就通过哈希函数出来42亿个整数,因为无符号整形的最大值也就42亿多。42亿个整数映射到位图也就42/32,一亿多个整数。4亿多个字节。而1GB是10亿多个字节,所以空间是足够的。然后在判断第二个文件中的字符串是否在布隆过滤器中,这样就可以大致的去除掉某些不是交集的字符串
精确算法是,比如两个文件分别叫做A和B,我们对于A和B切分一下,比如说切成1000份小文件,通过哈希函数来切分,又叫哈希切分。就是算出它们的哈希值,再模上1000,得到的值i就是小文件的称号Ai或Bi。这样再对于小文件根据相同称号分别进行处理,这样内存空间就足够了。处理小文件就是分别放到两个set中,set就会去重加排序,有序的找交集就好找多了。但是可能会出现小文件还是过大的情况,小文件过大有两种可能,一种是相同的太多,这样set就可以去重,不用管;一种是哈希值冲突的太多,它们本身并不相同,这样的话就换哈希函数再进行处理,二次哈希切分。