hello!我是bug。今天我们来进一步学习哈希的相关内容——位图和布隆过滤器:
(代码可能会有一点问题,请各位老铁指正 )
当我们进行对数据进行查找时,红黑树、AVL树、哈希表都是很好的选择。因为它们查找的效率极高。但是随着数据的增多,使用它们存储数据时,要申请大量的空间。
当数据量达到十亿时(假设为int类型),单存储数据就需要以4个G的内存,但是红黑树中还有指针三叉链、颜色标识,哈希表就更不用提了,其需要申请更大的空间。这个时候内存就成为了一个大问题,而且面对更多的数据时,红黑树和哈希表不再适用。这个时候位图就被引入了。
位图:就是用每一位来存放某种状态,判断数据存不存在,通常用于海量数据,且数据的状态较少。
那么位图具体是什么呢?如下图:
有8个二进制位,每个二进制位都对应一个下标,二进制中只能存储1和0,即两种状态。假设1表示数据存在、0表示数据不存在,那么当我们进行查找时,只要计算出数据的下标就可以找到对应的二进制,从而判断数据是否存在。⚽️⚽️
这里我们开辟一个数组,元素类型为int,一个int类型的元素占用32个bit位那么每个元素就可以表示32个数据。进行数据存储时,先计算数据位于哪个元素中,再计算数据在元素的第几个二进制位,通过位运算确定二进制位上存储的内容判断数据是否存在⚽️⚽️
注意❗️ ❗️
位图一般用来查找整型数据,因为整型数据可以直接一一映射,不需要进行转换。而其他类型就需要转换成整型、再来设置二进制位、查找操作。但是,保证其他类型转换成二进制位不发生冲突几乎是不可能的,所以就引出了下面的布隆过滤器(下面介绍)。
BitSet的相关接口:
函数 | 用法 |
---|---|
operator[] | 通过[]判断数据是否存在 |
set | 将数据对应的二进制位设置为1 |
reset | 将数据对应的二进制位设置为0 |
test | 判断数据是否存在 |
count | 返回数据的个数 |
size | 返回位图中比特位的总个数 |
位图的实现代码⬇️ ⬇️ :
#pragma once
#include
namespace lz
{
template<size_t n, class T = int>
class BitSet
{
private:
vector<T> _bit;
size_t _bit_count;
public:
//多开一个桶,因为除法是向下取整,这里位运算优先级低,要加括号
BitSet(size_t bit_count = n):_bit((bit_count >> 5) + 1), _bit_count(bit_count){}
bool operator[](size_t pos)const { return test(pos); }
BitSet& set()
{
//计算桶的个数
size_t bucket = (_bit_count >> 5) + 1;
//将每个桶中的比特位置为0
for (size_t i = 0; i < bucket; i++)
_bit[i] |= -1;
return *this;
}
BitSet& set(size_t pos)
{
assert(pos <= _bit_count);
size_t bucket = pos >> 5;
size_t index = pos % 32;
_bit[bucket ] |= (1 << index);
return *this;
}
BitSet& reset()
{
//计算桶的个数
size_t bucket = (_bit_count >> 5) + 1;
//将每个桶中的比特位置为0
for (size_t i = 0; i < bucket; i++)
_bit[i] &= 0;
return *this;
}
BitSet& reset(size_t pos)
{
assert(pos <= _bit_count);
size_t bucket = (pos >> 5);
size_t index = pos % 32;
_bit[bucket] &= ~(1 << index);
return *this;
}
bool test(size_t pos)const
{
assert(pos <= _bit_count);
size_t bucket = (pos >> 5);
size_t index = pos % 32;
return _bit[bucket] & (1 << index);
}
size_t size()const { return _bit_count; }
// 计算位图中比特为1的个数
size_t count()const
{
//每个数字里面bit为1的数量
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 count = 0;
for (size_t i = 0; i < _bit.size(); ++i)
{
int value = _bit[i];
int j = 0;
//一次判断一个字节,即8个bit位
while (j < sizeof(_bit[0]))
{
unsigned char c = value;
count += bitCnttable[c];
++j;
value >>= 8;
}
}
return count;
}
};
}
count:计算位图中被set的bit个数
不进行遍历,直接对字节进行判断。一个字节中有八个比特位,那我们要概括所有情况,就开辟一个数组,数组的大小为一个字节所有的可能情况,每个情况即下标对应存储的就是比特位为1的个数,那么我们直接通过下标就可以访问获得该数字中比特位为1的个数,再遍历每一个字节求和即可。
测试代码⬇️ ⬇️ :
void Test_BitSet()
{
lz::BitSet<200> bs1;
bs1.set(100);
bs1.set(100);
bs1.set(32);
bs1.set(40);
bs1.set(0);
//清除
//bs1.reset(100);
//bs1.reset(0);
// 全部清空
bs1.reset();
bs1.set();
cout << "bs1.operator[](100) : " << bs1.operator[](100) << endl;;
cout << "bs1.operator[](50) : " << bs1.operator[](50) << endl;;
cout << "bs1.test(100) : " << bs1.test(100) << endl;
cout << "bs1.test(50) : "<< bs1.test(50) << endl;
cout << "bs1.size() : " << bs1.size() << endl;
cout << "bs1.count() : " << bs1.count() << endl;
}
布隆过滤器:是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。即:为bitset和hash的结合。
针对其它类型的数据,冲突是必然的,我们只能进行缓解。即使用多个哈希函数进行映射,映射到多个位置中,多个位置全部冲突的概率比单个位置冲突要低得多,但是还是有一定几率的。布隆过滤器判断数据不存在没有误差,但是判断数据存在是有一定误差几率,所以如果只要求不存在的判断精确、存在的判断可以有误差的话,选择布隆过滤器。
优点:
1、插入和查找的时间复杂度为O(K),K为哈希函数的个数(一般较小)。
2、哈希函数之间没有关系,方便硬件并行运算。
3、 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。
4、在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。
5、数据量很大时,布隆过滤器可以表示全集,其他数据结构不能。
6、使用同一组散列函数的布隆过滤器可以进行交、并、差运算。
缺点:
1、有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白
名单,存储可能会误判的数据)
2、 不能获取元素本身
3、一般情况下不能从布隆过滤器中删除元素
4、如果采用计数方式删除,可能会存在计数回绕问题
注意❗️ ❗️
BloomFilter的接口很简单,就插入和查找。它是不支持删除的,因为删除的过程中,很可能影响到其它数据,因为多个数据可能会映射到同一个位置。
这个时候,如果一定要实现删除,那么就要使用计数操作。用多个比特位来替代原来的一个比特位。每当发生哈希冲突时,就进行自增操作,记录当前位置映射的次数,那么删除时,直接将次数自减即可。但是这种方法缺点也很明显,即比特位数量的选用,如果比特位选用过多,那么表示的数据也会更多,同时空间的消耗更大。相反,比特位选用过少,表示的数据也会更少,但是空间消耗也会随之降低。
BloomFilter的实现代码⬇️ ⬇️:
#pragma once
#include"BitSet.h"
namespace lz
{
struct BKDRHash
{
size_t operator()(const string& str)
{
size_t sum = 0;
for(size_t i = 0; i < str.size(); i++)
{
sum = sum * 131 + str[i];
}
return sum;
}
};
struct SDBMHash
{
size_t operator()(const string& str)
{
size_t sum = 0;
for(size_t i = 0; i < str.size(); i++)
{
sum = 65599 * sum + str[i];
}
return sum;
}
};
struct RSHash
{
size_t operator()(const string& str)
{
size_t sum = 0;
for (size_t i = 0; i < str.size(); i++)
{
sum = 63689 * sum + str[i];
}
return sum;
}
};
struct APHash
{
size_t operator()(const string& str)
{
size_t sum = 0;
for (size_t i = 0; i < str.size(); i++)
{
if ((i & 1) == 0)
sum ^= ((sum << 7) ^ str[i] ^ (sum >> 3));
else
sum ^= (~((sum << 11) ^ str[i] ^ (sum >> 5)));
}
return sum;
}
};
struct JSHash
{
size_t operator()(const string& str)
{
if (!str[0]) // 保证空字符串返回哈希值0
return 0;
size_t sum = 1315423911;
for (size_t i = 0; i < str.size(); i++)
{
sum ^= ((sum << 5) + str[i] + (sum >> 2));
}
return sum;
}
};
struct DJBHash
{
size_t operator()(const string& str)
{
if (!str[0]) // 这是由本人添加,以保证空字符串返回哈希值0
return 0;
size_t sum = 5381;
for (size_t i = 0; i < str.size(); i++)
{
sum += (sum << 5) + str[i];
}
return sum;
}
};
template<class T, size_t n, class Hash1 = BKDRHash,class Hash2 = SDBMHash,class Hash3 = RSHash, class Hash4 = APHash, class Hash5 = JSHash, class Hash6 = DJBHash>
class BloomFilter
{
public:
BloomFilter(size_t bf = n):_bf(bf * 6),_size(0){}
void insert(const T& val)
{
vector<size_t> v1(6);
v1.push_back(Hash1()(val) % _bf.size());
v1.push_back(Hash2()(val) % _bf.size());
v1.push_back(Hash3()(val) % _bf.size());
v1.push_back(Hash4()(val) % _bf.size());
v1.push_back(Hash5()(val) % _bf.size());
v1.push_back(Hash6()(val) % _bf.size());
for (auto e : v1)
{
_bf.set(e);
}
_size++;
}
bool IsInBloomFilter(const T& val)
{
//6个哈希函数分别映射
vector<size_t> v1(6);
v1.push_back(Hash1()(val) % _bf.size());
v1.push_back(Hash2()(val) % _bf.size());
v1.push_back(Hash3()(val) % _bf.size());
v1.push_back(Hash4()(val) % _bf.size());
v1.push_back(Hash5()(val) % _bf.size());
v1.push_back(Hash6()(val) % _bf.size());
//开始判断,有一个没找到,就不存在
for (auto e : v1)
if (!_bf.test(e))//如果没有找到,那么直接返回false
return false;
return true;
}
private:
//位图
lz::BitSet<n> _bf;
//有效元素个数
size_t _size;
};
}
测试代码⬇️ ⬇️:
void Test_BloomFilter()
{
//字符串
lz::BloomFilter<string,256> bf;
string arr[] = {
"left", "左边" ,"right", "右边","up", "向上"
,"down", "向下","left","左边","eat","吃"
,"sleep","睡觉","run","跑","jump","跳" };
//插入
for (const auto& str : arr)
bf.insert(str);
//判断
for (const auto& str : arr)
cout << bf.IsInBloomFilter(str) << " ";
cout << endl << "bf.IsInBloomFilter : " << bf.IsInBloomFilter("abc") << endl;
}