位图
位图的实现
template<size_t N>
class BitSet
{
public:
BitSet()
{
_bits.resize(N / 8 + 1, 0);
}
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_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;
};
位图的应用
//用twobitset统计一亿个单词中只出现一次的值
template<size_t N>
class twobitset
{
public:
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);
}
else
{
//不变
}
}
void Print()
{
for (size_t i = 0; i < N; i++)
{
if (_bs2.test(i))
cout << i << endl;
}
}
private:
BitSet<N> _bs1;
BitSet<N> _bs2;
};
void test_bitset3()
{
twobitset<100> bs;
int a[] = { 3,5,8,99,5,3,99 };
for (auto e : a)
{
bs.set(e);
}
bs.Print();
}
布隆过滤器
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
- 将哈希与位图结合,即布隆过滤器
概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
布隆过滤器的简易实现
struct BKDRHash
{
size_t operator()(const string& s)
{
// BKDR
size_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表示最多会插入多少个数据
template<size_t N,class K=string,
class Hash1=BKDRHash,class Hash2=APHash,class Hash3=DJBHash>
class BloomFilter
{
public:
void set(const K& key)
{
size_t len = N * _X;
size_t hash1 = Hash1()(key) % len;
_bs.set(hash1);
size_t hash2 = Hash2()(key) % len;
_bs.set(hash2);
size_t hash3 = Hash3()(key) % len;
_bs.set(hash3);
cout << hash1 << " " << hash2 << " " << hash3 << " " << endl;
}
bool test(const K& key)
{
size_t len = N * _X;
size_t hash1 = Hash1()(key) % len;
if (!_bs.test(hash1))
{
return false;
}
size_t hash2 = Hash2()(key) % len;
if (!_bs.test(hash2))
{
return false;
}
size_t hash3 = Hash3()(key) % len;
if (!_bs.test(hash3))
{
return false;
}
//判断在是存在误判的,不在是准确的
return true;
}
private:
static const size_t _X = 10;
BitSet<N* _X> _bs;
};
优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算。
缺点
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题。
1G是10亿字节
哈希切分
题目描述
给两个文件 A 和 B,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?
假设一个query30个字节,100亿就是3000亿字节,即300G。一种想法是把A和B按照大小分成1000个文件,那么每个文件就是300MB,然后一个一个比对,但效率太低了。
更好的方法是采用哈希切分,通过hash函数将A和B文件的query转化到编号的小文件中,hashii=hashfunc(query)%1000,放在小文件中以后,最终每个文件中存放的都是 hashi 相同的元素,这里的一个小文件就类似于一个哈希桶,里面的这些元素只有两种可能:重复、冲突。将 A 和 B 文件都采取相同的方式进行划分,最终 A 和 B 中相同的 query 一定会分别进入 Ai 和 Bi 编号相同的小文件。接下来我们只需要去对比 Ai 和 Bi 即可。
问题
哈希切割的话不是平均切割,那就会导致有的小文件比较小,有的比较大,那就有可能存在有的小文件超过了可用内存1G(那其实就是对应的冲突比较多)
那如果存在这样的文件,即分割之后还是比较大的这种,它其实又分为两种情况:
1.在这单个文件中,存在大量重复的query字符串
2.没什么重复值,大部分都是不同的
解决方案
那这两种不同的情况,我们的处理方式也是不同的
先把 Ai 的 query 读到一个 set 里面,如果 set 的 insert 报错抛异常(bad_alloc,内存错误),那么就说明该小文件中大多数 query 都是不相同的(即冲突)。如果能够全部读出来,insert 到 set 里面,那么说明 Ai 里面大部分都是相同的 query。
如果抛异常,说明有大量冲突,就再换一个哈希函数,进行二次切分