位图概念
- 【面试题】
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
三种解法如下:
- 遍历,时间复杂度O(N)。
- 排序 ( O ( N l o g N ) ) (O(NlogN)) (O(NlogN)),利用二分查找: l o g N logN logN(有足够的内存去排序)
- 位图解决(推荐解法)
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。 比如:
- 位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
位图的实现
class bitset
{
public:
bitset(size_t bitCount) :
_bit((bitCount >> 5) + 1),
_bitCount(bitCount)
{}
// 将which比特位置1
void set(size_t which) {
if (which > _bitCount)
return;
size_t index = (which >> 5);
size_t pos = which % 32;
_bit[index] |= (1 << pos);
}
// 将which比特位置0
void reset(size_t which) {
if (which > _bitCount)
return;
size_t index = (which >> 5);
size_t pos = which % 32;
_bit[index] &= ~(1 << pos);
}
// 检测位图中which是否为1
bool test(size_t which) {
if (which > _bitCount)
return false;
size_t index = (which >> 5);
size_t pos = which % 32;
return _bit[index] & (1 << pos);
}
// 获取位图中比特位的总个数
size_t size()const
{
return _bitCount;
}
// 位图中含比特为1的个数
size_t count()
{
// \数字 表实对应的数字(八进制省略0)
const char* pCount = "\0\1\1\2\1\2\2\3\1\2\2\3\2\3\3\4";
// 0 1 2 3 4 5 6 7 8 9 a b c d e f(十六进制中含1的个数)
size_t size = _bit.size();
size_t count = 0;
size_t i;
for (i = 0; i < size; i++)
{
int value = _bit[i];
int j;
// sizeof(_bit[0]) 是为了防止某些系统的int大小不固定
for (j = 0; j < sizeof(_bit[0]); j++, value >>= 8)
{
// 利用c进行截断(value32位,取低8位)
char c = value;
// 先看C的低4位,再看高4位(一次统计一个字节)
count += pCount[c & 0x0f];
c >> 4;
count += pCount[c & 0x0f];
}
}
return count;
}
private:
vector<int> _bit;
size_t _bitCount;
};
位图的应用
-
快速查找某个数据是否在一个集合中
-
排序
-
求两个集合的交集、并集等
-
操作系统中磁盘块标记
位图习题:
- 给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同, 如何找到top K的IP?如何直接用Linux系统命令实现?
Hash分桶法:
将100G文件分成1000份,将每个IP地址映射到相应文件中:file_id = hash(ip) % 1000
在每个文件中分别求出最高频的IP,再合并Hash分桶法;
使用Hash分桶法把数据分发到不同的文件;
各个文件分别统计top K;
Linux系统命令:sort log_file | uniq -c | sort -nr | head -K(得到前几名)
2. 给定100亿个整数,设计算法找到只出现一次的整数?
Hash分桶法,将100亿个整数映射到不同的区间,在每个区间中分别找只出现一次的整数。
利用位图(设置两个位图),遍历每个区间的整数,遍历的过程中给现的数在第一个位图中置1,如果遍历的过程中,发现这一位是1,则给第二个位图中此位置置1,这样遍历结束后,对比两个位图,找到只出现一次的整数(第一个位图位置为1,第二个位图位置为0,再还原回去,得到此数)
- 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
解法1 : 一个int的位存为512MB(这里的整数是32bit的话)
利用位图(设置两个位图,两个文件各给一个位图)。遍历第一个文件给出现的数在第一个位图中置1,遍历第二个文件给出现的数在第二个位图中置1,对比两个位图,找到相同位上皆为1,找到交集。
解法2 : 保险的方法应该是使用:桶分+组内bitmap。
使用hash函数将第一个文件的所有整数映射到1000个文件中,每个文件有1000万个整数,大约40M内存,
内存可以放下,把1000个文件记为 a1,a2,a3…a1000,用同样的hash函数映射第二个文件到1000个文件中,这1000个文件记为b1,b2,b3…b1000,由于使用的是相同的hash函数,所以两个文件中一样的数字会被分配到文件下标一致的文件中,分别对a1和b1求交集,a2和b2求交集,ai和bi求交集,最后将结果汇总,即为两个文件的交集
- 1个文件有100亿个int,1G内存,设计算法找到出现次数超过2次的所有整数
Bitmap扩展:用2个bit表示状态,00表示未出现,01出现过1次,10出现过2次,11表示出现过2次以上。
布隆过滤器
布隆过滤器提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:不能处理哈希冲突
- 将哈希与位图结合,即布隆过滤器
布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
向布隆过滤器中插入:"baidu"
// 假设布隆过滤器中元素类型为K,每个元素对应5个哈希函数
template<class K, class KToInt1 = KeyToInt1, class KToInt2 = KeyToInt2,
class KToInt3 = KeyToInt3, class KToInt4 = KeyToInt4,
class KToInt5 = KeyToInt5>
class BloomFilter
{
public:
BloomFilter(size_t size) : // 布隆过滤器中元素个数
_bmp(5*size),
_size(0)
{}
bool Insert(const K& key)
{
size_t bitCount = _bmp.Size();
size_t index1 = KToInt1()(key) % bitCount;
size_t index2 = KToInt2()(key) % bitCount;
size_t index3 = KToInt3()(key) % bitCount;
size_t index4 = KToInt4()(key) % bitCount;
size_t index5 = KToInt5()(key) % bitCount;
_bmp.Set(index1); _bmp.Set(index2); _bmp.Set(index3);
_bmp.Set(index4); _bmp.Set(index5);
_size++;
}
private:
bitset _bmp;
size_t _size; // 实际元素的个数
}
布隆过滤器的查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。 所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零, 代表该元素一定不在哈希表中,否则可能在哈希表中。
bool IsInBloomFilter(const K& key)
{
size_t bitCount = _bmp.Size();
size_t index1 = KToInt1()(key) % bitCount;
if (!_bmp.Test(index1))
return false;
size_t index2 = KToInt2()(key) % bitCount;
if (!_bmp.Test(index2))
return false;
size_t index3 = KToInt3()(key) % bitCount;
if (!_bmp.Test(index3))
return false;
size_t index4 = KToInt4()(key) % bitCount;
if (!_bmp.Test(index4))
return false;
size_t index5 = KToInt5()(key) % bitCount;
if (!_bmp.Test(index5))
return false;
return true; // 有可能在
}
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。
布隆过滤器删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了, 因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
-
无法确认元素是否真正在布隆过滤器中
-
存在计数回绕 (计数器超过上限)
布隆过滤器优点
-
增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
-
哈希函数相互之间没有关系,方便硬件并行运算
-
布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
-
在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
-
数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
-
使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器缺陷
-
有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
-
不能获取元素本身
-
一般情况下不能从布隆过滤器中删除元素
-
如果采用计数方式删除,可能会存在计数回绕问题
布隆过滤器习题:
- 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
- 精确算法:Hash分桶法
将两个文件中的query hash到N个小文件中,并标明query的来源;
在各个小文件中找到重合的query
将找到的重合query汇总
- 近似算法:BloomFilter算法
将第一个文件按照布隆过滤器放入位图中,然后从第二个文件中取出数字,在第一个位图查找,能找到则可能为交集。如果有一位找不到,则不存在交集。
- 如何扩展BloomFilter使得它支持删除元素的操作
将BloomFilter中的每一位扩展为一个计数器,记录有多少个hash函数映射到这一位;删除的时候,只有当引用计数变为0时,才真正将该位置为0。
倒排索引
正序索引: 文件索引单词(统计某个单词在文件中出了几次)
倒排索引: 单词索引文件(统计单词都在那些文件中出现过)
倒排索引创建索引的流程:
1) 首先把所有的原始数据进行编号,形成文档列表
2) 把文档数据进行分词,得到很多的词条,以词条为索引。保存包含这些词条的文档的编号信息。
题目:(倒排索引,也可)
给上千个文件,每个文件大小为1K—100M。给n个词,设计算法对每个词找到所有包含它的文件,你只有 100K内存
解答:
我们可以使用布隆过滤器来判断一个文件是否包含这n个单词生成n个布隆过滤器放到外存,我们事先定义好一个包含这n个单词信息的文件info,每当我们在一个文件找到一个对应的单词就将这个文件的信息写入info对应单词的位置。我们只有100K内存,这100K内存我们一部分用来存放布隆过滤器一部分可以存放文件,因为文件最小都为100K,所以我们可以尝试把它切分为50K的小文件,每个文件标志好所属的大文件,这样我们每次读入一个布隆过滤器和一个小文件,如果这个文件有对应的单词则在info中标记所属大文件的信息,如果没有则读入下一个布隆过滤器,把所有布隆过滤器都使用后,再读下一个文件重复上述步骤直至把所有文件都遍历完。
如有不同见解,欢迎留言讨论~~