试想一下:我们该如何在40亿个整数中判断一个数是否存在呢?
如果一个一个去遍历那就太慢了,时间复杂度就是O(n^2),直接pass掉;
如果先排序再二分,那排序的代价也是巨大的
如果能想到哈希表,那样的话要的空间太大,要4 * 40亿个字节的空间(2 ^ 10 = 1024,2 ^ 30 = 1024 * 1024 * 1024 ≈ 10亿,就是要2 ^ 34 个字节,1 GB = 1024 MB = 1024 * 1024 KB = 1024*1024*1024Byte(字节),也就是要差不多16个GB),堆上没有那么大的空间,但是已经很接近答案了;
如果我们只让一个比特位就代表一个整数呢?
这就是我们的位图了
位图
位图是什么?
我们都知道一个字节 = 8个比特位,一个整型就是32个比特位
先假设我们要在0 ~ 30中判断一个数是否存在,我们可以让一个bit位代表一个数是否存在(1代表存在,0代表不存在),这样我们每遍历一个数(假设是n)我们就将那个数所代表的bit位设置成1(bit_set |= (1 << n)),最后我们想要知道一个数存不存在(假设是k),我们只需要判断第k位是0还是1即可,这样我们就用O(1)的空间复杂度和O(n)的时间复杂度就解决了这个问题
cpp
#inlcude<iostream>
using namespace std;
int main()
{
srand(time(nullptr));// 设置种子
vector<int> v(30);
for (int i = 0; i < 30; i++)// 插入随机数
v.push_back(rand() % 31);
int bit_set = 0;
for (auto& i : v)// 设置bit_set的bit位,并打印数组
{
cout << i << ' ';
bit_set |= (1 << i);
}
cout << endl;
for (int i = 0; i <= 30; i++)// 判断 0 ~ 30 里哪些数存在
{
if (bit_set & (1 << i))
cout << i << "存在" << endl;
else
cout << i << "不存在" << endl;
}
return 0;
}
那么这种利用每个bit位代表一个数字的思想,就叫作位图
位图本质上就是一个数组,但是这个数组只存1和0
既然这样我们用位图的思想解决上面的问题不就能大大减少时间和空间了嘛!
位图的实现模拟
位图类的实现主要看:构造函数,设置1,设置0,判断是否存在
cpp
#include<iostream>
#include<vector>
using namespace std;
// 设置位图能判断0 ~ N个数
template<size_t N>
class bit_set
{
// 用vector储存位图数组
vector<int> _bs;
public:
// 构造函数
bit_set()// 设置vector大小
{
int size = (N / 32) + 1;
_bs.resize(size);
}
//设置1
void set(int n)
{
int i = n / 32;// 判断n在第几个位图
int j = n % 32;// n在该位图的第几位
_bs[i] |= 1 << j;
}
void reset(int n)
{
int i = n / 32;
int j = n % 32;
_bs[i] &= (~(1 << j));// 注意运算符优先级
}
// 判断
bool test(int n)
{
int i = n / 32;
int j = n % 32;
return _bs[i] & (1 << j);
}
};
void test_bitset1()
{
int a1[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6 };
int a2[] = { 5,3,5,99,6,99,33,66 };
bit_set<100> bs1;
bit_set<100> bs2;
for (auto e : a1)
{
bs1.set(e);
}
for (auto e : a2)
{
bs2.set(e);
}
for (size_t i = 0; i < 100; i++)
{
if (bs1.test(i) && bs2.test(i))
{
cout << i << endl;
}
}
}
int main()
{
test_bitset1();
return 0;
}
那么对于上面那个问题我们只需要
bit_set<-1> bs;
即可解决:由于是无符号整数,-1就被认定为了整型最大值(2 ^ 32 ≈ 40亿)
当然位图也是一个数据结构,在STL库中有对应的容器bitset
位图应用实训
接下来我们看几道位图相关的考察题目:
1.如何在100亿个整数中找到只出现一次的整数?
我们还是用位图,但这里我们不是按整数数量开空间,我们是按照范围来开空间!
由于整数大小不超过2^32-1,根据鸽巢原理,这100亿个整数一定会有重复的数,所以不用担心空间不够的问题,所以还是开2^32的空间大小
然后我们要确定只出现一次的整数:我们可以再多创建一个位图,分别命名为bs1,bs2,然后同时来计数。
即:
|---------|----------|----------|
| 该数的出现次数 | bs1的取值情况 | bs2的取值情况 |
| 0次 | 0 | 0 |
| 一次 | 0 | 1 |
| 两次 | 1 | 0 |
| 三次及以上 | 1 | 1 |
然后我们只要输出bs1为0,bs2为1的情况即可
2.给你两个文件,分别有100亿个整数,我们只有1GB内存,试找出两文件的交集
和上题一样,创造2个bit_set(bs1和bs2),但是不是统计数量,bs1记录文件1中数k的出现情况,bs2记录文件2中数k的出现情况
|----------------|----------|----------|
| 数k在文件1/2中的出现情况 | bs1的取值情况 | bs2的取值情况 |
| 文件1/2中均未出现 | 0 | 0 |
| 仅在文件1中出现 | 1 | 0 |
| 仅在文件2中出现 | 0 | 1 |
| 文件1/2中均出现 | 1 | 1 |
3.一个文件中有100亿个整数,1GB内存设计算法找出出现次数不超过2次的整数
这道题像是上面两道题的结合版,我们可以按照上面的思路来做;
我们也能复用STL库中的bitset设计一个类,来完成整个流程,我们只需调用函数即可:
cpp
#include<iostream>
#include<bitset>
using namespace std;
template<size_t N>
class twobitset
{
bitset<N> bs1;
bitset<N> bs2;
public:
void Set(int n)
{
int bool1 = bs1.test(n);
int bool2 = bs2.test(n);
if (!bool1 && !bool2) // 00 -> 01
bs2.set(n);
else if (!bool1 && bool2) // 01 -> 10
{
bs1.set(n);
bs2.reset(n);
}
else if (bool1 && !bool2) // 10 -> 11
bs2.set(n);
}
void Test(int n)
{
int bool1 = bs1.test(n);
int bool2 = bs2.test(n);
if (!bool1 && bool2)// 01
{
cout << n << "出现了1次" << endl;
}
else if (bool1 && !bool2)// 10
{
cout << n << "出现了2次" << endl;
}
}
};
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == nullptr)
{
cout << "打开文件错误!" << endl;
return 1;
}
srand(time(nullptr));
for (int i = 0; i < 100; i++)
fprintf(pf, "%d\n", rand() % 50 + 1);
fclose(pf);
pf = fopen("test.txt", "r");
if (pf == nullptr)
{
cout << "打开文件错误!" << endl;
return 1;
}
int n = 0;
twobitset<100> tbs;
while (fscanf(pf, "%d", &n) == 1)
tbs.Set(n);
for (int i = 1; i <= 50; i++)
{
tbs.Test(i);
}
fclose(pf);
return 0;
}

上面具体的文件操作不做详细说明,感兴趣的可以看看这篇博客
以上就是本期博客的所有内容,感谢阅读