文章目录
1.位图
1.1位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
1.2位图的实现
BitSet.hpp
#pragma once
#include<vector>
namespace bite
{
class bitset
{
public:
bitset(size_t bitCount)
:_set(bitCount/8+1)
,_bitCount(bitCount)
{}
//置1
void set(size_t which)
{
if (which >= _bitCount)
return;
//计算对应的字节
size_t index = (which>>3);
size_t pos = which % 8;
_set[index] |= (1 << pos);
}
//置0
void reset(size_t which)
{
if (which >= _bitCount)
return;
//计算对应的字节
size_t index = (which >> 3);
size_t pos = which % 8;
_set[index] &= ~(1 << pos);
}
//检测which比特位是否为1
bool test(size_t which)
{
if (which >= _bitCount)
return false;
//计算对应的字节
size_t index = (which >> 3);
size_t pos = which % 8;
return 0!=(_set[index] & (1 << pos));
}
//返回比特位总得个数
size_t size()
{
return _bitCount;
}
//返回为1的比特位的总数
size_t count()
{
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 szcount = 0;
for (size_t i = 0; i < _set.size(); i++)
{
szcount+=bitCnttable[_set[i]];
}
return szcount;
}
private:
vector<unsigned char> _set;
size_t _bitCount;
};
}
void TestBitSet()//测试
{
bite::bitset bt(100);
bt.set(10);
bt.set(20);
bt.set(30);
bt.set(40);
bt.set(41);
cout << bt.size() << endl;
cout << bt.count() << endl;
if (bt.test(40))
cout << "40 bite is 1" << endl;
else
cout << "40 bite is not 1" << endl;
bt.reset(40);
cout << bt.count() << endl;
if (bt.test(40))
cout << "40 bite is 1" << endl;
else
cout << "40 bite is not 1" << endl;
}
1.3 位图的应用
- 快速查找某个数据是否在一个集合中
- 排序(数据不能有重复)
- 求两个集合的交集、并集等
交集:映射两个位图,查看相同为1的就是交集
并集:映射两个位图,有1的就是并集 - 操作系统中磁盘块标记
2.布隆过滤器
2.1布隆过滤器提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:不能处理哈希冲突
- 将哈希与位图结合,即布隆过滤器
2.2布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
2.3布隆过滤器的插入
以下图片来自知乎:Young Chen
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如以下针对值:
1.首先是向布隆过滤器插入"baidu” ,假如"baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,如下图:
2.接着是向布隆过滤器插入"tencent” ,如果"tencent” 的三个不同的哈希函数分别生成了哈希值 3、4、8,如下图:
3.由上图可以知道,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “huawei” 这个值是否存在,假如这个值的哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,这说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “huawei” 这个值一定不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。
可能存在的原因:
随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样像某个值 “jingdong” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “jingdong” 这个值存在。
Common.hpp<----代码来源链接
#pragma once
#include <string>
class StrINT1
{
public:
size_t operator()(const std::string& s)
{
return SDBMHash(s.c_str());
}
unsigned int SDBMHash(const char *str)
{
unsigned int hash = 0;
while (*str)
{
// equivalent to: hash = 65599*hash + (*str++);
hash = (*str++) + (hash << 6) + (hash << 16) - hash;
}
return (hash & 0x7FFFFFFF);
}
};
struct StrINT2
{
public:
size_t operator()(const std::string& s)
{
return RSHash(s.c_str());
}
// RS Hash Function
unsigned int RSHash(const char *str)
{
unsigned int b = 378551;
unsigned int a = 63689;
unsigned int hash = 0;
while (*str)
{
hash = hash * a + (*str++);
a *= b;
}
return (hash & 0x7FFFFFFF);
}
};
struct StrINT3
{
public:
size_t operator()(const std::string& s)
{
return JSHash(s.c_str());
}
// JS Hash Function
unsigned int JSHash(const char *str)
{
unsigned int hash = 1315423911;
while (*str)
{
hash ^= ((hash << 5) + (*str++) + (hash >> 2));
}
return (hash & 0x7FFFFFFF);
}
};
struct StrINT4
{
public:
size_t operator()(const std::string& s)
{
return PJWHash(s.c_str());
}
// P. J. Weinberger Hash Function
unsigned int PJWHash(const char *str)
{
unsigned int BitsInUnignedInt = (unsigned int)(sizeof(unsigned int) * 8);
unsigned int ThreeQuarters = (unsigned int)((BitsInUnignedInt * 3) / 4);
unsigned int OneEighth = (unsigned int)(BitsInUnignedInt / 8);
unsigned int HighBits = (unsigned int)(0xFFFFFFFF) << (BitsInUnignedInt - OneEighth);
unsigned int hash = 0;
unsigned int test = 0;
while (*str)
{
hash = (hash << OneEighth) + (*str++);
if ((test = hash & HighBits) != 0)
{
hash = ((hash ^ (test >> ThreeQuarters)) & (~HighBits));
}
}
return (hash & 0x7FFFFFFF);
}
};
struct StrINT5
{
public:
size_t operator()(const std::string& s)
{
return ELFHash(s.c_str());
}
// ELF Hash Function
unsigned int ELFHash(const char *str)
{
unsigned int hash = 0;
unsigned int x = 0;
while (*str)
{
hash = (hash << 4) + (*str++);
if ((x = hash & 0xF0000000L) != 0)
{
hash ^= (x >> 24);
hash &= ~x;
}
}
return (hash & 0x7FFFFFFF);
}
};
struct StrINT6
{
public:
size_t operator()(const std::string& s)
{
return BKDRHash(s.c_str());
}
// BKDR Hash Function
unsigned int BKDRHash(const char *str)
{
unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return (hash & 0x7FFFFFFF);
}
};
struct StrINT7
{
public:
size_t operator()(const std::string& s)
{
return DJBHash(s.c_str());
}
// DJB Hash Function
unsigned int DJBHash(const char *str)
{
unsigned int hash = 5381;
while (*str)
{
hash += (hash << 5) + (*str++);
}
return (hash & 0x7FFFFFFF);
}
};
struct StrINT8
{
public:
size_t operator()(const std::string& s)
{
return APHash(s.c_str());
}
// AP Hash Function
unsigned int APHash(const char *str)
{
unsigned int hash = 0;
int i;
for (i = 0; *str; i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));
}
}
return (hash & 0x7FFFFFFF);
}
};
BloomFilter.hpp
#pragma once
#include"BitSet.hpp"
#include "Common.hpp"
//BloomFilter:位图+多个哈希
template<class T,class HF1=StrINT1,class HF2= StrINT2
,class HF3= StrINT3, class HF4= StrINT4
, class HF5= StrINT5>
class BloomFilter
{
public:
BloomFilter(size_t size=10)
:_bt(10*size)
,_size(0)
{}
bool Insert(const T& data)
{
size_t index1 = HF1()(data) % _bt.size();
size_t index2 = HF2()(data) % _bt.size();
size_t index3 = HF3()(data) % _bt.size();
size_t index4 = HF4()(data) % _bt.size();
size_t index5 = HF5()(data) % _bt.size();
_bt.set(index1);
_bt.set(index2);
_bt.set(index3);
_bt.set(index4);
_bt.set(index5);
return true;
}
bool IsIn(const T& data)//布隆过滤器的查找
{
size_t index = HF1()(data) % _bt.size();
if (!_bt.test(index))
return false;
index = HF2()(data) % _bt.size();
if (!_bt.test(index))
return false;
index = HF3()(data) % _bt.size();
if (!_bt.test(index))
return false;
index = HF4()(data) % _bt.size();
if (!_bt.test(index))
return false;
index = HF5()(data) % _bt.size();
if (!_bt.test(index))
return false;
_size++;
//元素可能在
return true;
}
size_t Size()const
{
return _size;
}
private:
bite::bitset _bt;
size_t _size;
};
void TestBloomFilter()//测试
{
//坏人名单
BloomFilter<string> bf;
bf.Insert("本拉登");
bf.Insert("希特勒");
bf.Insert("裕仁天皇");
cout << bf.Size() << endl;
if (bf.IsIn("蒋介石"))
cout << "坏人" << endl;
else
cout << "好人" << endl;
if (bf.IsIn("裕仁天皇"))
cout << "坏人" << endl;
else
cout << "好人" << endl;
}
2.4布隆过滤器的查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意: 布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
比如: 在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。
2.5布隆过滤器删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
- 无法确认元素是否真正在布隆过滤器中
- 存在计数回绕
2.6布隆过滤器优缺点
优点:
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点:
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白
名单,存储可能会误判的数据) - 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
3.海量数据面试题
3.1哈希切割
1.1给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
文件100G大文件—>分割—>1000份---->100M
分割大文件:如果能将相同的IP地址放到同一个文件中
如果有内存限制(比如1G)—>文件总大小/内存限制
哈希分割:
从源文件中获取一个IP地址—>IP(可逆转化为整型数据)%文件份数
统计每个IP地址出现的次数<IP,次数>即:<整型数据,次数>
利用unordered_map
m[ip]++;
然后for()—>找到出现次数最多的ip地址
1.2与上题条件相同,如何找到top K的IP?
堆—>最多前k个ip地址—><次数,ip地址>
1.3如何直接用Linux系统命令实现?
3.2位图应用
2.1 给定100亿个整数,设计算法找到只出现一次的整数?
2个比特位表示数据(一个字节只能表示4个数据):00、01、10、11(需要空间:2^32/4=1G)
00:没有出现
01:出现一次
10:出现多次
11:不用
取一个数据:对应哪个字节,哪两个比特位
if(00)
01
else if(01)
10
2.2 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
映射两个文件需要空间:512MB+512MB=1G
查看相同比特位是否为1:1是交集
2.3 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
类似第一题
2.4 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
位图解决
3.3布隆过滤器
3.1 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
3.2 如何扩展BloomFilter使得它支持删除元素的操作
3.4倒排索引
4.1给上千个文件,每个文件大小为1K—100M。给n个词,设计算法对每个词找到所有包含它的文件,你只有100K内存
4.常见的面试题
4.1 什么是哈希冲突?
4.2 处理哈希冲突的方法有哪些?优缺点分别是什么?
4.3 哈希表的增删查改的时间复杂度是多少?开放定制法哈希冲突很严重怎么办?哈希桶哈希冲突很严重怎么办?
4.4 海量数据处理的面试题?
4.5 unordered_map和map的区别是什么?哪一个更好一些?