Question
从40亿个没有排序且不重复的无符号整数中快速判断一个数的存在性。
【腾讯笔试题】
分析思路
由40亿个不重复的无符号整数,我们可以得到两点信息:
- 最大的整数是40亿
- 无符号整数unsigned long表示范围:0—4294967295(42亿)
接下来,我们需要利用一下数学知识:
4294967295 = 2^32字节,而2^30字节 = 1G
所以42亿约为4G
这意味着为了表示这些数,至少需要4G大小内存。
因此正常情况下,内存是不够存储这些数的。
位图(BitSet)能够解决这个问题。
位图(BitSet)
由上所述,由于内存不够导致数据无法存储,也就不能判断某数的存在性。
让我们把视线放在关键的问题上:数据的存在性
试想一下,数据的存在性本身是一个原子问题,所以我们可以用二进制来表示,即1表示存在。0表示不存在。
想通了这一点,我们就应该想到可以用一个比特位来表示数据,而非数据本身。而这种思想可以完美的解决数据存储的问题。
再拓展一下,我们就可以总结出问题的解决办法:
创建一个数组,数组中每个数据的每一个二进制位用来表示一个数据,0表示数据不存在,1表示数据存在。
这就是位图(BitSet)
根据位图的思想,我们就可以算出在此方法下数据所占内存大小:
2^32/2^5=2^27整型
2^27*4=2^29=512M,所以,有了位图,我们只需要开辟500M的内存,就可以存储每一个数据,与之前的4G比较,位图确实大大降低了内存消耗。
位图本身也是C++模板库STL中一个重要的容器
有了位图,我们就可以解决刚开始的那个问题。
Solution
接下来就是根据思想编写代码了。
为了方便对数据操作,使用STL另一个重要容器vector来生成数组。
首先,我们应该实现Set接口来存储数据,用位运算计算数据在数组中的下标,然后计算该数在此下标数据中具体的比特位位置,最后将对应比特位置为1。
其次,还应该实现ReSet接口方便实现数据的删除和位置重置,同样用位运算来完成。
最后,实现TestExist接口测试数据的存在性。
基于上述方法,编写C++代码如下:
#pragma once
#include <vector>
class BitSet
{
public:
BitSet(size_t range)
{
_a.resize((range >> 5), 0);//构造位图并将每位初始化为0
}
void SetNum(size_t num)//存放数据
{
size_t index = num >> 5;//计算该数在整形数组中的下标
size_t pos = num % 32;//计算该数在32个bite位中的位置
_a[index] |= (1 << pos);//将对应的比特位置为1
}
void ResetNum(size_t num)//重置数据
{
size_t index = num >> 5;
size_t pos = num % 32;
_a[index] &= ~(1 << pos);//将对应的比特位置为0
}
bool TestExistNum(size_t num)//判断某数的存在性
{
size_t index = num >> 5;
size_t pos = num % 32;
if (_a[index] & (1 << pos))
{
printf("该数存在\n");
return true;
}
else
{
printf("该数不存在\n");
return false;
}
}
protected:
vector <int> _a;
};
写完了代码,接下来就该测试了,根据代码编写如下测试用例:
void TestBitSet()
{
BitSet m1(-1);//-1的补码就是4294967295,此处用了强转
m1.SetNum(1);
m1.SetNum(5);
m1.SetNum(42900000000);
m1.ResetNum(5);
m1.TestExistNum(42900000000);
m1.TestExistNum(5);
}
调试过程:
1.初始化数组
2.分别插入1和5
3.重置数据
4.边界值插入及判断数据存在性
The End
位图的思想可以应用在很多方面,它可以减少内存消耗,是创新思维的体现。类似这种问题还有很多,比如在100亿个整数中找到出现一次的数,也可以通过位图来解决。
这个问题可以用两个比特位来表示一个数据,因为数据有3种状态,分别是不存在0,存在一次1,存在多次10。
当然,位图也不是万能的,当数据类型发生变化,如整型变成字符型。就要用位图的另一个变种方法,布隆过滤器(Bloom Filter)来解决。