一.位图
1.位图的概念
2.位图的实现
位图核心的三个操作是set
、reset
和test
。
set是将数据对应的比特位置设为1,reset是将x对应的比特置0,test用来查看数据在不在位于结构里,存在返回1,不存在返回0。
set() 函数的实现原理:
1<<j 位后变为 0000 1000 让_bits[ i ] 或等上这个一位完的数后 ,只是在要插入数据的位图结构上由0 变为1 而对其他的数据没有影响。
reset()函数的原理类似首先确定该数据对应的位图结构,让1左移j位让后取反 变为 1111 0111然后与上_bits[ i ] 结果是这一个数据对应的位图结构被置为0,而其他的位图结构不变。
test()函数
首先还是确定数据对应的位图结构中的位置,将1左移到对应位置,然后与上_bits[ i ] 如果数据是存在的那最终就返回的是1,否则就返回的是0。
3.位图代码
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace cyf
{
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize(N/8+1, 0); // 这里最值得注意 得保证所有的数据都能存下
} // 10 个数据 10 /8 =1 但是会有两个数据没有对应的位图
//所以 +1 最多浪费7个比特位 最少浪费一个比特位
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;
};
void test_bitset()
{
//bitset<-1> bs2;
bitset<0xffffffff> bs2;
bs2.set(10);
bs2.set(20);
bs2.set(30);
cout << bs2.test(11) << endl;
cout << bs2.test(20) << endl;
cout << bs2.test(33) << endl;
cout << bs2.test(52) << endl;
cout << bs2.test(45) << endl << endl;
bs2.reset(20);
bs2.set(666);
cout << bs2.test(10) << endl;
cout << bs2.test(20) << endl;
cout << bs2.test(3000) << endl;
cout << bs2.test(666) << endl;
cout << bs2.test(777) << endl;
}
}
4.位图结构的应用
给定 100 亿个整数,设计算法找到只出现一次的整数。
100亿个数字找到只出现一次的整数,这是KV模型的统计次数,数字有三种状态:0次、1次、1次以上,。这三种状态需要用两个比特位就可以表示,分别位00代表0次,01代表1次,10代表1次以上既可以。我们可以采用两个位图来实现,复用上面所实现的位图即可解决问题
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
if (!_bs1.test(x) && !_bs2.test(x))//00
{
_bs2.set(x);//01
}
else if (!_bs1.test(x) && _bs2.test(x))//01
{
_bs1.set(x);
_bs2.reset(x);//10
}
else
{
//10不变
}
}
void PrintOnce()
{
for (size_t i = 0; i < N; ++i)
{
if (!_bs1.test(i) && _bs2.test(i))
{
cout << i << endl;
}
}
cout << endl;
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
void test_twobitset()
{
twobitset<100> tbs;
int a[] = { 2,3,4,56,99,55,3,3,2,2,10 };
for (auto e : a)
{
tbs.set(e);
}
tbs.PrintOnce();
}
二.布隆过滤器
1.布隆过滤器的概念
布隆过滤器是 由布隆( Burton Howard Bloom )在 1970 年提出的 一种紧凑型的、比较巧妙的 概 率型数据结构 ,特点是 高效地插入和查询,可以用来告诉你 “ 某样东西一定不存在或者可能存 在 ” ,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式 不仅可以提升查询效率,也 可以节省大量的内存空间 。
位图只能针对整型,字符串通过哈希转化成整型,再去映射,对于整型没有冲突,因为整型是有限的,映射唯一的位置,但是对于字符串来说,字符串的数量是是无限的但是整形是有限的,就会发生冲突,会发生误判。这种情况查找一个字符串不在是准确的,因为字符串转换位整形后,对应的位图结构如果没有被标记为1,那就说明这个字符串不在,且一定是准确的。但是如果一个字符串的位图结构是被标记为1的,此时这个被标记的位图结构可能就是这个字符串,当然也有可能是别的字符串和这个字符串共用的一个位图结构,此时查找在的结果就是不准确的!!!
布隆过滤器:可以降低误判率
:让一个值映射多个位置,但是并不是消除误判!
但是依旧可能存在误判: 当一个字符串与位图结构中的另一个字符串的对应的比特位正好是相等,所以在布隆过滤器只是降低了误判率并没有消除误判率。
如果布隆过滤器长度比较小,比特位很快会被占为1,误判率自然会上升,所以布隆过滤器的长度会影响误判率,理论上来说,如果一个值映射的位置越多,则误判的概率越小,但是并不是位置越多越好,空间也会消耗。所以误判率和空间大小也要有一个均衡值:
k是哈希函数的个数,m是布隆过滤器的长度,n是插入元素的个数。K=3,ln2 取 0.7,那么 m 和 n 的关系大概是 m =4.2n
,也就是过滤器长度应该是插入元素个数的 4 -5倍
2.代码实现过滤器
#include<string>
#include <bitset>
namespace cyf
{
struct BKDRHash
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto ch : key)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
struct APHash
{
size_t operator()(const string& key)
{
unsigned int hash = 0;
int i = 0;
for (auto ch : key)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));
}
++i;
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& key)
{
unsigned int hash = 5381;
for (auto ch : key)
{
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;
}
};
// 假设N是最多存储的数据个数
// 平均存储一个值,开辟X个位
template<size_t N,
size_t X = 6,
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) % (N * X);
size_t hash2 = HashFunc2()(key) % (N * X);
size_t hash3 = HashFunc3()(key) % (N * X);
size_t hash4 = HashFunc4()(key) % (N * X);
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
_bs.set(hash4);
}
bool test(const K& key)
{
size_t hash1 = HashFunc1()(key) % (N * X);
if (!_bs.test(hash1))
{
return false;
}
size_t hash2 = HashFunc2()(key) % (N * X);
if (!_bs.test(hash2))
{
return false;
}
size_t hash3 = HashFunc3()(key) % (N * X);
if (!_bs.test(hash3))
{
return false;
}
size_t hash4 = HashFunc4()(key) % (N * X);
if (!_bs.test(hash4))
{
return false;
}
// 前面判断不在都是准确,不存在误判
return true; // 可能存在误判,映射几个位置都冲突,就会误判
}
private:
std::bitset<N* X> _bs;
};
3.删除
布隆过滤器一般没有删除,因为布隆过滤器判断一个元素是会存在误判,此时无法保证要删除的元素在布隆过滤器中,如果此时将位图中对应的比特位清0,就会影响到其他元素了。这时候我们只需要在每个比特位加一个计数器,当存在插入操作时,在计数器里面进行 ++
,删除后对该位置进行 --
即可。但是布隆过滤器的本来目的就是为了提高效率和节省空间,在每个比特位增加额外的计数器,空间消耗那就更多了
4.布隆过滤器的优缺点
优点
1. 增加和查询元素的时间复杂度为 :O(K), (K 为哈希函数的个数,一般比较小 ) ,与数据量大小无关2. 哈希函数相互之间没有关系,方便硬件并行运算3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点
1. 有误判率,即存在假阳性 (False Position) ,即不能准确判断元素是否在集合中 ( 补救方法:再建立一个白名单,存储可能会误判的数据 )2. 不能获取元素本身3. 一般情况下不能从布隆过滤器中删除元素4. 如果采用计数方式删除,可能会存在计数回绕问题