1. 位图
1.1 位图的概念
位图,就是用二进制位来表示数据的某种状态,例如判断数据是否存在,二进制位为1说明在,二进制位为0说明不在。位图适用于大量无重复的数据。
1.1 位图的实现
- 例子
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
分析
40亿个整形=160亿个字节≈16G,将16G的数据放到内存,空间显然是不太够的,所以更不用考虑遍历、排序+二分查找、用set存储。既然是判断在不在,就可以不需要存储整形。开2 ^ 32个比特位(2 ^ 29个字节 ≈ 0.5G)标记对应值在不在,在就将该比特位标记为1,不在就将比特位标记为0。
- 模拟实现
namespace zn
{
//非类型模板参数,将要开的bit位数传过来
template<size_t N>
class bitset
{
public:
//将空间开好,不建议扩容
bitset()
{
//记得向上取整,否则空间开辟不够
_v.resize(N / 32 + 1);
}
//set将n映射的位置置为1
void set(size_t n)
{
size_t i = n / 32;//在第i个整形
size_t j = n % 32;//在这个整形的第几个位上面
//如何将某bit位置1
_v[i] |= (1 << j);
}
//reset将n映射的位置置为0
void reset(size_t n)
{
size_t i = n / 32;
size_t j = n % 32;
//如何将某bit位置0
_v[i] &= ~(1 << j);
}
//判断这个数在不在->判断某bit位为1还是为0
bool test(size_t n)
{
size_t i = n / 32;
size_t j = n % 32;
//其他位都为0,只判断第j位是否为1
return _v[i] & (1 << j);
}
private:
vector<int> _v;
};
}
1.3 位图的应用
//1. 给定100亿个整数,设计算法找到只出现一次的整数(整形最多只有42亿多个,所以肯定有重复的)?
//法一:用两个bit位记录出现次数:01(一次),10(两次),11(三次),这样一个整形只能标记16个数据出现次数
//法二:开两个位图,对应bit位表示出现次数:01(一次),10(两次),11(三次)
template<size_t N>
class twobit
{
public:
void set(size_t N)
{
//00 -> 01
if (!_b1.test(N) && !_b2.test())
{
_b2.set(N);
}
//01 -> 10
else if (!_b1.test(N) && _b2.test(N))
{
_b1.set(N);
_b2.reset(N);
}
//超过两次的就没必要记录
}
bool is_once(size_t N)
{
return !_b1.test(N) && _b2.test(N);
}
private:
bitset<N> _b1;
bitset<N> _b2;
};
//2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
//法一:开两个位图,将文件一映射到位图一,将文件二映射到位图二,遍历其中一个文件,如果两个位图对应bit位都为1,说明是交集
//法二:一个文件所有值映射到一个位图,另一个文件判断在不在,出来的交集需要去重
//3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
//与第一题做法类似,用两个位图表示,找出出现次数为0的和出现次数为1的数目,相加即可。
2. 布隆过滤器
如果文件的内容是字符串,怎么办?可以将字符串转换成整形,然后在位图对应位置置为1。但由于无论怎么转换,总会有相同的整形值,然后发生冲突,导致误判。所以就有布隆过滤器。
2.1 概念
布隆过滤器,是用多个哈希函数,将一个数据映射到位图的多个位置,从而实现降低冲突概率。特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”(判断一个值在可能存在误判,因为其他数据的映射位置可能与该值重叠;判断一个值不在是准确的,通过该值得到的映射位置只要有一个为0,说明该值不在)。
2.2 模拟实现
#include<bitset>
#include<string>
using namespace std;
struct BKDRHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for (auto ch : str)
{
hash = hash * 131 + ch;
}
//cout <<"BKDRHash:" << hash << endl;
return hash;
}
};
struct APHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for (size_t i = 0; i < str.size(); i++)
{
size_t ch = str[i];
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
//cout << "APHash:" << hash << endl;
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& str)
{
size_t hash = 5381;
for (auto ch : str)
{
hash += (hash << 5) + ch;
}
//cout << "DJBHash:" << hash << endl;
return hash;
}
};
template<size_t N,class K = string,class HashFunc1 = BKDRHash,class HashFunc2 = APHash,class HashFunc3 = DJBHash>
class BloomFilter
{
public:
//将三个哈希函数映射的位置置1
void Set(const K& key)
{
//HashFunc1()是匿名对象
size_t num1 = HashFunc1()(key) % N;
size_t num2 = HashFunc2()(key) % N;
size_t num3 = HashFunc3()(key) % N;
_bs.set(num1);
_bs.set(num2);
_bs.set(num3);
}
//查询key是否在布隆过滤器
bool Test(const K& key)
{
//只要有一个映射位置为0,说明key不在
if (_bs.test(HashFunc1()(key)%N) == false)
{
return false;
}
if (_bs.test(HashFunc2()(key)%N) == false)
{
return false;
}
if (_bs.test(HashFunc3()(key)%N) == false)
{
return false;
}
//即使三个映射位置都为1,也可能存在误判
return true;
}
private:
bitset<N> _bs;
};
注意
布隆过滤器不支持reset,因为可能会影响到其他字符串的映射(将该位置置为0后,其如果其他字符串的映射位置也是该位置,这会影响以后的查找)。如果硬要支持reset,可以用多个比特位标识一个值作为计数,但这就削弱图和布隆的优势,本来就要处理大量数据,现在空间又变少,还有可能引发计数回绕(0减1后会回到该类型的上限,所有比特位为1)。
2.3 优点和缺点
- 优点
(1)查询和插入效率非常高,时间复杂度是O(K),K取决于哈希函数的个数。
(2)处理大量数据,特别是字符串。
(3)节省空间,空间利用率高。
(4)使用同一组散列函数的布隆过滤器可以进行交、并、差运算。
- 缺点
(1)不能进行删除操作。
(2)可以通过位图判断是否存在,但不能得到元素本身。
(3)存在误判。
2.4 应用场景
// 布隆过滤器的应用
// 题目:给两个文件A和B,分别有100亿个query(查询,每个query所用的字节不一定相同),我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
//
// 思路:创建1000个Ai小文件和1000个Bi小文件(i = 0,1,2,……,999),分别从A和B中读取query,通过哈希映射到Ai和Bi文件中,i = HashFunc(query)%1000,i是多少,query就进入第i个小文件。如果Ai和Bi非空,交集就是Ai和Bi。
//
// 方法:哈希切分:A和B中相同query一定会分别进入Ai和Bi编号相同的小文件。
//
// 问题:找交集,Ai读出来放到一个set,依次读取Bi的query在不在set中,在就是交集并且删掉。但还有一个问题,此方法是哈希切分不是平均切分,如果冲突太多或者相同的query太多,会导致某个Ai文件太大,甚至超过1G,怎么办?
//
// 解决方案:1.先把Ai的query读到一个set,如果set的insert报错抛异常(bad_alloc),那么说明Ai中
// 大多数query都是冲突的,此时只需换一个HashFunc进行二次切分,再读取Bi的query判断在不在set中,在就是交集。如果Ai中query能够全部读出来,说明Ai中大多数query都是相同的,此时放到set已经去重,再读取Bi的query判断在不在set中,在就是交集。
2.5 哈希切分的应用
// 哈希切分
// 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
// 思路:进行哈希切分,i = HashFunc(query)%1000,相同ip一定会进入同一个小文件,再用map
// 去分别统计每一个小文件中ip出现次数,就可以得到ip出现次数最多,或者出现次数最多的前K个。