🏆个人主页:企鹅不叫的博客
🌈专栏
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件
【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)
【初阶与进阶C++详解】第四篇:类和对象中(类的六个默认成员函数)
【初阶与进阶C++详解】第五篇:类和对象下(构造+static+友元+内部类
【初阶与进阶C++详解】第六篇:C&C++内存管理(动态内存分布+内存管理+new&delete)
【初阶与进阶C++详解】第七篇:模板初阶(泛型编程+函数模板+类模板+模板特化+模板分离编译)
【初阶与进阶C++详解】第八篇:string类(标准库string类+string类模拟实现)
【初阶与进阶C++详解】第九篇:vector(vector接口介绍+vector模拟实现+vector迭代器区间构造/拷贝构造/赋值)
【初阶与进阶C++详解】第十篇:list(list接口介绍和使用+list模拟实现+反向迭代器和迭代器适配)
【初阶与进阶C++详解】第十一篇:stack+queue+priority_queue+deque(仿函数)
【初阶与进阶C++详解】第十二篇:模板进阶(函数模板特化+类模板特化+模板分离编译)
【初阶与进阶C++详解】第十三篇:继承(菱形继承+菱形虚拟继承+组合)
【初阶与进阶C++详解】第十四篇:多态(虚函数+重写(覆盖)+抽象类+单继承和多继承)
【初阶与进阶C++详解】第十五篇:二叉树搜索树(操作+实现+应用KVL+性能+习题)
【初阶与进阶C++详解】第十六篇:AVL树-平衡搜索二叉树(定义+插入+旋转+验证)
【初阶与进阶C++详解】第十七篇:红黑树(插入+验证+查找)
【初阶与进阶C++详解】第十八篇:map_set(map_set使用+multiset_multimap使用+模拟map_set)
【初阶与进阶C++详解】第十九篇:哈希(哈希函数+哈希冲突+哈希表+哈希桶)
【初阶与进阶C++详解】第二十篇:unordered_map和unordered_set(接口使用+模拟实现)
文章目录
💎一、位图
🏆1.位图概念
位图:用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
🏆2.位图实现
框架
框架:每个元素用char表示,每一个char有八个bit’,可以表示八个数据,把N除以8再加1,判断多少个字节可以有这么多bit的空间,但是会浪费几个比特,这些空间都是可以忽略的
// N个比特位的位图 template<size_t N> class bitset { public: bitset() { // +1保证足够比特位,最多浪费8个比特 _bits.resize(N / 8 + 1, 0); } private: //存放的是字节 vector<char> _bits; };
x映射的位标记成1
- 找到x所处在的哪一位:先确定这个数应该处在第几个字节,也就是i = x / 8;然后通过把x对8取模,得到x在第i个整形的第j个位置
- 把改为设置为1:把1左移j位,然后得到00…010…00,用这个数和第i整数进行按位或的操作
//x映射的位标记成1 void set(size_t x) { // x映射的比特位在第几个char对象 size_t i = x / 8; // x在char第几个比特位 size_t j = x % 8; //将第i个对象的第j位改成1 _bits[i] |= (1 << j); }
x映射的位标记成0
- 找到x所处在的哪一位
- 把这位设置为0:把1左移j位,然后得到00…010…00,把这个数取反,然后和第i个整数进行按位与的操作
//x映射的位标记成0 void reset(size_t x) { // x映射的比特位在第几个char对象 size_t i = x / 8; // x在char第几个比特位 size_t j = x % 8; //将第i个对象的第j位改成0 _bits[i] &= (~(1 << j)); }
判断x映射的位是否为1
- 找到x所处在的哪一位
- 判断这一位的值:把1左移j位,然后得到00…010…00,然后返回这个数和第i个字节进行位与的的结果
/判断x映射的位是否为1 bool test(size_t x) { // x映射的比特位在第几个char对象 size_t i = x / 8; // x在char第几个比特位 size_t j = x % 8; return _bits[i] & (1 << j); }
测试代码:
void test_bit_set() { bitset<1000> bs; bs.set(22); bs.set(21); cout << bs.test(22) << endl; cout << bs.test(21) << endl; bitset<0xFFFFFFFF> bigBS; //写法二 //bitset<-1> bigBS; }
🏆3.位图应用
快速查找某个数据是否在一个集合中
排序+去重
操作系统中磁盘的标记等
缺点:智能处理整形数据
💎二、布隆过滤器
🏆1.布隆过滤器概念
布隆过滤器通过多个哈希函数将一个数据映射到位图的结构中,也就是一个数据映射位图的多个位置,可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
🏆2.布隆过滤器框架
四个个字符串哈希函数如下: 用他们作为缺省参数,默认处理字符串类型,映射越多消耗的空间就越多,效率会变低,哈希冲突概率也就越小
//各种字符串哈希函数 struct BKDRHash { size_t operator()(const string& s) { size_t value = 0; for (auto ch : s) { value *= 31; value += ch; } return value; } }; struct APHash { size_t operator()(const string& s) { size_t hash = 0; for (long i = 0; i < s.size(); i++) { if ((i & 1) == 0) { hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3)); } else { hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5))); } } return hash; } }; struct DJBHash { size_t operator()(const string& s) { size_t hash = 5381; for (auto ch : s) { hash += (hash << 5) + ch; } return hash; } }; struct JSHash { size_t operator()(const string& s) { size_t hash = 1315423911; for (auto ch : s) { hash ^= ((hash << 5) + ch + (hash >> 2)); } return hash; } }; //有多少HashFunc,一个Key就转换成几个整数去映射对应的几个比特位 //映射越多消耗的空间就越多,效率会变低,哈希冲突概率也就越小 template<size_t M, //代表范围,能够映射元素个数 class K = string, //传送过来的数据 class HashFunc1 = BKDRHash, class HashFunc2 = APHash, class HashFunc3 = DJBHash, class HashFunc4 = JSHash> class BloomFilter { public: private: bitset<M> _bs;//数据 };
🏆3.布隆过滤器的插入
- 先用不同的哈希函数计算出该数据分别要映射到位图的哪几个位置
- 然后把位图中的这几个位置设置为1
//插入数据Key void Set(const K& key) { //有M个比特位,所以要映射到M个范围之内 size_t hash1 = HashFunc1()(key) % M; size_t hash2 = HashFunc2()(key) % M; size_t hash3 = HashFunc3()(key) % M; size_t hash4 = HashFunc4()(key) % M; //将对应的位置set为1 _bs.set(hash1); _bs.set(hash2); _bs.set(hash3); _bs.set(hash4); }
🏆4.布隆过滤器的查找
- 先用不同的哈希函数计算出该数据分别要映射到位图的哪几个位置
- 然后判断位图中的这几个位置是否都为1,如果有一个不为1,说明该元素一定不在容器中,只有一个为1,可能存在误判,如果三个位置都存在则一定存在误判
//判断Key是否存在,不在才是准确的 bool Test(const K& key) { //如果有一个位置比特位不再,就返回false //只有一个在可能存在误判 size_t hash1 = HashFunc1()(key) % M; if (_bs.test(hash1) == false) { return false; } size_t hash2 = HashFunc2()(key) % M; if (_bs.test(hash2) == false) { return false; } size_t hash3 = HashFunc3()(key) % M; if (_bs.test(hash3) == false) { return false; } size_t hash4 = HashFunc4()(key) % M; if (_bs.test(hash4) == false) { return false; } //三个位置都存在则存在误判 return true; }
🏆5.布隆过滤器的删除
在删除一个元素时,可能会影响其他元素。也就是说,要删除的元素映射的位置可能会是其它元素映射的位置,所以直接删除元素会给后期查找某个元素带来极大的误判。
🏆6.布隆过滤器的优缺点
优点:
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点:
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
测试代码:
void TestBloomFilter() { BloomFilter<44> bf; string a[] = { "榴莲", "香蕉", "西瓜", "1115551111", "eeehkfff", "草莓", "休息", "来了", "你好", "set" }; for (auto& e : a) { bf.Set(e); } for (auto& e : a) { cout << bf.Test(e) << endl; } cout << endl; cout << bf.Test("set") << endl; cout << bf.Test("string") << endl; }
💎三、海量数据面试题
🏆1.哈希切割
-
给一个超过100G大小的log_file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 如何找到top K的IP?如何直接用Linux系统命令实现?
- 先创建100个小文件A0-A99,然后计算 i = Hashhash(IP)%100(这样相同的ip一定进入同一个小文件),i是多少,IP就进入编号为i的文件中,先将一个小文件加载到内存中,依次读取放入map<string, int> countMap中依次对每个小文件统计,同时用一个pair<string, int> maxip录当前出现次数最多的IP,统计完一个文件,clear掉,然后再统计另一个文件
- priority_queue< pair<ip, int>, 仿函数 >minHeap,找出现次数topk的ip。
- 以上存在问题,某个小文件太大:某个文件相同ip太多,统计次数。映射冲突到这个编号文件的ip太多,捕获内存不足异常,说明内存不够,针对这个小文件,再次换个哈希函数,进行哈希切分,再切成小文件,再对这个小文件依次统计
- linux有专门的指令
🏆2.位图应用
-
给定100亿个整数,设计算法找到只出现一次的整数?
100亿个整数占用40G的空间,如果直接加载到内存中,空间肯定是不够的,所以我们这里可以考虑用位图来处理。改进位图,用两个比特位表示整数,用其中的三种状态00(没出现)、01(出现一次)和10(出现两次及以上)。消耗内存为:24(2^32-1)/32 byte≈1G
template<size_t N> class _bitset { public: //00代表出现0次,01代表出现1次,10代表出现两次及以上 void set(size_t x) { int in1 = _bs1.test(x); int in2 = _bs2.test(x); //00->01出现次数从0次变成1次 if (in1 == 0 && in2 == 0) { _bs2.set(x); } //01->10出现次数从1次变成两次 else if (in1 == 0 && in2 == 1) { _bs1.set(x); _bs2.reset(x); } } //返回出现一次的数 bool is_once(size_t x) { return _bs1.test(x) == 0 && _bs2.test(x) == 1; } private: //复用bitset bitset<N> _bs1; bitset<N> _bs2; };
-
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
将文件1的整数映射到一个位图中, 将文件2的整数映射到另一个位图中,然后将两个位图进行按位与,与之后位图中为1的位就是两个文件的交集,代码参考上面一个问题
-
位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
和第一题类似,00代表出现0次,01代表出现一次,10代表出现两次及其以上,所以只要统计00和01即可
🏆3.布隆过滤器
-
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
近似算法:将其中一个放到布隆过滤器,另外一个判断交集,在就是交集,不在就不是交集
精确算法:i = Hashfunc(query) % 1000,把文件1和文件2分别切分成A0、A1…A999和B0、B1…B999,两个文件分别切分成1000个小文件,先将一个小文件加载到内存中,依次读取放入unordered_set中依次对每个小文件统计,然后读取另外一个文件中的query,判断是否在,在就是交集
-
如何扩展BloomFilter使得它支持删除元素的操作
用几个比特位来表示计数器。