位图是啥
首先看一道题:
首先像这种判断在不在的问题,我们第一想到的肯定是用set或者unordered_set。但是当数据量过大时,直接使用set这种就不适用了。40亿个整形的大小大概在16G左右,这样的话太大了。
但是仅仅只是判断在不在的问题时,我们没必要如此大费周章,我们可以用第几位的比特位是0还是1来表示这个数存不存在,这样原本要16G的大小就变成0.5G。这就是位图的思想,跟位运算有着很大的关系。
位运算的复习
常用的位运算:
1. &:按位与
如果同时为1则为1,其余情况都是0.
2. |: 按位或
如果同时为0则为0,其余情况都是1.
3. ~:取反
字面意思,1变成0,0变成1.
4. ^:异或
相同就是0,不相同就是1.
运算技巧
一个int a。a^(a-1)可以求出最右端的一个1。
现有int a,int b,a^b就是无进位相加。然后可以用(a&b)<<1,来获取最右端的一个进位。
深度解析异或
异或有些规律非常好用:
假设有一个整形 a。
1.一个数异或它本身等于0,比如a^a=0(*消消乐*)(重要)。
2.一个数异或0等于它本身,比如a^0=a。
3.它满足交换律和结合律。
数的存储规则复习
数字在计算机中是以补码的形式储存的,运算也是用的补码运算。
1.原码:
就是我们对一个数转换成二进制。
2.反码:
正码通过取反就是反码。
3.补码:
反码加1就是补码。
这三种码注意转换顺序。注意:正数的原码,反码,补码均相同。
为什么计算机需要搞这么多码来储存呢?(补充)
正数的相加减很容易想到并实现,但是对于负数该怎么办呢?计算机如何知道这个数是一个负呢?所以计算机将二进制的最高位不存储数据,将他作为符号位,0就说明该数是正数,1就说明该数是负数。
现在判断正数还是负数搞定了,那么正数与负数相加怎么做呢?我们来举例1+(-1)=0.1的补码是…………(省略31个0)1,-1的补码为1…………(省略30个1)1。因为最高位不参与运算,所以它们相加时,由于溢出,除了符号位都变成了0。于是就解决了正数和负数相加的问题。
那么现在还是可以来思考一个问题。1+(-1)后,符号位是0还是1呢?答案是0。毕竟0看起来好像可以用符号位为1的也可以用0的来表示,但是没有必要并且可能会有歧义。所以统一用符号位为0的来表示。那么1………………0不就空出来了?当然也不会让它闲着,于是它就是代表了INT_MIN。这就解释了为什么了int类型的大小范围是-2,147,483,648 到 2,147,483,647。当然扯这么多如果是无符号位(unsigned)那就没事了。
位图的实际运用
所以我们知道了位图的基本思想就是通过用比特位来解决海量数据的问题。以下先来简单的模拟实现一个位图,再来看看具体样例。
位图的应用:
先康康STL中位图的一些函数
代码模拟实现
namespace hzj
{
template<size_t N>
class bitset
{
public:
bitset()
{
_a.resize(N / 32 + 1);//开辟空间
}
//映射x位修改为1
void set(const size_t& x)
{
int n1 = x / 32;
int n2 = x % 32;
_a[n1] |= (1 << n2);
}
//映射x位修改为0
void reSet(const size_t& x)
{
int n1 = x / 32;
int n2 = x % 32;
_a[n1] &= (~(1 << n2));
}
//检查该位是否存在
bool test(const size_t& x)
{
int n1 = x / 32;
int n2 = x % 32;
return _a[n1] & (1 << n2) ;
}
private:
vector<int> _a;
};
}
可以看出位图的核心操作就是将值映射到比特位。
扩展示例:
1.100亿个整数,如何找到只出现一次的数?
思路:
对于每一个值,我们可以用两个比特位来表示。两个比特位一共有四种结果:00,01,10,11.那么我们可以用00表示不存在,01表示只出现了一次,大于01就说明出现了多次。
关于代码:
如果我们还是用一个位图来做此题的话,在找映射位的时候就比较困难,用哈希的话来说就是容易出现哈希冲突,因此我们可以用两个位图来做。有了之前的核心操作,实现就简单了不少
大致思想图:
模拟实现如下:
template<size_t N>
class twobitset
{
public:
void set(const size_t& x)
{
//还没出现过,所以00->01
if (!_b1.test(x) && !_b2.test(x))
{
_b1.set(x);
}
else if (_b1.set(x) && !_b2.set(x))//出现过一次了,01->10
{
_b1.reSet(x);
_b2.set(x);
}
//出现超过了01了就不用在统计了
}
bool once(const size_t& x)
{
return _b1.test(x) && !_b2.test(x);
}
private:
bitset<N> _b1;
bitset<N> _b2;
};
2.给两个文件,分别有100亿整数,我们只有1G内存,如何找到两个文件的交集?
关于这道题我们同样是使用两个位图,只需要把两个文件分别映射到两个位图中,然后把这两个位图对应的位置与一下,这样剩下的1就是两个文件的交集。
3. 100亿个整数,求出现不超过两次的数?
大致思路与第一题一样,稍对代码进行改造就可以求出。
布隆过滤器
布隆过滤器的提出
我们知道整数可以通过比特位来进行一一映射而不冲突。然后如果是字符串这种,那么就有可能发生冲突。
布隆过滤器的概念
代码模拟:
struct BKDRHash
{
size_t operator()(const string& s)
{
// BKDR
size_t value = 0;
for (auto ch : s)
{
value *= 31;
value += ch;
}
return value;
}
};
struct APHash
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (long i = 0; i < s.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N,
size_t X = 5,
class K = string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t len = X * N;
size_t index1 = HashFunc1()(key) % len;
size_t index2 = HashFunc2()(key) % len;
size_t index3 = HashFunc3()(key) % len;
/* cout << index1 << endl;
cout << index2 << endl;
cout << index3 << endl<<endl;*/
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
}
bool Test(const K& key)
{
size_t len = X * N;
size_t index1 = HashFunc1()(key) % len;
if (_bs.test(index1) == false)
return false;
size_t index2 = HashFunc2()(key) % len;
if (_bs.test(index2) == false)
return false;
size_t index3 = HashFunc3()(key) % len;
if (_bs.test(index3) == false)
return false;
return true; // 存在误判的
}
// 不支持删除,删除可能会影响其他值。
void Reset(const K& key);
private:
hzj::bitset<X* N> _bs;
};
布隆过滤器的查找
布隆过滤器的删除
布隆过滤器不能直接删除,因为删除是靠把比特位置成0来实现的,但是置成0的比特位又可能会影响到其他的数据。
布隆过滤器的优点
布隆过滤器的缺点
简单举一个应用场景(加深理解)
平常我们打游戏的创建新账号的时候需要先创建一个昵称,作为游戏服务器后台,我们怎么知道用户上来取的昵称是否存在呢?
如果我们用set,虽然效率还行,但是如果数据过于庞大,那么用户随便试一个名字可能都要等上几秒后才能知道该昵称是否存在,这个速度还是不太理想。仔细想想,如果我们对该昵称是否存在的判断不需要十分精确,那么此时布隆过滤器就可以上场了
这样使用布隆过滤器的话,对与存在的昵称就可以瞬间出结果,如果该昵称实际不存在,但是它显示存在也问题不是很大,反正又不会出现同名的用户就行。但是如果非要做出精确的判断的话,我们可以用两层数据结构,第一层用布隆过滤器,如果昵称不存在则第一层就能返回结果,第二层用set,第一层不能返回结果则再去第二层的set(数据库)中查找。
这样的话既能保证精确度的同时还能使效率有不错的提升,也更能理解“过滤器”这个词。
处理海量数据题目扩展
有些问题用位图和布隆过滤器依旧不能很好的解决。大多都是跟字符串有关的
一
这道题我们可以用哈希切分,比如将100亿个query(查询---可以理解位字符串)分成一千份的小文件,然后对每个文件进行编号,把每次的query通过哈希函数找到文件的编号,把它放进去,然后每次找交集的时候只需要先算出这个查询的编号,然后在这个编号的文件中查找就行了。
但是中间会有一个问题,那就是当冲突太多时,一个文件的大小可能会过大,甚至超过我们的内存。此时会有两个场景:
1.存在很多重复的元素。
2.不是重复元素而是很多冲突的元素
解决第一种我们可以先用set进行去重,如果只是有很多冲突的元素,超出了内存,那么set会抛出一个异常,此时我们可以换一个哈希函数,继续对这些元素进行哈希切分。如果是第二种,那么我们set会帮我们去重,也会减少很多重复元素的个数。
二
同样我们可以使用哈希切分的思想,通过哈希函数把相同的IP地址存入同一个文件,然后再来统计次数就可以了。