目录
1. 位图
用例题来学习位图的使用场景:
40亿个不重复的无符号整数,如何快速判断某数是否存在。
- 方案1:排序+二分
- 方案2:红黑树
- 方案3:哈希表
都不可以,整型(32比特位)能表示42亿多个整数。
40亿个整数,需要多少内存?40亿字节约等于4G,一个整数4字节,40亿个整数约16G。
一般的计算机,系统不可能给程序分配16G的内存。红黑树一个节点要存整数本身,颜色,左右子树,父的指针。哈希表比红黑树消耗少一点点,哈希表存数本身和next节点指针。只存16G的数据都存不下,更不用说还有附带消耗了。
- 方案4:
这道题的需求是查找某值在还是不在,可以用一个比特位来标志某数的状态。0就是不存在,1就是存在。
使用哈希思想,将数据和地址映射。采用直接定址法,第0位表示整数0的状态,第1位表示整数1的状态....
总共需要2^32(约等于42亿)个比特位,就可以标志所有的整型。
- 为什么不是开40亿个比特位?
因为这40亿个数都在整型范围内,最大的数可能是40亿,可能是42亿。开空间的个数不是看数据个数,而是看数据的范围。
1.1 位图模拟
数组可以是char数组,也可以是int数组。一个char能标志8个数,一个int标志32个数,除此之外没什么不同的。
#include<vector>
namespace my
{
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize(N/8+1,0);//+1是为了避免不能整除的时候,把空间开小了,可能造成越界访问问题。
}
void set(size_t x)//x对应位变成1
{
size_t i = x / 8;//第i个字节
size_t j = x % 8;//x的哈希地址是第i个字节的第j位
_bits[i] |=(1 << j);
}
void reset(size_t x)//x对应位变成0
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= (~(1 << j));
}
bool test(size_t x)//检查x是否存在
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1<<j);//_bits不能改变。这种写法不可以:return (_bits[i]>>j) & 1
}
private:
vector<char> _bits;
};
void test_bitset()//测试
{
//bit_set<100> bs1;
//bit_set<-1> bs2;
bitset<0xffffffff> bs3;
bs3.set(0);
bs3.set(888888);
bs3.set(6);
bs3.set(888);
bs3.set(8888);
bs3.set(953962);
bs3.reset(6);
cout << "6" << ":" << bs3.test(6) << endl;
cout << "888888" << ":" << bs3.test(888888) << endl;
cout << "888" << ":" << bs3.test(888) << endl;
cout << "8888" << ":" << bs3.test(8888) << endl;
cout << "953962" << ":" << bs3.test(953962) << endl;
cout << "0" << ":" << bs3.test(0) << endl;
}
}
- 库中提供bitset容器
1.2 运用
1. 给定100亿个整数,设计算法找到只出现一次的整数?
需要三种状态,出现0次(00),1次(01),一次以上(10)。
- 方法一:可以修改位图,原来用1个字节标志状态,现在用两个字节标志状态。set的时候,如果是00,变成01,如果是01变成10,如果是10就不变。最终遍历一遍,找出所有是01的位。
- 方法二:用两个位图。
#include<bitset>
namespace twobitset
{
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
if (!_bs1.test(x) && !_bs2.test(x))//00
{
_bs2.set(x);//01
}
else if (!_bs1.test(x) && _bs2.test(x))//01
{
_bs1.set(x);//10
_bs2.reset(x);
}
//10 不变
}
void PrintOnce()
{
//01打印
for (size_t i = 0; i < N; i++)
{
if (!_bs1.test(i) && _bs2.test(i))
{
cout << i << " ";
}
}
cout << endl;
}
private:
std::bitset<N> _bs1;
std::bitset<N> _bs2;
};
void test()
{
twobitset<100> tbs;//所有数在100之内
int a[] = { 3,5,6,10,10,10,15,20,21,33 };
for (const auto& e : a)
{
tbs.set(e);
}
tbs.PrintOnce();
}
}
- 100亿个数会不会存不下?
可以存下。因为位图开多大是由数的范围决定。100亿个数一定在整型范围内,会有重复的数。
2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
找交集一定要去重。两个文件都放到位图里,test(x)都为1就是交集。
3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数。
四种状态:00出现0次,01出现1次,10出现2次,11出现两次以上。
找出所有01和10的数。
哈希切分
4.给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
不能用位图,因为要统计次数。文件过大,分批放到map里处理。
1.平均切分:100G,每10G分成一个小文件。每次取小文件统计次数,然后clear。再进下一个小文件。这样统计出的次数不是一个ip真正的次数,因为统计到只是当前小文件中出现的次数,其他文件里可能还有相同的ip。
2.哈希切分:i=HashFunc(ip)%100,i是多少就放进几号小文件。%100是为了保证每个小文件不超过1G。
ip相同的一定在同一个哈希桶里,这个桶里统计出的次数一定就是某ip的全部次数。
映射结束后,如果某一小文件的大小超过1G。可能是两种情况:
1.很多的相同ip。这种情况map可以统计。
2.很多的不同ip。map无法统计,更换哈希函数:用不同的算法将字符串转成整型和修改切割分数(%10或者模其他数都行)。
解决方法:超过1G的小文件,也用map统计,没报错就是情况1,出错了就捕获异常(情况2内存不够用,会insert失败),然后换一个哈希函数,递归再切分。
1.3 优缺点
- 优点:节省空间,快。
- 缺点:要求数据范围相对集中。如果非常分散,空间消耗变大。数据类型必须是整型。
string可以用位图存放吗?
不行。
- 字符串转换成整型会发生碰撞。同一个位置,可能映射多个字符串。
因为字符串是无限的,而整型是有限的。不同字符串转整型,可能算出来的整型就是一样的。(1.用了不好的算式。不同字符串算出一样的数 2.超出整型范围截断后导致碰撞)
2. 无法确定字符串范围。
直接定址法的缺陷就是如果数据分散会导致空间浪费。除非有办法确定字符串范围,不然每次都开-1个位也太浪费了。
2. 布隆过滤器
布隆过滤器适用于整型以外的数据类型。
对位图进行改进,通过映射多个位置的方式,降低误判率。
比如:一个字符串,用两个HashFunc转出两个整型,映射位图的两个位置。当两个位置都为1的时候,有可能这个字符串存在,也有可能还是和别的字符串冲突,但是映射多个位置能降低一些误判率。
2.1 应用场景
1.不要求百分百准确
布隆过滤器判断一个数存在是有可能会误判的,而判断一个数据不存在一定是准确的。
判断昵称是否重复:把所有已存在昵称放到布隆过滤器里面,布隆过滤器里能快速判断当前输入昵称不在。
客户端按ID查找某用户数据,数据存在服务器上的数据库。可以访问数据库之前,先访问布隆过滤器,如果布隆过滤器判断该ID不在,不需要再去数据库找,把不存在的ID过滤掉。如果布隆过滤器判断该ID存在(可能误判),再去数据库里找。当误判率是5%时,100个不存在的数据,5个判断成在,另外95个不存在直接过滤掉,提高了这95个数据的查找效率。
2.2 模拟
布隆过滤器采用的定址方法是除留余数法。开固定大小的空间。(空间开大开小都能映射,只是效率问题。空间大,碰撞概率低。空间小碰撞概率高。)
如何选择合适的函数个数k和过滤器长度m
k=m/n*ln2 ->【m=k/0.7*n】
#include<vector>
namespace my
{
struct BKDRHash
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto ch : key)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
struct APHash
{
size_t operator()(const string& key)
{
unsigned int hash = 0;
int i = 0;
for (auto ch : key)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));
}
++i;
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& key)
{
unsigned int hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
//N是数据个数
//X是平均存一个数据所需开辟的位数。合理的、误报率低的X=k/0.7
template <size_t N,
size_t X=6,
class K=string,
class HashFunc1= BKDRHash,
class HashFunc2= DJBHash,
class HashFunc3= APHash>
class BloomFilter
{
public:
void set(const K& key)
{
size_t hashi1 = HashFunc1()(key) % (X * N);
size_t hashi2 = HashFunc2()(key) % (X * N);
size_t hashi3 = HashFunc3()(key) % (X * N);
_bs.set(hashi1);
_bs.set(hashi2);
_bs.set(hashi3);
}
bool test(const K& key)
{
size_t hashi1 = HashFunc1()(key) % (N * X);
if (!_bs.test(hashi1))
{
return false;
}
size_t hashi2 = HashFunc2()(key) % (N * X);
if (!_bs.test(hashi2))
{
return false;
}
size_t hashi3 = HashFunc3()(key) % (N * X);
if (!_bs.test(hashi3))
{
return false;
}
return true;//可能误判
}
private:
std::bitset<N * X> _bs;
};
void test_bloomerfilter1()
{
string str[] = { "猪八戒", "孙悟空", "沙悟净", "唐三藏", "白龙马1","1白龙马","白1龙马","白11龙马","1白龙马1" };
BloomFilter<10,5> bf;
for (auto& str : str)
{
bf.set(str);
}
for (auto& s : str)
{
cout << bf.test(s) << endl;
}
cout << endl;
srand(time(0));
for (const auto& s : str)//测试误判,出现1就说明误判
{
cout << bf.test(s + to_string(rand())) << endl;
}
}
}
- 不能reset,一个位置可能被多给数据映射,ret直接置成0,会影响其他数据。
强行支持reset的方法,计数。修改位图,需要多个位来标志一个数。空间消耗成倍增加。
2.3 运用
两个文件,分别有100亿个query,只有1G内存,找交集。给出精确算法和近似算法。
query一般是查询指令(如网络请求)或sql语句。假设平均每个query是50字节,100亿个就是500G。
- 近似算法
近似算法对交集准确度没那么高,可以允许有不是交集的在放在交集里。使用布隆过滤器把A文件存入。遍历另一个文件,拿到B的query,test(query)。test返回true的就是交集。把交集放到一个容器里,最后去重。
因为布隆过滤器判断存在,是有可能误判的,即把不存在的数据判断成存在。布隆过滤器算出的交集一定包含所有真正的交集,不会漏。
- 精确算法
和哈希切分那道的处理方法相似。
如果平均切分,一个query本来是交集,A文件的可能在A1号小文件,B文件的可能在B3号小文件,没办法找交集。
把两个文件哈希切用相同哈希函数分成n个小文件(编号i=HashFunc()(query)%1000)。
A文件中重复的query都在同一个小文件里,B文件中重复的也一定在同一个小文件。两文件交集一定在文件号数相同的Ai和Bi小文件里。
精确算法就把Ai和Bi小文件分别放进set1和set2,可以去重,同时在set1和set2的就是交集。