位图
位图概念
面试题
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯】- 遍历,时间复杂度O(N)
- 排序(O(NlogN)),利用二分查找: logN
- 位图解决
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:
位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
位图的实现
namespace bit { template<size_t N> class bitset { public: bitset(size_t bitCount) : _bit((bitCount>>5)+1), _bitCount(bitCount) {} // 将which比特位置1 void set(size_t which) { if(which > _bitCount) return; size_t index = (which >> 5); size_t pos = which % 32; _bit[index] |= (1 << pos); } // 将which比特位置0 void reset(size_t which) { if(which > _bitCount) return; size_t index = (which >> 5); size_t pos = which % 32; _bit[index] &= ~(1 << pos); } // 检测位图中which是否为1 bool test(size_t which) { if(which > _bitCount) return false; size_t index = (which >> 5); size_t pos = which % 32; return _bit[index] & (1<<pos); } // 获取位图中比特位的总个数 size_t size()const{ return _bitCount;} // 位图中比特为1的个数 size_t Count()const { int bitCnttable[256] = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8}; size_t size = _bit.size(); size_t count = 0; for(size_t i = 0; i < size; ++i) { int value = _bit[i]; int j = 0; while(j < sizeof(_bit[0])) { unsigned char c = value; count += bitCntTable[c]; ++j; value >>= 8; } } return count; } private: vector<int> _bit; size_t _bitCount; }; void test_bitset() { bitset<100> bs1; bs1.set(50); bs1.set(30); bs1.set(90); for (size_t i = 0; i < 100; i++) { if (bs1.test(i)) { cout << i << "->" << "在" << endl; } else { cout << i << "->" << "不在" << endl; } } bs1.reset(90); bs1.set(91); cout << endl << endl; for (size_t i = 0; i < 100; i++) { if (bs1.test(i)) { cout << i << "->" << "在" << endl; } else { cout << i << "->" << "不在" << endl; } } bitset<-1> bs2; bitset<UINT_MAX> bs3; bitset<0xffffffff> bs4; } template<size_t N> class two_bit_set { public: void set(size_t x) { // 00 -> 01 if (_bs1.test(x) == false && _bs2.test(x) == false) { _bs2.set(x); } else if (_bs1.test(x) == false && _bs2.test(x) == true) { // 01 -> 10 _bs1.set(x); _bs2.reset(x); } } //int test(size_t x) //{ // if (_bs1.test(x) == false // && _bs2.test(x) == false) // { // return 0; // } // else if (_bs1.test(x) == false // && _bs2.test(x) == true) // { // return 1; // } // else // { // return 2; // 2次及以上 // } //} bool test(size_t x) { if (_bs1.test(x) == false && _bs2.test(x) == true) { return true; } return false; } private: bitset<N> _bs1; bitset<N> _bs2; }; void test_bitset2() { int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 }; two_bit_set<100> bs; for (auto e : a) { bs.set(e); } for (size_t i = 0; i < 100; i++) { //cout << i << "->" << bs.test(i) << endl; if (bs.test(i)) { cout << i << endl; } } } void test_bitset3() { int a1[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 }; int a2[] = { 5,3,5,99,6,99,33,66}; bitset<100> bs1; bitset<100> bs2; for (auto e : a1) { bs1.set(e); } for (auto e : a2) { bs2.set(e); } for (size_t i = 0; i < 100; i++) { if (bs1.test(i) && bs2.test(i)) { cout << i << endl; } } } }位图的应用
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
布隆过滤器
布隆过滤器提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那
些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
- 将哈希与位图结合,即布隆过滤器
布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
布隆过滤器的插入
向布隆过滤器中插入:“baidu”
struct BKDRHash { size_t operator()(const string& s) { // BKDR 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; } }; template<size_t N, size_t X = 5, class K = string, class HashFunc1 = BKDRHash, class HashFunc2 = APHash, class HashFunc3 = DJBHash> class BloomFilter { public: void Set(const K& key) { size_t len = X*N; //仿函数的匿名对象调用operator对象 size_t index1 = HashFunc1()(key) % len; size_t index2 = HashFunc2()(key) % len; size_t index3 = HashFunc3()(key) % len; /* cout << index1 << endl; cout << index2 << endl; cout << index3 << endl<<endl;*/ _bs.set(index1); _bs.set(index2); _bs.set(index3); } bool Test(const K& key) { size_t len = X*N; size_t index1 = HashFunc1()(key) % len; if (_bs.test(index1) == false) return false; size_t index2 = HashFunc2()(key) % len; if (_bs.test(index2) == false) return false; size_t index3 = HashFunc3()(key) % len; if (_bs.test(index3) == false) return false; return true; // 存在误判的 } // 不支持删除,删除可能会影响其他值。 void Reset(const K& key); private: bitset<X*N> _bs; };使用库里的bitset避免开在对象栈帧里
#pragma once #include<bitset> #include<string> struct HashFuncBKDR { // BKDR size_t operator()(const string& s) { size_t hash = 0; for (auto ch : s) { hash *= 131; hash += ch; } return hash; } }; struct HashFuncAP { // AP size_t operator()(const string& s) { size_t hash = 0; for (size_t 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 HashFuncDJB { // DJB size_t operator()(const string& s) { size_t hash = 5381; for (auto ch : s) { hash = hash * 33 ^ ch; } return hash; } }; template<size_t N, class K = string, class Hash1 = HashFuncBKDR, class Hash2 = HashFuncAP, class Hash3 = HashFuncDJB> class BloomFilter { public: void Set(const K& key) { size_t hash1 = Hash1()(key) % M; size_t hash2 = Hash2()(key) % M; size_t hash3 = Hash3()(key) % M; _bs->set(hash1); _bs->set(hash2); _bs->set(hash3); } bool Test(const K& key) { size_t hash1 = Hash1()(key) % M; if (_bs->test(hash1) == false) return false; size_t hash2 = Hash2()(key) % M; if (_bs->test(hash2) == false) return false; size_t hash3 = Hash3()(key) % M; if (_bs->test(hash3) == false) return false; return true; // 存在误判(有可能3个位都是跟别人冲突的,所以误判) } private: static const size_t M = 10 * N; //bit::bitset<M> _bs; std::bitset<M>* _bs = new std::bitset<M>; }; void TestBloomFilter1() { string strs[] = { "百度","字节","腾讯" }; BloomFilter<10> bf; for (auto& s : strs) { bf.Set(s); } for (auto& s : strs) { cout << bf.Test(s) << endl; } for (auto& s : strs) { cout << bf.Test(s+'a') << endl; } cout << bf.Test("摆渡") << endl; cout << bf.Test("百渡") << endl; } void TestBloomFilter2() { srand(time(0)); const size_t N = 10000000; BloomFilter<N> bf; std::vector<std::string> v1; //std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html"; //std::string url = "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=65081411_1_oem_dg&wd=ln2&fenlei=256&rsv_pq=0x8d9962630072789f&rsv_t=ceda1rulSdBxDLjBdX4484KaopD%2BzBFgV1uZn4271RV0PonRFJm0i5xAJ%2FDo&rqlang=en&rsv_enter=1&rsv_dl=ib&rsv_sug3=3&rsv_sug1=2&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&inputT=330&rsv_sug4=2535"; std::string url = "猪八戒"; 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 urlstr = url; urlstr += std::to_string(9999999 + i); v2.push_back(urlstr); } 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"; string url = "孙悟空"; 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; }布隆过滤器的查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。
布隆过滤器删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
- 无法确认元素是否真正在布隆过滤器中
- 存在计数回绕
布隆过滤器优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器缺陷
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
海量数据面试题
哈希切割
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
依次读取每个ip,i=HashFunc(ip)%100每个ip就进入Ai小文件。那么相同的ip就进入相同小文件
依次使用map<string, int> countMap统计每个文件ip出现的次数。
(如果map抛异常,爆了,那么说明冲突很多,小文件很大,换哈希函数,二次切分处理)
位图应用
- 给定100亿个整数,设计算法找到只出现一次的整数?
- 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
- 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
布隆过滤器
- 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
Ai的query放set<string> seta
Bi的query放set<string> setbseta和setb找交集即可
query插入set里面的时候,抛异常
代表query太多,且重复不多
极端情况某个文件冲突很多
导致Ai域者Bi太大了,比如超过1G
换个哈希函数,对这个Ai和Bi文件再哈希切分
Ai和Bi小文件想象桶一样,重复和冲突的query都是进入相同的桶某个小文件太大,有两种可能,第一:相同的太多,这种读出来插入set去重了,不会影响 第二:冲突的太多,读出来插入set时会抛异常,需要二次处理,再换哈希函数,二次哈希切分
2. 如何扩展BloomFilter使得它支持删除元素的操作
每个位置改成多个位的引用计数就可以支持。比如一个映射位置给8个bit标记,但是这样空间消耗就大了