目录
一. 位图
例题一
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:
首先40亿个不重复的值意味着我们需要开42亿多个比特位,因为整型最大取值范围就是42亿多,我们开的不是个数。而是范围!能够保证映射出所有数的范围~即使只给了1个值我们也是按照最大范围的比特位来开~
我们以int整型(4字节)来进行划分
测试代码
namespace lj
{
template<size_t N>
class bitset
{
public:
bitset()
{
//划分整型数据
_bits.resize(N / 32 + 1, 0);
}
//把x映射的位置标记为1
void set(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_bits[i] |= (1 << j);
}
// 把x映射的位标记成0
void reset(size_t x)
{
assert(x <= N);
size_t i = x / 32;
size_t j = x % 32;
_bits[i] &= ~(1 << j);
}
bool test(size_t x)
{
assert(x <= N);
size_t i = x / 32;
size_t j = x % 32;
//不改变原先标记,只看结果
return _bits[i] & (1 << j);
}
private:
vector<int> _bits;
};
void test_bitset()
{
int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
bitset<100> bs1;
for (auto e : a)
{
bs1.set(e);
}
bs1.reset(1);
bs1.reset(4);
for (size_t i = 0; i < 100; i++)
{
if (bs1.test(i))
{
cout << i << "->" << "在" << endl;
}
else
{
cout << i << "->" << "不在" << endl;
}
}
}
}
完成后我们就可以去读取数据,然后用test去检查我们需要查找的数值是否在读取的数据之中~
例题二
给定100亿个整数,设计算法找到只出现一次的整数~
测试代码
template<size_t N>
class two_bitset
{
public:
//把x映射的位置标记为1
void set(size_t x)
{
// 00 -> 01 出现一次
if (_bs1.test(x) == false && _bs2.test(x) == false)
{
_bs2.set(x);
}
// 01 -> 10 出现两次
else if (_bs1.test(x) == false && _bs2.test(x) == true)
{
_bs1.set(x);
_bs2.reset(x);
}
// 10 -> 11 出现三次及其以上
else if (_bs1.test(x) == true && _bs2.test(x) == false)
{
_bs2.set(x);
}
}
int test(size_t x)
{
if (_bs1.test(x) == false && _bs2.test(x) == false)
{
return 0;
}
else if (_bs1.test(x) == false && _bs2.test(x) == true)
{
return 1;
}
else if (_bs1.test(x) == true && _bs2.test(x) == false)
{
return 2;
}
else
{
return 3;
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
void test_bitset2()
{
int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
two_bitset<100> bs;
for (auto e : a)
{
bs.set(e);
}
for (size_t i = 0; i < 100; i++)
{
cout << i << "->" << bs.test(i) << endl;
/*if (bs.test(i))
{
cout << i << endl;
}*/
}
}
下面是只出现1次的数据~
例题三
给定100亿个整数,设计算法找到出现次数不超过2次的整数~
例题四
给两个文件,分别有100亿个整数,只有1G内存,如何找到两个文件交集?
扩展
二. 布隆过滤器
前情提要:字符串映射为整型,再由整型映射到比特位的流程中第一次映射就有可能发生冲突~
当我们要判断”利害“该字符串是否存在时会因为与”厉害“发生冲突导致判定其存在,但存在的是”厉害“而非”利害“,这种冲突是哈希函数映射偶然重合,是无法避免的~因为字符串是无限的,而整型最多也才42亿多个,所以我们只能减少这种冲突~
我们设置让每个字符串进行多次映射,虽然这样也会出现映射冲突,但检验的门槛变高了,例如我们想要知道”美团“是否存在,那么就需要经过三重映射认证,但凡有一层认证失败都能证明其不存在,虽然还是会有刚好全部冲突的可能性,但概率比之前是下降好多了~
#include <bitset> #include <string> //三种不同的哈希映射函数 struct HashFuncBKDR { size_t operator()(const string& s) { size_t hash = 0; for (auto ch : s) { hash *= 131; hash += ch; } return hash; } }; struct HashFuncAP { 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 { size_t operator()(const string& s) { size_t hash = 5381; for (auto ch : s) { hash = hash * 33 ^ ch; } return hash; } }; template<size_t N,class K = string,class Hash1 = HashFuncBKDR,class Hash2 = HashFuncAP,class Hash3 = HashFuncDJB> class BloomFilter { public: private: static const size_t M = 10 * N; bitset<M> _bs; };
通过上述关系式子我们可以推出布隆过滤器的大致长度,这里我们多开点空间(10*N)~
代码:
#include <bitset> #include <string> //三种不同的哈希映射函数 struct HashFuncBKDR { size_t operator()(const string& s) { size_t hash = 0; for (auto ch : s) { hash *= 131; hash += ch; } return hash; } }; struct HashFuncAP { 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 { size_t operator()(const string& s) { size_t hash = 5381; for (auto ch : s) { hash = hash * 33 ^ ch; } return hash; } }; template<size_t N,class K = string,class Hash1 = HashFuncBKDR,class Hash2 = HashFuncAP,class Hash3 = HashFuncDJB> class BloomFilter { public: void Set(const K& key) { //匿名对象构造 size_t hash1 = Hash1()(key) % M; size_t hash2 = Hash2()(key) % M; size_t hash3 = Hash3()(key) % M; _bs.set(hash1); _bs.set(hash2); _bs.set(hash3); } bool Test(const K& key) { //对目标字符串进行三重认证 size_t hash1 = Hash1()(key) % M; if (_bs.test(hash1) == false) return false; size_t hash2 = Hash2()(key) % M; if (_bs.test(hash2) == false) return false; size_t hash3 = Hash3()(key) % M; if (_bs.test(hash3) == false) return false; return true; // 虽然认证成功但还是会存在误判(有可能3个位都是跟别人冲突的,所以误判) } private: static const size_t M = 10 * N; bitset<M> _bs; }; void TestBloomFilter1() { string str[] = { "美团","饿了么","拼多多" }; BloomFilter<10> bf; for (auto& s : str) { //标记 bf.Set(s); } for (auto& s : str) { //测试是否存在 cout << bf.Test(s) << endl; } for (auto& s : str) { //测试是否冲突 cout << bf.Test(s + 'a') << endl; } //测试是否冲突 cout << bf.Test("每团") << endl; cout << bf.Test("美鷒") << endl; }
布隆过滤器应用场景
有了布隆过滤器可以快速判断该用户名不存在并返回结果,若该用户名存在则进入数据库进行下一步的验证查看是否误判最后返回结果~
不过法二还有问题需要注意:
之所以划分为1000份是为了避免太多字符串映射到同一文件下导致该文件超出1G,不过就算是这样也会有超出1G的风险~
这时候就分为两种情况:
- 如果是因为出现大量重复而导致的内存溢出,那我们可以通过容器set来去重减小其文件大小~
- 如果不是因为大量重复就单纯极端情况映射过多,那就再对Ai和Bi文件换个哈希函数进行切分
布隆过滤器优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器缺点
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题