目录
位图
位图概念的引入
位图的实现
实现功能
开辟bit空间
数据输入set
数据删除reset
数据确认test
代码汇总
容器位图的衍生使用
布隆过滤器
布隆过滤器提出
布隆过滤器概念
布隆过滤器的实现
布隆过滤器的删除
布隆过滤器的特点
布隆过滤器的误判率
布隆过滤器的使用场景
精准查询 - 不允许误判
简陋查询 - 允许误判
哈希切割
所谓海量数据处理,就是指数据量太大,无法在较短时间内迅速解决,或者无法一次性装入内存。所以对于海量数据处理方法的运用是相关重要的。
对于位图、布隆过滤器、哈希切割需要哈希算法的思维。
【C++】-- 哈希(上万字详细配图配代码从执行一步步讲解)_川入的博客-CSDN博客
位图
位图概念的引入
题#:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
解题思路通常为:
- 遍历(但是找一个数据时间复杂度就为O(N))
- 排序(O(NlogN)),然后利用二分查找(O(logN)。(40亿个无符号整数约等于16G,光看内存所需就是极为恐怖的)
- 利用位图解决。
位图的原理简单来说就是一个计数排序。不同的是每一个元素只有一个bit位的大小,就是说,其只有0与1两种状态数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:
由于没有字节的类型于是需要我们自行进行分割使用,所以使用char或int类型的数组皆可。
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
位图的实现
实现功能
#include<vector>
using namespace std;
namespace cr
{
template<size_t N> // 非类型模板参数 N为需开辟的bit位个数
class bitset
{
public:
//开辟bit空间
bitset();
//数据输入set
void set(size_t x);
//数据删除reset
void reset(size_t x);
//数据确认test
bool test(size_t x);
private:
vector<char> _bits; // 此处采取vector容器实现
};
}
开辟bit空间
所知N为需开辟的bit位个数,而我们利用vector<char>容器开辟了一段数组,而8bit位为1byte:
//开辟bit空间
bitset()
{
_bits.resize(N / 8 + 1, 0);
}
由于 N / 8 是会省去余数。所以,时常会因为 N / 8 的除不净,而导致的开辟空间不足。而又因为采取的容器是vector<char>容器,所以一次空间开辟至少为char(8bit),所以只能在 N / 8 的基础上 + 1。
由于位图是利用bit位的0与1判断数据是否存在,所以数据置0是至关重要的。
数据输入set
//数据输入set
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
因为底层采取的是vector<char>容器,所以每8bit位就是vector<char>容器实现的数组的一位。所以根据x / 8判断其应在char数组的哪一位中。而x % 8即为其在此位中的哪一个字节。
由于是位操作。即利用位操作符 << 数据 1 ,保证只有数据的位置为1,然后利用位操作符 | 的有1即为1。记录数据是否存在。
数据删除reset
//数据删除reset
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= ~(1 << j);
}
因为底层采取的是vector<char>容器,所以每8bit位就是vector<char>容器实现的数组的一位。所以根据x / 8判断其应在char数组的哪一位中。而x % 8即为其在此位中的哪一个字节。
由于是位操作。即利用位操作符 << 数据 1 并取反,保证只有数据的位置为0,然后利用位操作符 & 的都为1才是1。将存在的数据记录删除。
数据确认test
//数据确认test
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);
}
因为底层采取的是vector<char>容器,所以每8bit位就是vector<char>容器实现的数组的一位。所以根据x / 8判断其应在char数组的哪一位中。而x % 8即为其在此位中的哪一个字节。
由于是位操作。即利用位操作符 << 数据 1 ,保证只有数据的位置为1,然后利用位操作符 & 的都为1才是1。判断数据是否存在。
代码汇总
#include<iostream>
#include<vector>
using namespace std;
namespace cr
{
template<size_t N> // 非类型模板参数
class bitset
{
public:
//开辟bit空间
bitset()
{
_bits.resize(N / 8 + 1, 0);
}
//数据输入set
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
//数据删除reset
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= ~(1 << j);
}
//数据确认test
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);
}
private:
vector<char> _bits;
};
}
int main()
{
cr::bitset<100> bs;
bs.set(8);
bs.set(9);
bs.set(20);
cout << bs.test(8) << endl;
cout << bs.test(9) << endl;
cout << bs.test(20) << endl;
bs.reset(8);
bs.reset(9);
bs.reset(20);
cout << bs.test(8) << endl;
cout << bs.test(9) << endl;
cout << bs.test(20) << endl;
return 0;
}
位图的应用
- 快速查找某个数据是否在一个集合中。
- 排序 + 去重。
- 求两个集合的交集、并集等。
- 操作系统中磁盘块标记。
位图对于处理大量数据很方便,但是只能运用于整数。
位图的特点
- 快、节省空间。
- 相对局限,只能映射处理整形。
由于位图只能映射处理整形,于是对此我们要采取新的方式:布隆过滤器
STL中有bitset(位图)
容器位图的衍生使用
#题:给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
解题思路:
使用两个bitset(位图),分别记录两个文件中的数据的存在状态,最后将两个两个bitset(位图)结合(映射位都是1的值就是交集)。
Note:
对于bitset容器的空间开辟使用bitset<-1> bs;(数据的范围与个数无关,所以我们只需要取到整数的大小范围,即:(size_t) -1)
#题: 1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
解题思路:利用两个位图进行计数。template<size_t N> class twobitset { public: void set(size_t n) { bool insert1 = _bs1.test(n); bool insert2 = _bs2.test(n); if (insert1 == false && insert2 == false) // 0次 + 1 { _bs1.set(n); } else if (insert1 == true && insert2 == false) // 1次 + 1 { _bs2.set(n); _bs1.reset(n); } else if (insert1 == false && insert2 == true) // 2次 + 1 { _bs1.set(n); } } // 此题不需要 //void reset(size_t n) //{ // bool erase1 = _bs1.test(n); // bool erase2 = _bs2.test(n); // if (erase1 == true && erase2 == true) // 3次 - 1 // { // _bs1.reset(n); // } // else if (erase1 == false && erase2 == true) // 2次 - 1 // { // _bs2.reset(n); // _bs1.set(n); // } // else if (insert1 == true && insert2 == false) // 1次 - 1 // { // _bs1.reset(n); // } //} // 找到只出现一次和二次的 void print_once_twice_num() { for (size_t i = 0; i < N; ++i) { if (!(_bs1.test(i) == true && _bs2.test(i) == true)) { cout << i << endl; } } } private: bitset<N> _bs1; // 低位 bitset<N> _bs2; // 高位 };
布隆过滤器
布隆过滤器提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。
问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了
- 将哈希与位图结合,即布隆过滤器
布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
其本质上就是一个位图,对于位图我们知道:其是用每一位来存放某种状态,是适用于海量数据,用来判断某个数据存不存在的。这正映衬着使用新闻客户端看新闻时,不停地推荐新的内容,每次推荐时的去重。
可是,内容是由字符串代表的,而字符串是有很多的状态。字符的不同,长度的不同等,皆会导致字符串转换的数值会有重复。这代表,如果我们使用位图完全同样的思维,会导致大量资源因为哈希地址的相同而被判断为出现过。
于是,如何防止大量不同的字符串因为哈希地址的相同而导致的判断不准确就是重点:
采用多个位置映射
理论而言:一个值映射的位越多,误判的概率越低。但是也不能映射的太多,映射位越多,那么空间的消耗就越多。
(此处采取三个位映射)
采取多个位映射只能一定程度上的减少误判,并不能完全的避免。
如何选择哈希函数个数和布隆过滤器长度
(下列数据来自于链接中的知乎)
选择适合的 k 和 m 值公式:
此文,我们使用第二公式(通过:3个哈希函数,ln2 ≈ 0.693):
布隆过滤器的实现
采取对bitset(位图)的再封装实现。
字符串哈希算法
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; // 可能存在误判
}
// 此处不支持删除
//void Reset(const K& key);
private:
const static size_t _ratio = 3; //误判过高可以往上加
std::bitset<_ratio* N>* _bits = new std::bitset<_ratio* N>;
};
int main()
{
BloomFilter<10> bf;
string arr1[] = { "苹果", "西瓜", "阿里", "美团", "苹果", "字节", "西瓜", "苹果", "香蕉", "苹果", "腾讯" };
for (auto& str : arr1)
{
bf.Set(str);
}
// 相同字符串的判断
for (auto& str : arr1)
{
cout << bf.Test(str) << " ";
}
cout << endl << endl;
// 相似字符串的判断
string arr2[] = { "苹果核", "西瓜", "阿里巴巴", "美团", "苹果皮", "字节"};
for (auto& str : arr2)
{
cout << str << ":" << bf.Test(str) << endl;
}
}
布隆过滤器的删除
一般不支持删除,因为支持的话很有可能干扰到其他值。毕竟一个数据占多个哈希地址位,是会干扰到其他的数据的,有可能多个数据映射了同一个位置,毕竟多个哈希地址有一个不存在,即数据不存在。
如果一定要支持删除,那么可以采取计数的方式:
利用多个bitset(位图)进行计数型的布隆过滤器实现。
- 1个位图:1(计数最大为:1)
- 2个位图:11(计数最大为:3)
- 3个位图:111(计数最大为:7)
- ……以此类推
布隆过滤器的特点
由于其采用的是一个或多个bitset(位图)结合多个哈希地址映射实现,所以对于数据的存在是有误差的:
- 在:不准确的,存在误差的
- 不在:准确的,不存在误判
布隆过滤器的误判率
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; // 可能存在误判
}
// 此处不支持删除
//void Reset(const K& key);
private:
const static size_t _ratio = 3; //误判过高可以往上加
std::bitset<_ratio* N>* _bits = new std::bitset<_ratio* N>;
};
void TestBloomFilter()
{
srand(time(0));
const size_t N = 100000;
BloomFilter<N> bf;
std::vector<std::string> v1;
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(1234 + i));
}
for (auto& str : v1)
{
bf.Set(str);
}
// 相似(通过相同的字符串向后追加)
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
{
std::string url = "http://www.cnblogs.com/-clq/archive/2021/05/31/2528153.html";
url += std::to_string(rand() + i);
v2.push_back(url);
}
size_t n2 = 0;
for (auto& str : v2)
{
if (bf.Test(str))
{
++n2;
}
}
cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;
// 不相识
std::vector<std::string> v3;
for (size_t i = 0; i < N; ++i)
{
string url = "zhihu.com";
url += std::to_string(rand() + i);
v3.push_back(url);
}
size_t n3 = 0;
for (auto& str : v3)
{
if (bf.Test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}
int main()
{
// 测试误判率
TestBloomFilter();
return 0;
}
布隆过滤器的使用场景
精准查询 - 不允许误判
在日常生活中时常会进行罪犯的抓捕,而对于罪犯的抓捕需要提取一些数据到电脑中,在利用这些数据到海量的数据库中寻找。但是,数据库的大小是海量的,大到不是一台移动电脑就能存下的,甚至需要数台服务器存储。如果可疑的数据一一到数据库中寻找,会因为如:硬盘查询速度缓慢,数据总量太多,网络的延迟等,都会造成长时间数据搜索。
这个时候就需要利用布隆过滤器对数据进行过滤,将大量的可疑数据在电脑中就进行一次大筛选。
简陋查询 - 允许误判
在日常生生活中,针对于某个游戏、某个app需要进行用户注册的时候,有一项选择是用户名的注册。有时候就会提醒你,你的用户名与他人相撞,需要更改用户名,这个时候,系统对于用户名是否有人使用的查询就是使用布隆过滤器。因为系统并没有必要精准的查询用户名是否真正的被占用。
哈希切割
#题:给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法。
解题思路:
哈希切割
- 假设每个query是30byte,则100亿query需要的空间为:3000亿byte约等于300G(这对于内存来说是一个恐怖的数据,所以需要用到哈希切分)
- 假设两个文件分别叫A和B
#题:给一个超过100G大小的log fifile, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
解题思路:
哈希切割
与上一题同样的道理,只不过最后用的不是set,而是map,通过编号相同的小文件Ai和Bi组成的map中,找到出现最多的。
#附加:与上题条件相同,如何找到top K的IP?
通过前面,编号相同的小文件组成的map的情况下,写一个K个数据的小根堆,再通过一一个map,以此筛选top K