布隆过滤器
位图
位图的概念
在介绍位图之前我们先看一个题目
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在 这40亿个数中。
对于这个问题有我们可以想到很多解决的方法
- 遍历,时间复杂度$O(N)$
- 排序+二分法(排序我们可以使用快排)所以就是时间复杂度就是O(NlogN + logN)
- 使用红黑树或者哈希表
==但是以上的方法都是要在内存中的!而且这是一个**40亿个整数!**光是数据量就是16g!一般的内存其实是放不下的!所以红黑树和哈希表就已经被否决了!==
既然放内存放不下我们是能不能用归并排序进行外排序!但是在磁盘里面就不能用二分法查找!磁盘里面是不支持随机访问的!
因为数据量实在是太大了!所以导致了我们常规的方法都不能使用了!
因为==我们只要知道这个值到底在不在这个数据里面!==——==而如何标记一个值在不在呢?用0和1就可以了!我们只要一个bit位就可以==
位图的思想其实也就是一个哈希——但是不是一般的哈希表
==就像是上面的这幅图一样!——因为在计算机里面二进制是左大右小的!像是1 就是00000001所以0-7就是从右边开始写起来!==
==这个时候42亿个数据只要512m的内存大小!==
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用 来判断某个数据存不存在的。
位图的实现
首先我们要解决如何开辟和映射的问题——==因为C++是不支持按bit位开辟空间的!
==也可以按照int类型 除8与模8改成除32与模32==
成员变量
#pragma once #include <iostream> #include <vector> namespace MySTL { template<size_t N>//使用非类型模版参数 class bitset { private: std::vector<char> _bit; }; }
成员函数
构造函数
#pragma once #include <iostream> #include <vector> namespace MySTL { template<size_t N>//使用非类型模版参数 class bitset { public: bitset() { //我们开辟的空间不是N,而是N/8+1 //因为我们要的是N个bit,而一个字节是8个bit //为什么要+1呢因为防止不是8的倍数,例如20,20/8=2,但是20个bit需要3个字节 //所以我们要+1 _bit.resize(N/8+1,0); //如果想使用位移要记得加括号,因为<<的优先级比+低 //_bit.resize((N>>3)+1,0); } private: std::vector<char> _bit; }; }
set
set就是将bit位置为1
#include <vector> namespace MySTL { template<size_t N>//使用非类型模版参数 class bitset { public: void set(size_t x)//set就是将bit位置为1 { size_t index = x/8;//确定在哪个字节 size_t pos = x % 8;//确定在字节中的哪个bit //将对应的BIT位置为1 //可以使用或进行运算! //因为或要两个都为0才为0,其他情况都为1 //我们只要有一个除了与这个BIT对应的位置为1,其他位置都为0的数 //我们就可以通过或将这个BIT位置为1,其他位置不变 //无论这个BIT原来是0还是1,都会变成1 _bit[index] |= (1<<pos); //(1<<pos),将1左移pos位,就是将1放在pos位置上 //因为二级制位是左高右低,所以要向左移动pos位 //这里和大小端没有关系,因为我们使用的是char,char是一个字节 } private: std::vector<char> _bit; }; }
reset
#pragma once #include <iostream> #include <vector> namespace MySTL { template<size_t N>//使用非类型模版参数 class bitset { public: void reset(size_t x)//reset就是将bit位置为0 { size_t index = x/8; size_t pos = x % 8; //reset是将该bit位置为0.无论原先是0还是1 //这时候我们可以使用与运算 //因为与运算要两个都为1才为1,其他情况都为0 //我们可以使用一个除了这个BIT位置为0,其他位置都为1的数 //例如pos的值为 2,先将1左移2位,就是将1放在第二位上 //然后进行取反,就是将这个数的所有位取反 //然后再进行与运算,就可以将这个BIT位置为0,其他位置不变 _bit[index] &= ~(1<<pos); } private: std::vector<char> _bit; }; }
test
#pragma once #include <iostream> #include <vector> namespace MySTL { template<size_t N>//使用非类型模版参数 class bitset { public: bool test(size_t x)//检查这个bit是否为1 { //这个也很简单,我们只要有一个除了这个BIT位置为1,其他位置都为0的数 //然后进行与运算,就可以得到这个BIT的值 //如果为0,就是false,如果不为,就是true size_t index = x/8; size_t pos = x % 8; return _bit[index] & (1<<pos); } private: std::vector<char> _bit; }; }
位图总结
==位图用来判断大量的整形数据存不存在的时候效率是非常高的!==
==但是位图不是万能的!因为位图只能用来判断整形!==
而库里面也为我们提供了这个库
==不过库里面的接口比我们实现的更多,我们主要实现了的它的几个关节接口的实现!==
位图应用
==位图一般都是用来处理大量的数据==
例1:给定100亿个整数,设计算法找到只出现一次的整数?
有三种状态 1. 0次,2. 1次,3.1次以上
==这其实就是位图的变形!==
==那么我们应该如何实现这个呢?——将我们上面的代码进行修改么?==
不用那么麻烦!我们可以复用上面的代码!——我们可以开两个位图!
//代码实现 #pragma once #include <iostream> #include <vector> namespace MySTL { template<size_t N> class TowBitSet { public: void set(size_t x) { if(!_bit1.test(x) && !_bit2.test(x))//00的情况 { _bit2.set(x);//第一次出现,变成01 } else if(!_bit1.test(x) && _bit2.test(x))//01的情况! { _bit1.set(x); _bit2.reset(x);//变成10 } else if(!_bit1.test(x) && _bit2.test(x))//10的情况 { return;//什么不动 } } //打印出现一次的数 void printonce() { for(size_t i = 0;i<N;++i) { if(_bit1.test(i) && !_bit2.test(i)) { std::cout<<i<<" "; } } } private: bitset<N> _bit1; bitset<N> _bit2; }; }
而且我们是不用担心内存不够的!因为位图的大小只与范围相关!而与实际的数据多少无关
==哪怕是1000亿个整数,它的范围大小都是整数的范围(0~42亿9千万多)
列2:给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
思路1:==我们可以将其中一个文件给映射进位图里面,然后将另一个文件去位图里面判断在不在!然后将结果放在另一个文件!如果在那么就是交集,如果不再就不是交集!==——但是要去重!因为如果判断的交集的文件里面有很多重复的!那么存放交集的文件里面也会有很多重复的元素!所以我们要将交集去重!
思路2:==将两个文件都分别放进两个位图里面!然后依次判断两个位图中相同的位置!如果位置都为1,就是交集,如果不都为1,那么就不是交集!==
这样子就不用去重了!因为位图自己就已经完成了去重
例3:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
这个其实和例1是相似的!就是通过多个位图来标记状态!
==不超过两次那么就是说有四种状态==
位图优缺点
位图的优点
- 节省空间
- 效率高(快)
位图的缺点
- 要求范围相对集中,范围特别分散,空间消耗就提升(例如有100个值,一个最小是1,最大是42亿,那么消耗就很大了!)
- 只能针对整形!
布隆过滤器的概念
为什么会有布隆过滤器这种东西呢?
==布隆过滤器其实是在位图的基础上进一步的产物==
从位图我们可以看出来,判断一个值是否存在其实不用那么麻烦,如果是使用哈希表或者红黑树,我们还要把值都存起来!然后再找!这样子其实消耗很大!如果数据量太大了!那么红黑树和哈希表就不够用了!——所以有了位图,==只用一个标记位来标记一个值在或者不再!==
==上面我们知道位图的缺点之一就是只能针对整形!那么我们有没有办法字符串进行映射呢?——答案是使用哈希函数(hashFunc(str))将字符串转换整形!然后通过这个整形来映射!不止是字符串!我们可以通过不同的哈希函数!将不同的类型转换为整形!==
当我们需要知道是否存在的时候,我们就将这个字符串转化为整形,去查看这个位图所在的位置是不是1,是就是存在!
但是这个方法也是有缺陷的!因为万一有多个字符串同时映射一个位置的时候就会导致误判!
因为字符串是无限的!但是整形是有限的!
所以判断==在这种情况是==不准确的!
因为可能不在,但是位置与别的字符串冲突了导致了误判!
也就是说即使不再对应的位置也可能是1
但是反过来!==不在这种情况却是准确的!==
如果不在那么那个位置一定是0
同时我们也要将映射的范围进行限定!
如果才100个字符串!却将所有整形开出来,那么空间浪费也很大!但是如果不全部开出来会导致hashFunc算出来的值有小,有的大!范围可能不够!所以我们必须模一下
==布隆过滤器就是上面方法的一种优化!==
既然会出现误判!那么我们有什么办法来减少误判呢?
==但是这种方法是不能避免误判的!只能减小误判的概率!因为有可能所有的位置都跟别的冲突了!==——这种方法就叫布隆过滤器!
==布隆过滤器的改进==
映射多个位置,降低误判率!(但是不是映射位置越多越好!应该这样子会造成很大空间浪费!)
布隆过滤器的应用
1 )可以适用于一些不一定准确的场景——注册时候的昵称判重!
在比较简单的场景下面,假如我们想要快速的判断!我们就可以用用到布隆过滤器!将所有的名称都映射进位图里面!然后再
2 )提高效率——做一个前置过滤
==如果对某些字段经常进行查找,但是又发现有很多值都是不在的!那么就可以提高效率了!==
但是布隆过滤器也不能太多了!因为布隆过滤器也是要消耗内存的!如果太多了也可能回影响效率!
3 )黑名单——这种不太要求准确,但是要快速判断的!
例如一些不宜内容的网页,或者垃圾邮箱,因为如果要传入到服务器里面进行判断那么太慢了
但是放到布隆里面就很快了!
布隆过滤器的实现
#pragma once #include <iostream> #include <bitset> #include <string> using namespace std; namespace MySTL { template<class T> struct BKDRHash { size_t operator()(const T &str) { size_t hash = 0; for(auto chi :str) { size_t ch = (size_t)chi; hash = hash * 131 + ch; } return hash; } }; template<class T> struct SDBMHash { size_t operator()(const T &str) { size_t hash = 0; for(auto chi :str) { size_t ch = (size_t)chi; hash = 65599 * hash + ch; //hash = (size_t)ch + (hash << 6) + (hash << 16) - hash; } return hash; } }; template<class T> struct RSHash { size_t operator()(const T &str) { size_t hash = 0; size_t magic = 63689; for(auto chi :str) { size_t ch = (size_t)chi; hash = hash * magic + ch; magic *= 378551; } return hash; } }; template<size_t N,//需要存储的最大数据! size_t X= 5,//平均存储一个值,开辟X位的空间! class K = std::string, class HashFunc1 = BKDRHash<K>, class HashFunc2 = SDBMHash<K>, class HashFunc3 = RSHash<K>>//哈希函数个数和玻璃长度有关系! class BloomFilter { public: void set(const K& key) { size_t hash1 = HashFunc1()(key) % (X*N);//要加上()因为 % 优先级高于* size_t hash2 = HashFunc2()(key) % (X*N); size_t hash3 = HashFunc3()(key) % (X*N); _bs.set(hash1); _bs.set(hash2); _bs.set(hash3); } bool test(const K& key) { size_t hash1 = HashFunc1()(key) % (X*N); if(!_bs.test(hash1)) return false; size_t hash2 = HashFunc2()(key) % (X*N); if(!_bs.test(hash2)) return false; size_t hash3 = HashFunc3()(key) % (X*N); if(!_bs.test(hash3)) return false; //前面判断不在是不准确的! return true;//可能存在误判!映射的几个位置都冲突了! } private: std::bitset<N*X> _bs; }; }
关于要几个哈希函数,和布隆过滤器的长度有关!
详解布隆过滤器的原理,使用场景和注意事项 - 知乎 (zhihu.com)关于想在这个方面更加的深入可以看这篇文章
==过滤器是用来减低误判的!不是用来避免误判的!==
==这个不支持reset!因为一个位置可能会被多个值给映射!如果reset掉可能就会影响其他的查找!==
如果我们把find给reset了!那么我们就会影响Insert!
==但是我们也可以强制让它支持reset!那就是进行计数!有几个值映射相同的位置!那么计数器就是几,如果要reset,那么就去计数器--,等计数器为0的时候,才将位置置为0!==
但是这样子有一个很大的麻烦就是会导致空间成倍的增加!因为我们不知道一个位置究竟有多少个映射!所以这就导致我们必须尽可能大的去开
==所以与其这样,不如就不支持reset!==
测试
#pragma once #include <iostream> #include <bitset> #include <string> namespace MySTL { void test_bloomfilter() { 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(i)); } for (auto& str : v1) { bf.set(str); } // v2跟v1是相似字符串集,但是不一样 std::vector<std::string> v2; for (size_t i = 0; i < N; ++i) { std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html"; url += std::to_string(999999 + 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(i+rand()); v3.push_back(url); } size_t n3 = 0; for (auto& str : v3) { if (bf.test(str)) { ++n3; } } cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl; } }
==我们可以通过增大X与增加哈希函数个数用来减少误判率!==