所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。 数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。例如:
STL提供了bitset容器供用户使用,其中常用的函数接口如下所示。
函数名称 | 函数作用 |
---|---|
bitset(); | 构造一个空的位图 |
set(size_t pos) | 将pos位置的bit位设置为1 |
reset(size_t pos) | 将pos位置的bit位设置为0 |
reset(size_t pos) | 返回pos位置bit位的结果 |
其中每一个位置pos对应一个整型数字。STL库中的bitset是用数组实现的,而不是vector。所以如果操作大量数据的时候,可以在堆上new一个bitset,避免栈溢出。
位图速度快,省时间,采用直接定址法,不存在哈希冲突。但是位图相对局限,只能映射处理整形。
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize(N / 8 + 1, 0);
}
//将对应的比特位置为1
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
//将对应的比特位置为0
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;
};
void test_set()
{
bitset<-1> bs1;
//bitset<0xffffffff> bs2;
bs1.set(8);
bs1.set(9);
bs1.set(20);
cout << bs1.test(8) << endl;
cout << bs1.test(9) << endl;
cout << bs1.test(20) << endl;
bs1.reset(8);
bs1.reset(9);
bs1.reset(20);
cout << bs1.test(8) << endl;
cout << bs1.test(9) << endl;
cout << bs1.test(20) << endl;
}
(1)给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
40亿个整数,在内存中占用大约16G空间,使用搜索树和哈希表都不行,因为内存中存储不了。可以使用外排序+二分查找,但是因为数据太大,只能放到磁盘文件中,要进行大量的IO,效率低。这里最好使用位图解决,只需要512MB就可以解决。
(2)给定100亿个整数,设计算法找到只出现一次的整数?
使用kv的统计次数搜索模型,使用两个bitset,表示一个数是否在可以分为三种情况,0次可以用00表示,1次可以用01表示,2次及以上可以用10表示。
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
bool inset1 = _bs1.test(x);
bool inset2 = _bs2.test(x);
// 00
if (inset1 == false && inset2 == false)
{
// -> 01
_bs2.set(x);
}
else if (inset1 == false && inset2 == true)
{
// ->10
_bs1.set(x);
_bs2.reset(x);
}
else if (inset1 == true && inset2 == false) //三次及其以上
{
// ->11
_bs1.set(x);
_bs2.set(x);
}
}
void print_once_num()
{
for (size_t i = 0; i < N; ++i)
{
if (_bs1.test(i) == false && _bs2.test(i) == true)
{
cout << i << endl;
}
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
void test_bitset()
{
int a[] = { 3, 4, 5, 2, 3, 4, 4, 4, 4, 12, 77, 65, 44, 4, 44, 99, 33, 33, 33, 6, 5, 34, 12 };
twobitset<100> bs;
for (auto e : a)
{
bs.set(e);
}
bs.print_once_num();
}
(3) 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
这个问题也可以使用两个bitset,文件一的值映射和文件二的值映射分别存到两个bitset中,当两个映射位都是1的值就是交集。
(4)一个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整
数?
这个问题是问题二的变形,使用两个bitset,表示一个数是否在可以分为四种情况,0次可以用00表示,1次可以用01表示,2次可以用10表示,三次及其以上使用11表示。
使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。新闻客户端推荐系统如何实现推送去重的?
用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。用哈希表存储用户记录,会浪费空间。用位图存储用户记录,但是位图一般只能处理整形,如果内容编号是字符串,就无法处理了。这时就要使用布隆过滤器,将哈希与位图结合。
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
使用哈希算法将字符串转换成整形去映射一个位置来标记。理论而言,一个值映射的位越多,误判率越低,但是空间消耗越高。那么如何选择合适的布隆过滤器长度呢?有专家学者研究过,当插入一个元素的时候,开4.2个位是最合适的,整数倍近似为5。
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。
所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。 一种支持删除的方法是将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
布隆过滤器优点
布隆过滤器缺陷
struct HashBKDR
{
// BKDR
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
struct HashAP
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
// N表示准备要映射N个值
template<size_t N,
class K = string,
class Hash1 = HashBKDR,
class Hash2 = HashAP,
class Hash3 = HashDJB>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
_bits->set(hash1);
size_t hash2 = Hash2()(key) % (_ratio * N);
_bits->set(hash2);
size_t hash3 = Hash3()(key) % (_ratio * N);
_bits->set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
if (!_bits->test(hash1))
return false; // 准确的
size_t hash2 = Hash2()(key) % (_ratio * N);
if (!_bits->test(hash2))
return false; // 准确的
size_t hash3 = Hash3()(key) % (_ratio * N);
if (!_bits->test(hash3))
return false; // 准确的
return true; // 可能存在误判
}
private:
const static size_t _ratio = 5;
std::bitset<_ratio* N>* _bits = new std::bitset<_ratio* N>;
};
(1)给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
可以把一个文件当中的query(字符串)放到一个布隆过滤器中去,在看另外一个在不在,存在误判,所以是近似算法。
假设每个query占有30byte,那么100亿个query占用300G空间,很现实,内存放不下。精确算法是使用哈希切分。
(2)给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?如何找到top K的IP?
读取每个ip,i=Hash(ip)%500,这个ip就进入第i个小文件。相同的ip,一定进入了同一个小文件。使用map,依次对每个小文件统计次数,如果找topK,就建一个K个值为
(3)如何扩展BloomFilter使得它支持删除元素的操作。
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。 一种支持删除的方法是将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作,优势被削弱了,一般不会这几支持删除操作的布隆过滤器。