目录
- 一、位图
- 1.1 经典题目
- 1.2 位图概念
- 1.3 位图的应用
- 1.4 关于位图的三个经典问题
- 二、布隆过滤器
- 2.1 布隆过滤器的提出
- 2.2 布隆过滤器的概念
- 2.3 布隆过滤器的插入
- 2.4 布隆过滤器的查找
- 2.5 布隆过滤器删除
- 2.6 代码实现
- 2.7 布隆过滤器的优点
- 2.8 布隆过滤器的缺陷
- 2.9 布隆过滤器的应用场景
- 三、海量数据处理
- 3.1 布隆过滤器
- 3.2 位图应用
- 3.3 哈希切割
一、位图
1.1 经典题目
假设有这么一道题目:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
1、先把数据用set存起来,再查找一个无符号整数可以吗?
不可以!为什么?因为40亿个整数大概要占16G内存,但是一般配置的计算机都是开不出16G的内存的,除非有些超级计算机,所以这个方法并不适用于所有计算机。
2、排序+二分查找可以吗?
也不适合,因为数据量太大而不能一次性全部加载到内存中,所以需要在磁盘中进行归并排序,要进行大量的IO,效率低;并且二分查找的时候也是要在磁盘中进行的,所以这个方法也不适合解决这个问题!
这也不行,那也不行,那该如何解决这个问题呢?
上面两种方法之所以不适用,是因为数据量太大了,并且又是整形,所占的内存空间太多,所以我们不能开辟那么大的内存出来,那如果我们能够用一个很小的空间标志出这些数据是否存在,那不就可以了吗?在这道题中我们的目标是知道一个数在不在这堆数里面,是在不在的问题,所以我们完全没有必要把这40亿个整数存起来,只需要标志一下这些数是否存在就可以了,标志一个数在不在我们其实只需要一个比特位就可以了,某个数在就把对应的比特位设置成1,不在就把对应的比特位设置成0,这样一来,本来需要4个字节,即32个比特位的空间存放一个整形,现在变成了只需要一个比特位标志,所以40亿个整数占用的空间就从16G变成了500M,500M的内存我们是可以开辟出来的,我们把开辟出来标志数据在不在的这段空间叫做位图,这样我们就能把这40亿个整数都标志到这个位图的对应位置,再查找某一个数在不在的时候就只需要查看位图中对应位置的是否为1,是1就表示这个数在,是0就表示这个数不在。
如此一来,就能把这道题给解决了。
1.2 位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
因为500M内存我们的计算机是能够开辟出来的,所以我们用的是直接定址法,即每一个整数直接映射到位图上的对应的位置。
位图代码实现:
namespace BitSetC
{//N是非类型模板参数template <size_t N>class bitset{public://构造函数,N表示该位图能映射的数据的最大值,每一个映射的值是一个比特位,所以应该根据//N的值求出我们需要开辟多少个char的空间,因为/的特点是把余数舍去了,所以为了映射数据//时不越界,所以应该多开辟一个char的空间,所以是开辟N/8+1个空间bitset(){_bs.resize(N / 8 + 1);}void set(size_t x){//计算x应该映射到第几个char中size_t i = x / 8;//计算x在第i个char中的第几个位置size_t j = x % 8;//通过位运算把第i个char的第j个位置设置为1//就把这个x设置进位图了_bs[i] |= (1 << j);}bool test(size_t x){//计算x应该映射到第几个char中size_t i = x / 8;//计算x在第i个char中的第几个位置size_t j = x % 8;//利用位运算检查第i个char的第j个位置是否被设置成1了//如果设置了,就证明这个x在位图中,否则,x不在位图中if (((_bs[i] >> j) & 1) == 1){return true;}else{return false;}}void reset(size_t x){//计算x应该映射到第几个char中size_t i = x / 8;//计算x在第i个char中的第几个位置size_t j = x % 8;//通过位运算把第i个char中的第j位设置为0即可_bs[i] &= (~(1 << j));}private://这里的vector的元素可以是char,也可以是其他类型vector<char> _bs;};
}
1.3 位图的应用
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
1.4 关于位图的三个经典问题
1、 给定100亿个整数,设计算法找到只出现一次的整数?
虽然是100亿个整数,但是整数最多也就40多亿个,所以一个位图占用的空间还是500M,因为要找出只出现一次的整数,如果只用一个位图只能标志一个数在不在,并不能很好的统计该数出现的次数,所以我们可以用两个位图来表示一个数字出现的次数,因为两个比特位可以表示出四种情况,00表示出现了0次,01表示出现了1次,10表示出现了2次,11表示出现了3次,如果一个数字出现了2次以上的就标志为11就行了,因为我们要找的是出现一次的数字,2次及以上的都是不符合要求的。
所以解决这个问题的大概思路是创建两个位图bs1和bs2,然后遍历这100亿个整数,判断bs1和bs2对应位置的情况,如果在这个数对应位置中,bs1=0,bs2=0,说明这个数还没有出现过,此时bs2设置为1;如果bs1=0,bs2=1,说明这个数字已经出现过一次,此时把bs1设置为1,bs2设置为0;如果bs1=1,bs2=0,说明这个数字已经出现了2次,这时把bs2设置为1,或者说设不设置都可以,因为这个数一定是不符合要求的。这样就能把所有数字出现的次数存放在了这两个位图中,再遍历一遍这批数,同时通过位图查看哪一些数映射到位图上对应位置的结果是bs1=0,bs2=1的数就是出现一次的数。
2、位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
同理,找出不超过2次的整数也是先按照第1题那样把这100亿个整形设置到两个位图bs1和bs2中,然后再遍历这100亿个数,找到在位图中对应位置的bs1=0,bs2=1或者bs1=1,bs2=0的数,就是出现次数不超过2的数,统计即可。
下面是用我们自己实现的位图解决1、2问题的参考代码:
namespace BitSetC
{//N是非类型模板参数template <size_t N>class bitset{public://构造函数,N表示该位图能映射的数据的最大值,每一个映射的值是一个比特位,所以应该根据//N的值求出我们需要开辟多少个char的空间,因为/的特点是把余数舍去了,所以为了映射数据//时不越界,所以应该多开辟一个char的空间,所以是开辟N/8+1个空间bitset(){_bs.resize(N / 8 + 1);}void set(size_t x){//计算x应该映射到第几个char中size_t i = x / 8;//计算x在第i个char中的第几个位置size_t j = x % 8;//通过位运算把第i个char的第j个位置设置为1//就把这个x设置进位图了_bs[i] |= (1 << j);}bool test(size_t x){//计算x应该映射到第几个char中size_t i = x / 8;//计算x在第i个char中的第几个位置size_t j = x % 8;//利用位运算检查第i个char的第j个位置是否被设置成1了//如果设置了,就证明这个x在位图中,否则,x不在位图中if (((_bs[i] >> j) & 1) == 1){return true;}else{return false;}}void reset(size_t x){//计算x应该映射到第几个char中size_t i = x / 8;//计算x在第i个char中的第几个位置size_t j = x % 8;//通过位运算把第i个char中的第j位设置为0即可_bs[i] &= (~(1 << j));}private://这里的vector的元素可以是char,也可以是其他类型vector<char> _bs;};
}//给定100亿个整数,设计算法找到只出现一次的整数?
//1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
template <size_t N>
class TwoBitSet
{
public:void set(size_t x){//x出现0次,则原来的位图组合是00,则把_bs1的x位置和_bs2的x位置设置为01if (!_bs1.test(x) && !_bs2.test(x)){_bs2.set(x);}//x出现1次,则原来的位图组合是01,则把_bs1的x位置和_bs2的x位置设置为10else if (!_bs1.test(x) && _bs2.test(x)){_bs1.set(x);_bs2.reset(x);}//x出现2次及以上,则原来的位图组合是10,则把_bs1的x位置和_bs2的x位置设置为11else if (_bs1.test(x) && !_bs2.test(x)){_bs2.set(x);}}//查找只出现一次的数bool is_once(size_t x){if (!_bs1.test(x) && _bs2.test(x)){return true;}return false;}//1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数bool FindLowerTwo(size_t x){//出现1次if (!_bs1.test(x) && _bs2.test(x)){return true;}//出现2次else if (_bs1.test(x) && !_bs2.test(x)){return true;}//出现0次或者2次以上return false;}
private:BitSetC::bitset<N> _bs1;BitSetC::bitset<N> _bs2;
};
3、给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
这里也肯定是用位图来解决的,因为有100亿个整数,整形的范围是固定的42亿9千万个,所以一个位图还是500M的空间。
所以先创建两个位图bs1和bs2,然后分别把这两个文件的整数设置到bs1和bs2位图中。此时我们如何找到这两个位图中的交集呢?我们知道1&1=1,1&0=0,0&1=1,而位图中的每一个位置要么是0,要么是1,如果两个文件都有同一个数,即交集,那么它们在各自位图中的映射的位置一定是一样的,所以把两个位图进行按位与操作,得到的结果就是它们的交集了,并且通过位图来查找交集还自带了去重的操作,因为交集是一个无重复元素的集合。那这两个位图怎么做按位与操作呢?其实这里不用真的把两个位图做按位与操作的,只需要在遍历这些数的时候判断该数是否同时在bs1和bs2中就可以了。
int main()
{//给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?//假设nums1和nums2就是100亿个整数的文件int nums1[] = { 1,2,3,6,8,2,3,6 ,6 };int nums2[] = { 1,7,9,6 };BitSetC::bitset<10> bs1;BitSetC::bitset<10> bs2;for (const auto& e : nums1){bs1.set(e);}for (const auto& e : nums2){bs2.set(e);}for (size_t i = 0; i < 10; i++){//同一个元素两个位图都设置了说明这个元素是交集if (bs1.test(i) && bs2.test(i)){cout << i << " ";}}cout << endl;return 0;
}
二、布隆过滤器
2.1 布隆过滤器的提出
我们在刷抖音时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。那么问题来了,抖音客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐视频时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录。缺点:浪费空间
- 用位图存储用户记录。缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
- 将哈希与位图结合,就叫做布隆过滤器。
2.2 布隆过滤器的概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中的多个位置。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
2.3 布隆过滤器的插入
2.4 布隆过滤器的查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中的多个位置上,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"zhaoyun"时,假设3个哈希函数计算的哈希值为:2、4、6,刚好和其他元素的比特位重叠,即其他元素的刚好把2,4,6比特位都设置了,此时布隆过滤器告诉你该元素存在,但是该元素是不存在的。
2.5 布隆过滤器删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。比如你要删除的元素的映射位置是1,3,5,那么你想删除这个元素就要把1,3,5位置都设置为0,但是可能其他元素通过哈希函数计算出来的映射位置也是1或者3或者5,所以如果把1,3,5的位置改成0的话,其它本来存在的元素也会变成不存在,这显然是不对的。基于这样的原因,有人提出既然直接置成0有可能会影响到其它元素,那么就用一个引用计数记录这个位置被映射了多少次,每一次删除的时候就减减这个引用计数,直到引用计数减为0的时候才把这个位置设置为0,这样删除就可以不影响其它元素了。
这个方法听起来确实可行,但是它真的可行吗?
其实不是的,因为某个元素存在是会有误判的,所以你怎么确定你这个元素是存在布隆过滤器里的呢?你都无法确定你这个元素是否存在,你怎么敢删除呢?就好比“zhaoyun”这个元素,假设它通过哈希函数算出来的映射位置是2,4,6,而2,4,6三个位置刚好又被其他元素都映射了,也就是说这个“zhaoyun”的存在是误判的,它压根就不存在,此时你删除“zhaoyun”,即把2,4,6三个位置的引用计数都减1,这符合逻辑吗?这显然不符合,你压根就不存在布隆过滤器中,你如何删除。所以用引用计数的方法实现布隆过滤器的删除也是不可行的,也是会影响到其他元素的,所以无论如何布隆过滤器都是不支持删除操作的。
2.6 代码实现
//针对字符串的哈希函数
struct BKDRHash
{size_t operator()(const string& str){size_t hash = 0;for (auto ch : str){hash = hash * 131 + ch;}return hash;}
};struct APHash
{size_t operator()(const string& str){size_t hash = 0;for (size_t i = 0; i < str.size(); i++){size_t ch = str[i];if ((i & 1) == 0){hash ^= ((hash << 7) ^ ch ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));}}return hash;}
};struct DJBHash
{size_t operator()(const string& str){size_t hash = 5381;for (auto ch : str){hash += (hash << 5) + ch;}return hash;}
};template <size_t N,class K = string,class Func1= BKDRHash, class Func2 = APHash, class Func3 = DJBHash>
class BloomFilter
{
public://为了减少误判率,所以一个字符串可以通过多个哈希函数//映射到多个位置,只有当全部的映射位置都被设置为1了才算// 是这个字符串存在,只要有任意一个位置没有被映射,就说//明这个字符串是不存在的void set(const K& key){//创建仿函数的匿名对象直接调用哈希函数计算映射位置//因为我们只有N个映射位置,所以需要%N才能映射到合// 法的对应的映射位置上size_t hash1 = Func1()(key) % N;size_t hash2 = Func2()(key) % N;size_t hash3 = Func3()(key) % N;//把通过哈希函数计算出来的这三个映射位置都设置为1,//代表这个字符串是存在的_bs.set(hash1);_bs.set(hash2);_bs.set(hash3);}bool test(const K& key){//分别检查key通过不同哈希函数计算出来的映射位置//是否都被设置成了1,只要其中任意一个位置不被设//置,就说明这个字符串是不存在的size_t hash1 = Func1()(key) % N;if (!_bs.test(hash1)){return false;}size_t hash2 = Func2()(key) % N;if (!_bs.test(hash2)){return false;}size_t hash3 = Func3()(key) % N;if (!_bs.test(hash3)){return false;}//来到这里说明该字符串在这三个映射的位置都被设置成1了//此时可以认为该字符串是存在的,但是也是会存在误判的//想要降低误判率,可以增加每一个字符串映射位置的个数//或者增加N,但是凡是都是有两面性的,增加了哈希函数就//会在映射时增加计算映射位置的时间,增大N就需要开辟更// 多的空间return true;}
private:bitset<N> _bs;
};
2.7 布隆过滤器的优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关。
- 哈希函数相互之间没有关系,方便硬件并行运算。
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算。
2.8 布隆过滤器的缺陷
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身,只能判断元素存不存在。
- 不能从布隆过滤器中删除元素。
- 如果采用计数方式删除,还是会影响到其他元素,因为可能要删除的元素本身就不存在。
2.9 布隆过滤器的应用场景
三、海量数据处理
3.1 布隆过滤器
- 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
3.2 位图应用
- 给定100亿个整数,设计算法找到只出现一次的整数?
- 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
- 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
这3个问题在上面位图的地方已经解答过了。
3.3 哈希切割
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
按每一条log file出现的次数建立一个k个元素的小堆,最后这个堆的k个数就是topk的IP,用linux系统指令实现是:
cat log.txt | cut -f1 -d' ' | sort | uniq -c | sort -nr | head -n 1上述命令按以下步骤执行:使用cat命令读取日志文件。
使用cut命令提取每行中的第一个字段(即IP地址)。
使用sort命令对IP地址进行排序。
使用uniq -c命令统计每个唯一的IP地址出现的次数。
使用sort -nr命令按照计数的逆序(从高到低)对IP地址进行排序。
使用head -n 1命令选择排序后的第一行(即出现次数最多的IP地址)。
要找到top K的IP地址,您可以将最后一步的命令更改为head -n K,其中K是您想找到的IP地址的数量。
以上就是今天想要跟大家分享的内容啦,你学会了吗?如果感觉到有所帮助,那就点亮一下小心心,点点关注呗,后期还会持续更新C++相关的知识哦,我们下期见!!!!