位图
首先我们来看一道腾讯的面试题:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
那么你会想到哪些解决方法呢?
- 遍历,时间复杂度 O(N)
- 排序 O(N * logN),利用二分查找 O(logN)
上面两种方法真的可以解决吗?40亿个不重复的无符号整数在内存中占多大空间呢?
232大概是42亿,4G空间大小,一个整数占4个字节,那就是16G的空间大小,实际上我们电脑的内存没有这么大。
那是否可以采用哈希进行映射处理呢?
可以开232个空间,对所有数直接定址法建立映射关系, 这样依然是16G的空间,内存不够用。
因此我们需要改进,可以开232个比特位,每一个比特位来标记一个数字,这样就只用了500M的空间大小。
位图解决:
数据是否在给定的整型数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
位图的实现:
namespace BitSet
{
class bitset
{
public:
bitset(size_t N)
{
_bs.resize(N / 32 + 1, 0);
_num = 0;
}
//将某一位设置成1
void set(size_t x)
{
size_t index = x / 32; // 算出映射的位置在第几个整型
size_t pos = x % 32; // 算出x在整型的第几个位
_bs[index] |= (1 << pos); // 第pos个位置成1
}
//将某一位设置成0
void reset(size_t x)
{
size_t index = x / 32; // 算出映射的位置在第几个整型
size_t pos = x % 32; // 算出x在整型的第几个位
_bs[index] &= ~(1 << pos); // 第pos个位置成0
}
// 判断x在不在(也就是说x映射的位是否为1)
bool test(size_t x)
{
size_t index = x / 32; // 算出映射的位置在第几个整型
size_t pos = x % 32; // 算出x在整型的第几个位
return _bs[index] & (1 << pos);
}
private:
std::vector<int> _bs;
size_t _num;
};
void test_bitset()
{
bitset bs(100);
bs.set(99);
bs.set(98);
bs.set(97);
bs.set(5);
bs.reset(98);
for (size_t i = 0; i < 100; ++i)
{
printf("[%d]:%d\n", i, bs.test(i));
}
}
}
布隆过滤器
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:只能处理整型,对于字符串不能处理
- 将哈希与位图结合,即布隆过滤器
我们呢可以采用字符串哈希算法,先将字符串转换成整型,再使用位图进行处理。但是这样仍然不能解决数据冲突的问题,不同的字符串转换出来的整型可能会相同。因此只能尽可能的想办法减少冲突的概率,可以多开几个位图空间,每一条数据分别用不同的字符串哈希算法转换得到不同的整型数字,再分别取给每个位图中标记处理,这样很好的减少了数据冲突的概率。
布隆过滤器的实现:
#include <iostream>
#include "bitset.hpp"
#include <string>
using namespace std;
namespace BloomFilter
{
struct HashStr1
{
// BKDR
size_t operator()(const std::string& str)
{
size_t hash = 0;
for (size_t i = 0; i < str.size(); ++i)
{
hash *= 131;
hash += str[i];
}
return hash;
}
};
struct HashStr2
{
// RSHash
size_t operator()(const std::string& str)
{
size_t hash = 0;
size_t magic = 63689; // 魔数
for (size_t i = 0; i < str.size(); ++i)
{
hash *= magic;
hash += str[i];
magic *= 378551;
}
return hash;
}
};
struct HashStr3
{
// SDBMHash
size_t operator()(const std::string& str)
{
size_t hash = 0;
for (size_t i = 0; i < str.size(); ++i)
{
hash *= 65599;
hash += str[i];
}
return hash;
}
};
template <class K = std::string, class Hash1 = HashStr1, class Hash2 = HashStr2, class Hash3 = HashStr3>
class bloomfilter
{
public:
//查阅资料知需要开数据量的4~5倍左右空间
bloomfilter(size_t num)
: _bs(5 * num)
, _N(5 * num)
{}
void set(const K& key)
{
size_t index1 = Hash1()(key) % _N;
size_t index2 = Hash2()(key) % _N;
size_t index3 = Hash3()(key) % _N;
//cout << index1 << endl;
//cout << index2 << endl;
//cout << index3 << endl << endl;
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
}
bool test(const K& key)
{
size_t index1 = Hash1()(key) % _N;
if (_bs.test(index1) == false)
{
return false;
}
size_t index2 = Hash2()(key) % _N;
if (_bs.test(index2) == false)
{
return false;
}
size_t index3 = Hash3()(key) % _N;
if (_bs.test(index3) == false)
{
return false;
}
return true; // 但是这里也不一定是真的在,还是可能存在误判
// 判断在,是不准确的,可能存在误判
// 判断不在,是准确
}
void reset(const K& key)
{
// 将映射的位置给置0就可以?
// 不支持删除,可能会存在误删。一般布隆过滤器不支持删除
}
private:
BitSet::bitset _bs; // 位图
size_t _N;
};
void test_bloomfilter()
{
bloomfilter<std::string> bf(100);
bf.set("abcd");
bf.set("aadd");
bf.set("bcad");
cout << bf.test("abcd") << endl;
cout << bf.test("aadd") << endl;
cout << bf.test("bcad") << endl;
cout << bf.test("cbad") << endl;
}
}
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
布隆过滤器优点:
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
布隆过滤器缺陷:
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素