所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
在STL容器中有实现。
位图的实现很简单,主要成员函数有置1(即插入),置0(删除),检测某个数是否存在
#pragma once
#include
#include
namespace ns_bit_set
{
template<size_t N> // 非类型模板参数,表示位图的大小,能够表示数据的个数
class BitSet
{
public:
BitSet()
{
// 创建一个大小为N位的位图,但是要加1,因为这里的整除是向下取整
_bits.resize(N / 8 + 1, 0);
}
// 将表示x的位置1
void set(size_t x)
{
// 找到在第几个char里面,用i表示
// 找到在一个char里面的第几位,用j表示
size_t i = x / 8;
size_t j = x % 8;
// 将该位置1
_bits[i] |= (1 << j);
}
// 删除某一位,即将对应的bit位置0
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
// 置0
_bits[i] &= (~(1 << j));
}
// 检测某一个数是否存在
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
// 大于0为真, 等于0为假
return _bits[i] & (1 << j);
}
private:
vector<char> _bits;
};
}
优点: 速度快,并且节省空间
缺点: 只可以映射整形(如果有负数则开两个图,取反,映射到另外一个图)
相关题型:
40亿个整数大约需要40G的空间,一般计算机没有这么大的内存。
这个题最好是用位图解决,开一个32位无符号整数的最大范围的位图,然后将40亿个数存入位图即可。每次查询都是O(1)的时间复杂度。
所以需要两个位图,最多可以表示四种状态
使用 00表示没有出现,01表示出现一次, 10表示出现两次及以上。
template<size_t N>
class TwoBitSet
{
public:
void Set(size_t x)
{
if (!_bs1.test(x) && !_bs2.test(x)) // 00 -> 01
{
_bs2.set(x);
}
else if (!_bs1.test(x) && _bs2.test(x)) // 01 -> 10
{
_bs1.set(x);
_bs2.reset(x);
}
// 10 表示已经出现2次或以上,不用处理
}
void PrintOnceNum()
{
for (size_t i = 0; i < N; ++i)
{
if (!_bs1.test(i) && _bs2.test(i)) // 01
{
cout << i << endl;
}
}
}
private:
bit::bitset<N> _bs1;
bit::bitset<N> _bs2;
};
方法一:将其中一个文件1的整数映射到一个位图中,读取另外一个文件2中的整数,判断在不在位图中,在就是交集。此时消耗的内存是整数的范围232,也就是512M。但有缺陷,遇到重复的值也要查找。
方法二:将文件1的整数映射到位图1中,将文件2的整数映射到位图2中,然后将两个位图中的数按位与,按位与之后为1的位就是交集,消耗内存1G。
和第二题一样,这次有四种状态:00 表示没有出现, 01表示出现一次,10表示出现两次,11表示出现三次及以上。所以同样可以用两个位图实现。类似的如果有6种状态就需要3个位图表示。
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在” ,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
将字符串映射为整数,但是字符串可以说是无穷的,那么就一定存在映射冲突。
布隆过滤器对某个东西的判断情况
降低误判的方式
但是由于映射多个空间后,占用的位图空就比原来多了,所以位图的空间也要增加的。这也是减少冲突的有效方式。
所以位图空间的大小 m,映射函数的个数 k,插入元素的个数 n,怎么选择误判率p更低呢?
当然在条件允许的情况下,m 和 k越大越好,但是 m 的增大对误判率的减少是更有效的。
这里有个公式,可以选择合适的 k 和 m
布隆过滤器的优缺点
优点:
缺点:
#pragma once
#include
#include
// 三种字符串哈希方式
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;
}
};
// N 是要存储的个数, X是真正位图给的空间
template<size_t N, size_t X,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash>
class BloomFilter
{
void Set(const K& key)
{
size_t len = X * N;
size_t index1 = HashFunc1()(key) % len;
size_t index2 = HashFunc2()(key) % len;
size_t index3 = HashFunc3()(key) % len;
_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;
};
布隆过滤器通常应用在允许误判的场景之中
黑名单校验
发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。把所有黑名单都放在布隆过滤器中,再收到邮件时,判断邮件地址是否在布隆过滤器中即可。
身份验证:
比如注册账号时昵称或者用户名保证是不同的,当用户输入一个名称后,通过查找布隆过滤器,如果不存在就一定不存在,如果通过了布隆过滤器的判断,再去数据库中对比一次,这样通过一层布隆过滤器可以提高这个查找系统的效率
近似算法:
将文件1中的query映射到一个布隆过滤器中,读取文件2中的query,判断在不在布隆过滤器中,在就是交集。
但是这是不准确的,由于存在误判,会把一些不是交集的查询也统计出来。
精确算法:
使用哈希切分,即对读入的查询使用哈希函数将查询分组,分组的大小由数据量决定,A和B对应的组里面若存在交集,则是A和B的交集。这是因为它们使用同样的哈希算法。这样求出每份小文件的交集之和就行。
但是如果是大量的交集,或者很多数据映射到同一个小文件导致依然无法读到内存,就需要换个哈希函数继续切分。
mapcountMap
读取Ai中的IP统计出次数,一个文件读完,清空map
,然后再读另一个文件,使用优先级队列priority_queue>
存放每个文件最多的IP和对应次数,获得top K的IP。