位图 (Bitmap) 是一种基于位操作的数据结构,用于表示一组元素的集合信息。它通常是一个仅包含0和1的数组,其中每个元素对应集合中的一个元素。位图中的每个位(或者可以理解为数组的元素)代表一个元素是否存在于集合中。当元素存在时,对应位的值为1;不存在时,对应位的值为0。位图常用于判断某个元素是否属于某个集合,或者对多个集合做交集、并集或差集等集合运算。
位图本质是个数组,用来存放0和1。
位图通过自身数组中的每个位来代表集合(我们要处理的数据)中的元素,每个位是0或1,代表元素的存在与否(0,不存在;1,存在)。
我们先看一下下面的例子:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
1.首先当然可以遍历这40亿个数,这样效率会太低了,肯定直接pass掉.
2.也可以考虑搜索树或者哈希表,操作之前一定要把数据先存下来构建树或表。但是仔细一想,40亿个数,占多少空间?
40亿个无符号整数,每个大小4个字节,则一共占用160亿字节。
1GB = 1024 MB, 1MB = 1024KB, 1KB = 1024Byte(字节).
所以一共占用16000000000/1024/1024/1024≈14.9G.
所以要用这些方法的话一上来就占用14.9G的系统内存,大多数电脑是吃不消的,更别说搜索树结点还存其他属性信息,现在内存16G的电脑也不算很多。这肯定不行的.
3.当然还可以存在磁盘上,进行外排序+二分查找。但是想一下,这一切是在磁盘上进行,磁盘的速度可是非常慢的,而且要对这么多数据排序,以及在磁盘上进行二分查找,无论代码还是时间都是复杂和比较慢的.
下一个方法,就是我们大名鼎鼎的位图来解决了,具体是怎么样的呢?
首先,我们要给数组开辟一块空间,这片空间我们开多少呢,要根据数据范围开空间,而不是数据个数。
这个我相信大家都能理解,毕竟如果只有两个数,一个是1,一个数是40亿,按数据个数开两个空间的话只能存下1和2,40亿肯定是存不下的。
所以无符号整型的数据范围是0~2^32-1,所以我们要开这么大的空间.那么一共多大呢?
由于我们是使用比特位进行表示每一个数是否存在的,所以相当于是2^32-1个比特位。由于1byte=8bit.根据上面所说,一共占用(2^32-1)/8/1024/1024/1024=0.5GB=512MB.
所以我们开好空间以后,我们只需要将数对应的位置为1即可.
比如数据{1,4,9,15,17,23},在位图中是什么样的?
这样到时候直接判断对应位置是不是1即可判断某个数是否存在.
具体怎么设置,取消,判断看下面。
主要包含三个核心接口:设置(设为1)、重置(设为0)、判断(是0还是1)。
对于每一步可以看注释.
#include
#include
using namespace std;
namespace ayf
{
//N代表数据范围
template
class bit_set
{
public:
bit_set()
{
_bits.resize(N / 8 + 1, 0);
}
void set(size_t x)
{
//由于一个组是char,所以x/8是计算在哪个char组里
size_t i = x / 8;
//一个char里有8位,x%8是计算出在char组里面的具体哪一位
size_t j = x % 8;
//这个建议大家画图理解,首先_bits[i]是对应的char组,然后1< _bits;
};
}
接下来你可以测试一些数据,比如set一些或reset一些.
当然如果想创建无符号整数的测试集,范围是0~2^32-1,可以直接用-1替代,因为-1的二进制是全1.
转化为无符号整数就是最大的。或者0xffffffff。同样地道理。如下
hyx::bit_set<-1> bs1;
这样创建即可.
位图常见三道面试题
下面来几个位图的拓展题:
1.给定100亿个整数,设计算法找到只出现一次的整数?
首先100亿个整数会不会有空间的问题?答案是肯定不会的,因为开空间和数据范围有关系,和数据个数没有关系,100亿个整数,每个数的范围都是42亿(2^32-1)之内,不会说有100亿个不重复的整数.
首先问题是找出现一次的整数,很明显是个key-value模型。那这至少需要两个位图了。所以就需要建立双位图解决.
既然求出现一次,那么肯定是以下三种情况:(左边次数,右边对应的位图状态)
0次 00
1次 01
2次及以上 10
所以上面可知,第二个位图是由两个位进行表示.这样又得需要各种控制,也是不太方便.
其实位图也是一个数据结构,STL库中也有对应的容器——bitset.
那我们直接用两个位图分别表示这两个位不就可以了吗,当然!
所以我们要自己建立一个有两个位图的类,如下
整体思路是:依次判断两个位:
1.若为00,说明这个数一次也没出现过,将其改为01.即将第二个位图设为1.
2.若为01,说明这个数出现了一次,将其改为10,即第一个位图设为1,第二个位图设为0.
后面可以写一个成员函数来输出符合条件的数:
template
class twobitset
{
public:
void set(size_t x)
{
bool inset1 = _bs1.test(x);
bool inset2 = _bs2.test(x);
//00
if (inset1 == false && inset2 == false)
{
//->01
_bs2.set(x);
}
else if (inset1 == false && inset2 == true)
{
//->10
_bs1.set(x);
_bs2.reset(x);
}
}
void print_once_num()
{
for (int i = 0; i < N; i++)
{
//筛选出两个位图为01的数
if (_bs1[i] == false && _bs2[i] == true)
{
cout << i << endl;
}
}
}
private:
bitset _bs1;
bitset _bs2;
};
void test_oncenum()
{
int a[] = { 1,1,2,3,4,5,5,5,6,6,6,6,7,9,22 };
twobitset<100> bs;
for (auto e : a)
{
bs.set(e);
}
bs.print_once_num();
}
然后我们调用测试函数,得到:
可以发现已经输出了出现个数只为1的数.
2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
这道题比较简单,思路是建立两个位图,然后分别把两个文件里的数据set到位图里,然后最后将两个位图&一下,然后再从最后的结果中找位是1的即可(利用test).
3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数.
这个题和1题类似,无非就是多了一种状态:3次及3次以上。最后对应的状态是以下这样:
0次 00
1次 01
2次 10
3次及3次以上 11
然后要在twobitset的set中多加一个判断条件:当位图位10时,下一次改为11.
//10
else if (inset1 == true && inset2 == false)
{
//->11
_bs2.set(x);
}
然后输出的时候变化条件为bs1[i]==true和bs2[i]==true(11)即可.
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中
如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为O(n),O(log n),O(1)。
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure)高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
当你往简单数组或列表中插入新数据时,将不会根据插入项的值来确定该插入项的索引值。这意味着新插入项的索引值与数据值之间没有直接关系。这样的话,当你需要在数组或列表中搜索相应值的时候,你必须遍历已有的集合。若集合中存在大量的数据,就会影响数据查找的效率。
针对这个问题,你可以考虑使用哈希表。利用哈希表你可以通过对 “值” 进行哈希处理来获得该值对应的键或索引值,然后把该值存放到列表中对应的索引位置。这意味着索引值是由插入项的值所确定的,当你需要判断列表中是否存在该值时,只需要对值进行哈希处理并在相应的索引位置进行搜索即可,这时的搜索速度是非常快的。
举例说明:
查找字符"美团"是否存在时,会找到
这三个绿色的位置,看看是否都为1
首先,布隆过滤器的底层也是位图,所以
只需封装一层即可实现一个布隆过滤器!
但实现布隆过滤器的关键有以下几个
一个字符串映射几个位置?
怎样把字符串转换为整数?
一般而言,一个字符串映射的越多,那么误判率就越低,但是映射过多会导致不同的字符串映射到相同的位置,所以一般映射三个位置,并且将字符串转换为整数也就需要三种不同的方法,我在网上找了一些字符串转整数的算法,请看下面的代码:
//三个不同的字符串映射成整数的函数
struct HashBKDR
{
size_t operator()(const string& key)
{
size_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;
}
};
将这三个仿函数传入类,用于字符串转整型
布隆过滤器的实现:
// N表示准备要映射N个值
template
class Bloom_Filter
{
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);
if (!_bits->test(hash1))
return false; // 准确的
size_t hash2 = Hash2()(key) % (_ratio * N);
if (!_bits->test(hash2))
return false; // 准确的
size_t hash3 = Hash3()(key) % (_ratio * N);
if (!_bits->test(hash3))
return false; // 准确的
return true; // 可能存在误判
}
void reset(const K& key)//支持删除操作的话,可能会把其他数据对应的映射值删除
{}
private:
const static size_t _ratio = 5;//开的空间越大,误判率越小
std::bitset<_ratio* N>* _bits = new std::bitset<_ratio * N>;//标准库中的位图是在栈上开辟的静态数组,过大会栈溢出
};
布隆过滤器的查找是一个很玄幻的过程:
分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定
不在哈希表中,否则可能
在哈希表中
因为哈希函数可能存在冲突的原因,如下:
所以我们得出一个结论:
布隆过滤器说一个元素存在,那它可能存在
布隆过滤器说一个元素不在,那它一定不在
布隆过滤器的删除操作:
如果你理解了上面的内容,你一定能明白布隆过滤器是不支持删除的,因为删除一个关键字时可能将其他的关键字的一部分也给删除了,因为一个bit位只能存储一个二进制信息!
处理海量数据的面试题
位图的应用:
布隆过滤器的应用: