之前我们学习了unordered_set
和unordered_map
的使用,并了解和模拟实现了其地层结构——哈希桶,并对自己实现的哈希桶进行了封装,封装模拟实现了的unordered_set
和unordered_map
,同时我们了解到,哈希表的查找性能很高时间复杂度为〇(1),所以其思想也得到了广泛的应用,本文我们就来介绍其思想的两个应用 —— 位图和布隆过滤器。
在STL官方库中也有位图:
当我们有海量的数据要处理的时候,例如下面的例题:
【腾讯】 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。
通过上面的计算我们得到,10亿个字节的数据大概是占1个G,那么40亿个无符号整数,则是占16个G的内存。
4,294,967,295
,我们只需要给0 ~ 4,294,967,295
个比特位注意:
int
来标识的话,那么一个int
能标识256
种状态这就是用位图的方式来解决该问题。
位图如何开空间:
因为我们开的是比特位的整数个,但是又没有直接精确控制到比特位个数的开空间方法,所以采用一下方式:
位图如何插入标识位:
这里的插入并不是直接将数据插入到位图中,而是将数据对应的哈希地址所在的比特位标识成1。
char
为一个vector
的数据类型,所以我们先定位在哪一个char中18
,我们先定位它在哪一个char
中,所以我们18 / 8
,先定好位18 % 8
定位其具体在这个char
中的哪一个比特位如何实现插入?
我们想到了或运算和位运算:
只需要将1左移要标记的位之后与原来的数或一下,就成功标记好了。
如何实现删除:
/、%
操作通过任务管理器来查看内存
由图也就验证了开了四十二亿个比特位,大概是五百多MB。
具体代码:
//位图既节省空间又快。〇(1)就能判断
//N个比特位的位图 10 16
template<size_t N>//非类型模板参数
class bitset
{
public:
bitset()
{
//+1保证足够比特位,最多浪费8个
_bits.resize(N / 8 + 1, 0);
}
//x映射的位标记成1
void set(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
_bits[i] |= (1 << j);
}
void reset(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
//! && ||
//~ & |
_bits[i] &= (~(1 << j));//不管原来的位置是1还是0,都要搞成0
}
//判断x在不在,是1返回true,是0返回false
bool test(size_t x)
{
//x映射的比特位在第几个char对象
size_t i = x / 8;
//x在char第几个比特位
size_t j = x % 8;
return _bits[i] & (1 << j);
}
private:
std::vector<char> _bits;
//vector _bits;
};
位图的应用
1. 给定100亿个整数,设计算法找到只出现一次的整数。
很显然用之前学的容器内存是放不下40G这么大的数据的,那我们如何用位图来解决?
每个位图还是原来的大小(四十二亿九千万个),因为计算机表示的整数也就4,294,967,295
个,其余的也都是重复。
0 ~ 4,294,967,295
之间的,所以我们开两个位图,重复的数对应的位置在每个表中是一样的。template<size_t N>
class two_bitset
{
public:
void set(size_t x)
{
int in1 = _bs1.test(x);
int in2 = _bs2.test(x);
if (in1 == 0 && in2 == 0)
{
_bs2.set(x);
}
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<N> _bs1;
bitset<N> _bs2;
};
2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
因为只给了1个G的内存,我们首先想到的是用到两个位图,正好1个G。
解决办法:
3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
在之前我们讲到了位图,他能迅速判断一个数是否在海量数据中。位图是直接映射,也不存在哈希冲突,空间消耗几乎没有,并且快,直接是O(1),但是位图只是适合于整形的查找,并不适用于浮点数字符串甚至是一些自定义类型
的查找。
布隆过滤器是由 布隆(Burton Howard Bloom) 在1970年提出的一种紧凑型的、比较巧妙的概率型数据结构
,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
比如说是10亿个字符串是否在一个文件中。
我们来判断一下能否用红黑树哈希表来存储这10亿个字符串呢?
此时一个叫布隆的人,正如概念中所提到的运用了位图的思想,将字符串转化成一个整数,然后映射到位图当中。
如图我们给出一组ip:
既然哈希冲突解决不了,那么我们就降低冲突的概率,降低误判:
布隆过滤器采用的是一个字符串映射多个值,在判断在不在时,当着多个值都存在的时候,才能判断其存在,不然就是不在。
我们截取一下公式:
也就是说在3个哈希函数的时候,没插入一个元素,就需要5个比特位来标识。
布隆过滤器是复用位图的:
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;
}
};
struct JSHash
{
size_t operator()(const string& s)
{
size_t hash = 1315423911;
for (auto ch : s)
{
hash ^= ((hash << 5) + ch + (hash >> 2));
}
return hash;
}
};
template<size_t M,
class K = string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash,
class HashFunc4 = JSHash>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = HashFunc1()(key) % M;
size_t hash2 = HashFunc2()(key) % M;
size_t hash3 = HashFunc3()(key) % M;
size_t hash4 = HashFunc4()(key) % M;
//cout << hash1 << " " << hash2 << " " << hash3 << endl;
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
_bs.set(hash4);
}
bool Test(const K& key)
{
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; //存在误判
}
//布隆过滤器的删除可能会影响别的数据存储情况,所以不支持直接删除 -- 需要付出代价的
bool Reset(const K& key);
private:
bitset<M> _bs;
};
测试1
void TestBloomFilter1()
{
//插入10个值
//BloomFilter<60, string, BKDRHash, APHash, DJBHash> bf;
BloomFilter<60> bf;
string a[] = { "苹果", "香蕉", "西瓜", "111111111", "eeeeeffff", "草莓", "休息", "继续", "查找", "set" };
for (auto& e : a)
{
bf.Set(e);
}
for (auto& e : a)
{
cout << bf.Test(e) << " ";
}
cout << endl;
cout << bf.Test("芒果") << " ";
cout << bf.Test("string") << " ";
cout << bf.Test("ffffeeeee") << " ";
cout << bf.Test("31341231") << " ";
cout << bf.Test("ddddd") << " ";
cout << bf.Test("3333343") << " ";
}
我们提供了四个哈希函数,那么每插入一个数据就差不多需要60个比特位。
void TestBloomFilter2()
{
srand(time(0));
const size_t N = 1000;//<>内放的是常量
BloomFilter<6 * 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(1234 + i));
}
for (auto& str : v1)
{
bf.Set(str);
}
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(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.无法确认元素是否真正在布隆过滤器中
2.存在计数回绕
布隆过滤器优点:
1.增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
2.哈希函数相互之间没有关系,方便硬件并行运算
3.布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4.在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
5.数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6.使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器缺陷:
1.有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
2.不能获取元素本身
3.一般情况下不能从布隆过滤器中删除元素
4.如果采用计数方式删除,可能会存在计数回绕问题
1.注册的时候,快速判断一个昵称是否使用过:
2.黑名单:
3.过滤层,提高查找数据效率:
哈希切割:
问题:
解决方案一:
100G
的log中的IP肯定是不能放在红黑树或者哈希表
中的。解决方案二:
注意:
存在问题:
某个文件太大了,哈希表和红黑树中放不下
布隆过滤器找交集:
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法。
假设每个query平均30byte,100亿query就是300G。
似算法:
精确算法: