文章目录
一.位图
位图法就是bitmap的缩写,所谓bitmap,是用每一位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存不存在的。
位图特点:
1.快,节省空间
2.相对局限,只能处理整形
1.1 位图的应用
1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记
例如:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。
1G = 1024MB 1MB = 1024KB 1KB = 1024 Byte 1 Byte = 8 bit
那么我们先看看40亿个不重复的无符号整数会占多少内存40亿整数 = 160亿字节
1G = 1024MB = 102410241024 Byte = 2^30 Byte ,就约等于10亿多字节
那么40亿个整数大概会占15~16G的内存
我们看下面的方法,在对于数据量太大的情况下都效果一般
- 排序(O(NlogN)),利用二分查找: logN (但由于数据量太大,只能存在磁盘中,不方便支持二分查找,效率低)
- 探索树和哈希表(由于探索树与哈希表中不止会存数据,还会存储其他的事物(如探索树会存储左右节点的指针),在使用时就可能会导致内存不足)
对于这类题,使用位图去解决就比较好
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在.
我们用位图的话就只需要(2^32 -1)个bit位,
(2^32 - 1)bit 约等于 512 MB,就相比上面的几种方法,内存消耗就小了许多
1.2 位图的实现
位图最主要功能的实现
template<size_t N>
class bitset
{
public:
bitset()
{
//多开一个字节的空间,防止出现10/8 = 0的情况
_bits.resize(N/8 + 1, 0);
}
void set(size_t x)
{
// x/8 在第几个char中
// x%8 在该char的第几个位置上
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;
};
二. 布隆过滤器
2.1 布隆过滤器的提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉
那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用
户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那
些已经存在的记录。 如何快速查找呢?
1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理
了。
3. 将哈希与位图结合,即布隆过滤器
2.2 布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间
2.3 布隆过滤器的插入
插入原理
模拟实现
//用多个Hash仿函数计算出多个散列值
struct HashBKDR
{
size_t operator()(const string& key)
{
rsize_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
struct HashAP
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB
{
size_t operator()(const string& key)
{
size_t hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N,
class K = string,class Hash1 = HashBKDR,
class Hash2 = HashAP,class Hash3 = HashDJB>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
_bits.set(hash1);
size_t hash2 = Hash2()(key) % (_ratio * N);
_bits.set(hash2);
size_t hash3 = Hash3()(key) % (_ratio * N);
_bits.set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
size_t hash2 = Hash2()(key) % (_ratio * N);
size_t hash3 = Hash3()(key) % (_ratio * N);
if(_bits.test(hash1) && _bits.test(hash2)&& _bits.test(hash3))
{
return true;//可能存在误判
}
return false;//准确的
}
private:
const static size_t _ratio = 5;
//插入一个值需要开的位数,哈希函数个数改变,这个也得改
bitset<_ratio*N> _bits;//实际存储个数
};
下面的是一篇写布隆过滤器的原理,使用场景和注意事项的文章链接,可以知道这里的_ratio是怎么来的。
https://zhuanlan.zhihu.com/p/43263751/
2.4 布隆过滤器的查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特
位一定为1。所以可以按照以下方式进行查找: 分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可
能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其
他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。
其查询元素的过程如下:
1. 通过k个无偏hash函数计算得到k个hash值
2. 依次取模数组长度,得到数组索引
3. 判断索引处的值是否全部为1,如果全部为1则存在(这种存在可能是误判),如果存在一个0则必定不存在
2.5 布隆过滤器删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也
被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
三 海量数据的题目
3.1 位图的题目
1.给定100亿个整数,设计算法找到只出现一次的整数?
方法一:
由于需要找只出现一次的整数,就不能单纯的向上面一样只记录出现了没有,我们这里要记录能出现一次的,那么我们就可以将它分成【出现0次,出现1次,出现2次及以上】的三种情况,那么我们在设计的时候就只需要2个比特位来映射一个值就行了
方法二:
对于这种,除了用2在一个位图中用两个比特位来映射以外,我们还能用2个位图用相同的一个比特位来进行组合
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
bool insert1 = _bs1.test(x);
bool insert2 = _bs2.test(x);
//00
if (insert1 == false && insert2 == false)
{
//->01
_bs2.set(x);
}
//01
else if (insert1 == false && insert2 == true)
{
//->10
_bs1.set(x);
_bs2.reset(x);
}
}
void print_once_num()
{
for (size_t i = 0; i < N; i++)
{
if (_bs1.test(i) == false && _bs2.test(i) == true)
{
cout << i << endl;
}
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
void Test_twobitset()
{
int arr[] = { 1,2,3,1,2,3,5,6,8 };
twobitset<100> tws;
for (auto e : arr)
{
tws.set(e);
}
tws.print_once_num();
}
2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
,先将2个文件都分成n个小文件,再用2个位图对相应的小文件文件进行映射,每2个相应的小文件在位图中的位置都为一的都是2个文件的交集(哈希切分)
3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
这道题与题目一类似,只不过这道题的分类变为了【出现0次,出现1次,出现2次,出现3次及以上】这4种情况,就需要用 00(0次),01(1次),10(2次),11(3次及以上)来代表这4种情况。
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
bool insert1 = _bs1.test(x);
bool insert2 = _bs2.test(x);
//00
if (insert1 == false && insert2 == false)
{
//->01
_bs2.set(x);
}
//01
else if (insert1 == false && insert2 == true)
{
//->10
_bs1.set(x);
_bs2.reset(x);
}
}
void print_once_num()
{
for (size_t i = 0; i < N; i++)
{
if ((_bs1.test(i) == false && _bs2.test(i) == true)||
_bs1.test(i) == true && _bs2.test(i) == false)
{
cout << i << endl;
}
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
3.2 布隆过滤器的题目
1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
精确算法
哈希切分
1.假设每个query 占30byte,那么100亿个query约要占300G的内存
2.我们需要将2个300G内存的数据文件,只用1G的内存找他们的交集,我们把这两个大文件A,B分别按照hash切分,分成1000分的小文件,这样就可以放到内存中,然后我们拿A中的小文件分别和B中的比对,这样就可以找到两个文件的交集
估计算法步骤:
(1) 通过字符串哈希算法,将字符串转换成数字
(2) 通过散列函数将这些数字映射到内存中
(3)判断映射的位置是否都存在,存在表明有交集.
2.如何让布隆过滤器能支持删除
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
- 无法确认元素是否真正在布隆过滤器中
- 存在计数回绕
3.3 哈希切割
给一个超过100G大小的log file, log中存着IP地址,
设计算法找到出现次数最多的IP地址?
我们先考虑一下,100G大小的文件,一般是无法存到普通的计算机中的,我们的硬盘根本没这么大; 我们可以使用前面讲的位图,一个整形32位,最多可以存42亿多的数据,100G的大文件,最多需要3.2G就可以放进去,但是却难以统计最多的IP地址。
为了解决上面的问题,我们可以把大文件放到小文件中,再来统计就会很容易 。
算法思想:哈希切分
(1)哈希函数hash(IP)%1000个文件,这样大文件就可以分成1000个小文件。
(2)字符串哈希函数将字符串转换为整数
(3)通过同一个散列函数映射到相应的文件,此时同一个ip一定会映射到同一个文件
(4)用<ip,次数>统计,用map或者unordered_map实现
与上题条件相同,如何找到top K的IP?
(1)本题采用哈希切分,如上题,统计每个ip出现的次数
(2)用文件的前K个数建小堆
(3)用K+1个数和堆顶相比,大的话替换,调整堆