今天给大家带来来分享一下位图的使用
位图的海量数据处理题目
通过几道海量数据处理的题目来引出今天的内容。
1.给40亿个无符号的整型,没有排过序。给一个无符号的整型,如何快速判断一个数是否在这40亿个数中。
这一道题肯定会有同学想用二分,先将数进行排序,然后二分。确实,二分是一个非常快速的算法。还有同学会想到用set插入+find查找。也不失为一个方法。但我们首先应该考虑内存的问题,排序需要一个40亿整型数组,set需要不大于40亿个结点。40亿个无符号整型大约有16个G,1个 G大约有10亿字节,40亿unsigned_int类型的数大约160字节也就是16G。正常的电脑内存也可以有16G,但我们还有许多别的程序在运行,如果运行这一个程序就16G,这样我们的电脑会裂开。
这里有一种新的方法,就是位图。这里就有哈希的思想在里面。位图是将每一个数映射在每一个对应的位上,一个字节是8个比特位,一个整型是32个比特位,也就是说,一个整型的32位能找到对应的32个元素,因此,我们能节省很多的空间。所以,位图是本题最好的解决办法。
40亿字节就需要40亿比特位,约等于0.5G。无符号int 的最大数达到42亿多,也就是2^32次方。这里咱们可以多开一点空间,开大于0.5G多一点。
如图所示,假如我要把50这个元素映射到第五十个比特位,也是第二个整型中的其中一位。
如何找到某个元素x存储在哪个位置呢?这里我定义两个变量,i 和 j。计算x存储在数组的第i个整型数据中,计算x存储在第j位。 可以以取模的方式来计算。 i=x/32 j=x%32(这里的32是一个整型的32位,我们要计算存储在哪个整型的第几个比特位)。
//置为1
void set(size_t x)
{
assert(x <= N);
int i = x / 32;
int j = x % 32;
_bit[i] = _bit[i] | (1 << j);
}
这里把元素对应的位映射为1。算法:将该映射位与1向左移j位进行或操作。有些同学会疑惑有些内存是以小端存储,有的以大端存储,如果或了,小端和大端得到的结果不就不一样了吗,这该怎么办呢?
可能有些同学不知道大小端。我来解释一下,一个数据的低位字节存低地址中,高位字节存高地址中这就是小端,大端的情况就反过来了。这里我们存储1,查看存储情况。
这里采用的就是小端存储,看上图,地址的最后2位,74,75,76,77存的是01,00,00,00.
1的32位二进制为00000000 00000000 00000000 00000001这里采用16进制存储,也就是1个16进制数等于4个2进制数,所以按正常的手写是这样的00 00 00 01,从左往右就是从低位字节到高位字节。将手写的和图中的联系起来就知道在内存中怎么存储的了。所以大端的情况就是01 00 00 00,如果这两种不同的情况去或,得到的结果肯定不同。但不要担心,编译器会自己为我们解决这些问题,大家不需要在意编译器是怎么帮我们去实现的,只要知道即可。
//置为0
void reset(size_t x)
{
int i = x / 32;
int j = x % 32;
_bit[i] = _bit[i] & ~(1 << j);
}
如果元素不存在了,可以把元素对应的位映射为0。算法:让该映射位与1左移j位然后取反后的结果进行与操作。
bool test(size_t x)
{
assert(x <= N);
int i = x / 32;
int j = x % 32;
return _bit[i] & (1 << j);
}
这里是测试对应位的元素还在不在,在的话就返回true(1),不在就返回false(0)。
这里是位图的完整实现代码:
//位图
template<size_t N>
class Bitset
{
public:
Bitset()
{
_bit.resize(N / 32 + 1, 0);
}
//置为1
void set(size_t x)
{
assert(x <= N);
int i = x / 32;
int j = x % 32;
_bit[i] = _bit[i] | (1 << j);
//return _bot[i] == 0 ? false : true;
}
//置为0
void reset(size_t x)
{
int i = x / 32;
int j = x % 32;
_bit[i] = _bit[i] & ~(1 << j);
}
bool test(size_t x)
{
assert(x <= N);
int i = x / 32;
int j = x % 32;
return _bit[i] & (1 << j);
}
private:
vector<int> _bit;
};
void test()
{
Bitset<100> bs;
bs.set(11);
bs.set(19);
bs.set(30);
bs.set(45);
for (size_t i = 0; i < 100; i++)
{
if (bs.test(i))
cout << i << "->" << "在" << endl;
else
cout << i << "->" << "不在" << endl;
}
}
经过上面的分析,我将继续加大难度,给出一道新题目让同学们继续思考。
2.给定100亿个整数,设计算法找到只出现一次的整数
这里不知道会不会有同学疑惑,咱们无符号整型最大也才42亿多,100亿个整数是怎么来的,这里明确的说,会有大量的重复的数。我们要做的就是用位图将重复的数映射到相应的位上,同时统计次数。肯定会有同学想到,咱们的位图的每一位不是0就是1,代表出现0次和1次,如果多次重复的数,就不能还用0和1了吧。2次及以上就要有2个二进制数存储了是吧。于是,通过这个想法,我们想可以再多用一个位图不就行了吗。这样最多能统计出现3次的数,但出现超过3次的怎么办?
这个我们看题目,只找到出现一次的数,超过2次的我们就不要管了,如果超过3次,就当把两个二进制映射成11就行了,我们不需要管他出现几次,我们只看出现一次的数。
这里我们把上面那个位图的二进制看成2个二进制的低位,下面的看成高位。这里存50,如果出现1次上(1),下(0),2次上(0),下(1),依此类推。这样我们就能解决这道题目了。这里也只开了1个多G一点的空间。
这里我展示实现的代码:
template<size_t N>
class two_bit_set
{
public:
void set(size_t x)
{
//01
if (bs1.test(x) == false && bs2.test(x) == false)
{
bs2.set(x);
} //10
else if (bs1.test(x) == false && bs2.test(x) == true)
{
bs1.set(x);
bs2.reset(x);
}
}
bool _test(size_t x)
{
if (bs1.test(x) == false && bs2.test(x) == true)
return true;
return false;
}
private:
Bitset<N> bs1;
Bitset<N> bs2;
};
};
这里的代码是基于上面的代码而写的。希望大家都能耐心看一下哈。
3.给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集。
根据前几个问题,这个问题就显而易见了。开辟两个位图,将两个文件给定的元素分别映射到相应位图的相应位上,然后将两个位图的同一位置进行&(与),如果与后为1就是交集,为0不是交集。
4.给定100亿个整数,设计算法找到只出现一次的整数,给定0.5G的内存。
这个题相较前几题又把内存给不断缩小,因此灵活应变,100亿整数有大量重复的数,我们int的最大值为2^32因此我们可以分段映射。当把0~2^31-1的数映射完,并统计他的个数为1的数然后删除,接着统计2^31次方到2^32-1的数,接着重复操作上述步骤。