T公司第三轮面试,问题如下:
A文件有40亿个QQ号码,B文件有40万个QQ号码,所有QQ号码都是无符号整数,求A和B的交集,可用内存是600M.
暴力算法(鸡肋)
先来看暴力算法,简单直接而粗暴:
#include <iostream>
#define M 5
#define N 3
using namespace std;
int main() {
int a[M] = {3, 4, 5, 9, 2};
int b[N] = {4, 1, 2};
for (int i = 0; i < M; i++)
{
for (int j = 0; j < N; j++)
{
if (a[i] == b[j])
{
cout << a[i] << endl;
}
}
}
return 0;
}
结果:

这种算法挺鸡肋的,因为内层循环是线性查找,整体时间复杂度是O(MN). 为什么不用哈希查找呢?
哈希表(局限)
用哈希表进行改进,hash[x]=x, 即在下标x处存x的值:
#include <iostream>
#define M 5
#define N 3
using namespace std;
int main() {
int a[M] = {3, 4, 5, 9, 2};
int b[N] = {4, 1, 2};
int H[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
for (int i = 0; i < M; i++)
{
H[a[i]] = a[i]; // hash table
}
for (int j = 0; j < N; j++)
{
if (H[b[j]] == b[j])
{
cout << b[j] << endl;
}
}
return 0;
}
结果:

可以看到,没有双重循环,时间复杂度大大降低。
flag表(依旧局限)
受哈希表的启发,可以对元素x进行0或1标记,即为flag表:
#include <iostream>
#define M 5
#define N 3
using namespace std;
int main() {
int a[M] = {3, 4, 5, 9, 2};
int b[N] = {4, 1, 2};
int H[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
for (int i = 0; i < M; i++)
{
H[a[i]] = 1; // set a flag
}
for (int j = 0; j < N; j++)
{
if (H[b[j]] == 1)
{
cout << b[j] << endl;
}
}
return 0;
}
结果:

现在比较接近正确答案了,但转念一想:A有40亿个QQ号码, B有40万个QQ号码,如果一次读到内存,肯定超过600M,所以,上述方法都失效。
反思一下flag表,可以看到,为了存a数组中的5个整数,H数组需要10个无符号整数来记录,太浪费了。其实,用10个bit就可以了。如此一来,存储量就压缩成了原来的1/32, 所用内存在600M以内,符合要求,如下:

我们可以看到,使用bitmap后, 1个unsigned int,就能表示32个整数的存在与否。求A和B交集的程序如下:
#include <iostream>
#include <fstream>
using namespace std;
#define BIT_INT 32 // 1个unsigned int可以标志32个QQ的存在与否
#define SHIFT 5
#define MASK 0x1f
#define N 4294967296 // 2的32次方, 能覆盖到所有的QQ号码
unsigned int *a = NULL;
// 必须用堆
void createArr()
{
a = new unsigned int[1 + N / BIT_INT];
}
// 堆释放
void deleteArr()
{
delete []a;
a = NULL;
}
// 将所有位都初始化为0状态
void setAllZero()
{
memset(a, 0, (1 + N / BIT_INT) * sizeof(unsigned int));
}
// 设置第i位为1
void setOne(unsigned int i)
{
a[i >> SHIFT] |= (1 << (i & MASK));
}
// 设置第i位为0
void setZero(unsigned int i)
{
a[i >> SHIFT] &= ~(1 << (i & MASK));
}
// 获取第i位的值
int getState(unsigned int i)
{
return (a[i >> SHIFT] & (1 << (i & MASK))) && 1;
}
// 用bitmap记录是否存在
void setStateFromFile()
{
ifstream cin("a.txt");
unsigned int n;
while(cin >> n)
{
setOne(n);
}
}
// 交集
void printCommonNumber()
{
ifstream cin("b.txt");
unsigned int n;
while(cin >> n)
{
if(1 == getState(n))
{
cout << n << " ";
}
}
cout << endl;
}
int main()
{
createArr();
setAllZero();
// a.txt: 4 5 7 2 9 2 4 8 0 11 (a.txt中可以有40亿个无符号整数)
// b.txt: 6 11 0 2 3 (b.txt中可以有40万个无符号整数)
setStateFromFile();
printCommonNumber(); // 交集是:11 0 2
deleteArr();
return 0;
}
可以看到,结果符合预期。
从最开始的暴力算法,到哈希表,再到flag表,再到bitmap, 逐渐优化,终于解决了问题。在实际开发中,bitmap的应用也是非常广泛的。

本文探讨了在有限内存条件下,如何高效求解两个大型文件中QQ号码的交集问题。从暴力算法逐步优化至使用bitmap,最终实现了在600M内存限制下,处理40亿和40万QQ号码的交集计算。





