前言:
前面我们学习了unordered_map和unordered_set和哈希表哈希桶等,并且我们自己用哈希桶封装了unordered_map和unordered_set。我们知道哈希的查找效率非常高为O(1),本章我们将延续哈希的思想,共同学习哈希的应用。
目录
概念:
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
在STL官方库中就有位图:文档入口--->位图文档
主要的接口如下图:
这里我们对于位图有了初步的理解,下面我们通过几道题目来深入理解一下。
这里只是介绍,下面我们在模拟实现中会对位图有更详细的解释。
我们来看一道面试题:
大家看到这道题,绝大部分人的第一思路就是历遍比较查找,但是这是40亿个数啊...
1、我们先来算一下40亿个数存储起来的大小:
先用10亿Byte为例:
40亿个数,每个数4Byte,那么40亿个数的大小就是16G左右。
2、我们能否通过之前学的容器将这么多的数据存储进去:
第一种方法:
我们用vector存放这些数,然后历遍查找。
首先对于40亿数查找效率O(N)很低,把这些数都读出来比较至少也得要16G连续的内存,不太现实。
第二种方法:
使用查找效率高的容器存放:
3、我们能否通过排序的方式:
4、直接定址法——用一个比特位标识映射值在不在
通过上面分析,发现我们之前学的一些方法都不太能行得通。
所以我们要用到了今天学习的位图。
4,294,967,295
,我们只需要给0 ~ 4,294,967,295
个比特位
注意:
这里开的是42亿九千万个(整形最大值个)比特位,而不止题目中的40亿个
用一个比特位标识的原因
这就是用位图的方式来解决该问题。
1、位图类的成员:
我们是通过每一个比特位来表示该位映射的值存不存在的,所以类中的成员我们引入一个vector来存放状态值。但是,vector中存放的是什么类型的数据比较好呢?
我们知道,比特位是最小的存储单位,我们的数据类型最小是char,1个字节,占有8个比特位,我们还是存储最小的数据类型比较好,这样便于后面的数据映射。
2、位图如何开空间:
因为我们vector存放的数据类型最小是char,是一个字节(只能8个8个这样开比特位空间),但是我们开的是比特位的整数个(可能不是8的整数个),所以没有直接精确控制到比特位个数的开空间方法,故采用以下方式:
3.位图如何插入标识位:
这里的插入并不是直接将数据插入到位图中,而是将数据对应的哈希地址所在的比特位标识成1。
char
为一个vector
的数据类型,所以我们先定位在哪一个char中18
,我们先定位它在哪一个char
中,所以我们18 / 8
,先定好位18 % 8
定位其具体在这个char
中的哪一个比特位4、位图如何实现删除:
和上面插入一个道理,只不过用到的位运算不一样:
/、%
操作
5、如何实现查找:
由图也就验证了开了四十二亿个比特位,大概是五百多MB。
具体代码:
-
- template<size_t N>
- class bitset
- {
- public:
- bitset()
- {
- //+1保证足够的比特位,最多浪费8个
- _bits.resize(N / 8 + 1, 0);
- }
-
- void set(size_t x)
- {
- size_t i = x / 8;
- size_t j = x % 8;
-
- _bits[i] |= (1 << j);
- }
-
- void reset(size_t x)
- {
- size_t i = x / 8;
- size_t j = x % 8;
-
- _bits[i] &= ~(1 << j);
- }
-
- bool test(size_t x)
- {
- size_t i = x / 8;
- size_t j = x % 8;
-
- return _bits[i] & (1 << j);
- }
-
- private:
- vector<char> _bits;
- };
很显然用之前学的容器内存是放不下40G这么大的数据的,那我们如何用位图来解决?
每个位图还是原来的大小(四十二亿九千万个),因为计算机表示的整数也就4,294,967,295
个,其余的也都是重复。
0 ~ 4,294,967,295
之间的,所以我们开两个位图,重复的数对应的位置在每个表中是一样的。-
- template<size_t N>
- class twobitset
- {
- 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)
- {
- _bs2.reset(x);
- _bs1.set(x);
- }
- }
-
-
- void Print()
- {
- for (size_t i = 0; i < N; ++i)
- {
- if (_bs2.test(i))
- {
- cout << i << endl;
- }
- }
- }
- private:
- bitset<N> _bs1;
- bitset<N> _bs2;
- };
-
-
-
- void test_twobitset()
- {
- int a[] = { 3, 45, 53, 32, 32, 43, 3, 2, 5, 2, 32, 55, 5, 53,43,9,8,7,8 };
- twobitset<100> bs;
- for (auto e : a)
- {
- bs.set(e);
- }
-
- bs.Print();
- }
因为只给了1个G的内存,我们首先想到的是用到两个位图,正好1个G。
解决办法:
在找交集之前我们一定要先做的是去重,对每个文件都要去重,不然就会找到重复交集。
在之前我们讲到了位图,他能迅速判断一个数是否在海量数据中。位图是直接映射,也不存在哈希冲突,空间消耗几乎没有,并且快,直接是O(1),但是位图只是适合于整形的查找,并不适用于浮点数字符串甚至是一些自定义类型
的查找。
布隆过滤器是由 布隆(Burton Howard Bloom) 在1970年提出的一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
比如说是10亿个字符串是否在一个文件中。
我们来判断一下能否用红黑树哈希表来存储这10亿个字符串呢?
此时一个叫布隆的人,正如概念中所提到的运用了位图的思想,将字符串转化成一个整数,然后映射到位图当中。
如图,我们通过某哈希函数,把字符串转成数值,然后通过位图标识:
但是,把字符串通过哈希函数转成数值这一过程可能会导致一种情况的发生:
不同字符串对应同一个比特位。如图中“美团”和“B站”字符串。
这种情况的发生我们是没有办法避免的,但是我们可以降低这种事件的发生:
由于上述情况的发生,我们可以得出结论:
我们截取一下公式:
通过上述公式,我们哈希函数个数k取3得到:
4.35 * n = m
也就是说在3个哈希函数的时候,没插入一个元素,就需要5个比特位来标识。
布隆过滤器是复用位图的:
-
- struct BKDRHash
- {
- size_t operator()(const string& s)
- {
- size_t hash = 0;
- for (auto ch : s)
- {
- hash += ch;
- hash *= 31;
- }
-
- return hash;
- }
- };
-
- struct APHash
- {
- size_t operator()(const string& s)
- {
- size_t hash = 0;
- for (long i = 0; i < s.size(); i++)
- {
- size_t ch = s[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& s)
- {
- size_t hash = 5381;
- for (auto ch : s)
- {
- hash += (hash << 5) + ch;
- }
- return hash;
- }
- };
-
- // N最多会插入key数据的个数
- template<size_t N,
- class K = string,
- class Hash1 = BKDRHash,
- class Hash2 = APHash,
- class Hash3 = DJBHash>
- class BloomFilter
- {
- public:
- void set(const K& key)
- {
- size_t len = N * _X;
- size_t hash1 = Hash1()(key) % len;
- _bs.set(hash1);
-
- size_t hash2 = Hash2()(key) % len;
- _bs.set(hash2);
-
- size_t hash3 = Hash3()(key) % len;
- _bs.set(hash3);
-
- //cout << hash1 << " " << hash2 << " " << hash3 << " " << endl << endl;
- }
-
- bool test(const K& key)
- {
- size_t len = N * _X;
-
- size_t hash1 = Hash1()(key) % len;
- if (!_bs.test(hash1))
- {
- return false;
- }
-
- size_t hash2 = Hash2()(key) % len;
- if (!_bs.test(hash2))
- {
- return false;
- }
-
- size_t hash3 = Hash3()(key) % len;
- if (!_bs.test(hash3))
- {
- return false;
- }
-
- // 在 不准确的,存在误判
- // 不在 准确的
-
- return true;
- }
- private:
- static const size_t _X = 6;
- bitset<N* _X> _bs;
- };
测试1:
- void test_bloomfilter1()
- {
- BloomFilter<100> bs;
- bs.set("sort");
- bs.set("bloom");
- bs.set("hello world hello bit");
- bs.set("test");
- bs.set("etst");
- bs.set("estt");
-
- cout << bs.test("sort") << endl;
- cout << bs.test("bloom") << endl;
- cout << bs.test("hello world hello bit") << endl;
- cout << bs.test("etst") << endl;
- cout << bs.test("test") << endl;
- cout << bs.test("estt") << endl;
-
- cout << bs.test("ssort") << endl;
- cout << bs.test("tors") << endl;
- cout << bs.test("ttes") << endl;
- }
通过结果判断,面对一些短小的字符串,判断的准确率还是挺高的。
测试二:
-
- void test_bloomfilter2()
- {
- srand(time(0));
- const size_t N = 10000;
- 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";
- //string url = "https://www.cctalk.com/m/statistics/live/16845432622875";
- 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.增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 2.哈希函数相互之间没有关系,方便硬件并行运算
- 3.布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 4.在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 5.数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 6.使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器缺点:
- 1.有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 2.不能获取元素本身
- 3.一般情况下不能从布隆过滤器中删除元素
- 4.如果采用计数方式删除,可能会存在计数回绕问题
1.注册的时候,快速判断一个昵称是否使用过:
2.黑名单:
3.过滤层,提高查找数据效率:
哈希切割:
问题:
解决方案一:
解决方案二:
然后再用红黑树和哈希表来统计:
注意:
存在问题:
某个文件太大了,哈希表和红黑树中放不下
解决办法:是针对小文件再分割,再用其他哈希函数,进行哈希分割,再切分成小文件。
布隆过滤器找交集:
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
假设每个query平均30byte,100亿query就是300G。
近似算法:
精确算法:
感谢您的阅读!