引出
面试题:给出 40 亿个不重复的无符号整数,没有排过序。给定一个无符号整数,如何快速判断这个数是否在这 40 亿个无符号整数中。[ 腾讯面试题 ]
- 想法一:将 40 亿个数据存放到 set 里面,然后再查找指定的无符号整数。
时间复杂度:o( l o g 2 N log_2N log2N)。 - 将 40 亿个无符号整数排序之后二分查找。
我们先不考虑效率问题,实现上面的两种方案,都需要将 40 亿个整数加载到内存中。那么 40 亿个整数全部加载到内存中,需要多大空间呢?
40 0000 0000 × 4 = 160 0000 0000 字节 = ( 160 0000 0000 ÷ 1024 ÷ 1024 ÷ 1024 ) G B ≈ 15 G B 40\space0000\space0000 \space \times \space 4 = 160\space0000\space0000\space字节 = \space (160 \space 0000 \space 0000 \space \div \space 1024 \space \div \space 1024 \space \div \space 1024) \space GB\space \approx \space 15 \space GB 40 0000 0000 × 4=160 0000 0000 字节= (160 0000 0000 ÷ 1024 ÷ 1024 ÷ 1024) GB ≈ 15 GB
好的,我们需要的内存大概是 15 GB。
对于 set 来说,除了数据域,还要维护其他的指针信息。15 GB 的内存还不够。
普通电脑的内存也就 16 GB左右吧!但是操作系统这个软件也需要内存空间哇!所以说用 set 和 排序加二分查找都不是理想的办法!
我们再来看看问题:题目的要求是判断一个数在不在!那么一个数在或者不在,只有两种状态。因此我们可以用一个二进制位来表示一个数在不在。
因此我们可以开辟
2
32
=
42
9496
7296
2^{32} = 42 \space 9496 \space 7296
232=42 9496 7296 个比特位,用哈希表中的直接定址法(哈希表的直接定址法开辟的空间需要包含给出的 40 亿个整数的范围),将 40 亿个整数一一映射到不同的比特位。
那么我们需要的空间:
(
2
32
÷
8
÷
1024
÷
1024
)
M
B
=
512
M
B
(2^{32} \space \div 8 \space \div 1024 \space \div 1024) \space MB = 512 \space MB
(232 ÷8 ÷1024 ÷1024) MB=512 MB
这样一来,内存的问题是不是就轻松搞定啦!
上面使用的方法就是位图
位图基本结构定义
- 在 C++ 中我们并没有比特位的类型,因此在位图的底层,使用的数据结构就是
vecctor<char>
或者vector<int>
。只要数据类型支持位运算就没多大问题。 - 我们应该如何确定位图中底层
vector
的空间大小呢?首先这个vector
肯定不能是静态的。因此就需要我们传参数来决定vector
的大小。当然你可以在bitmap
(位图) 的构造函数做文章。但是库函数中并不是这么实现的。
你还记得模板的非类型模板参数吗?如果你需要复习,点击这里:➡️ C++模板详解
我们定义一个非类型模板的参数,这样就可以不在构造函数传参啦!
#pragma once
#include<cstddef>
#include<vector>
namespace Tchey
{
template <size_t N>
class bitmap
{
private:
vector<char> _a;
};
}
构造函数
我们接收了一个非类型的模板参数,根据这个非类型的模板参数,我们就能确定 vector
的大小啦!
- 如果你实现的
vector
存储的数据类型是 int。那么构造函数中,你的vector
需要的空间大小就是:
( 非类型模板参数 N ÷ 32 ) + 1 (非类型模板参数\space N \space \div \space 32 ) + 1 (非类型模板参数 N ÷ 32)+1
这个加一是为了应对需要的空间不是int
类型的整数倍的情况,这是必须的。 - 如果你实现的
vector
存储的数据类型是 char。那么构造函数中,你的vector
需要的空间大小就是:
( 非类型模板参数 N ÷ 8 ) + 1 (非类型模板参数\space N \space \div \space 8 ) + 1 (非类型模板参数 N ÷ 8)+1
这样看起来 vector
存储 char
更加节省内存呢?只不过这点节省好像真的没多大必要呢!
bitmap()
{
int size = N / 8 + 1;
_a.resize(size);
}
void set(size_t x)
这个函数将参数 x 映射的那个位置在位图中标记为 1。
那么,我们拿到参数 x 如何将这个位置标记为 1 呢?在下图中,红色矩形是一个位图,红色矩形中的黑色矩形是一个一个的 char
,假设我们要将下图中 x = 12
的位置标记为 1。
- 因为我们的
vector
存储的是char
,我们要找到 x 在vector
中的哪个下标,只需要将 x ÷ 8 x \div 8 x÷8 即可。不妨记作:i
。 - 除了获取 x 在
vector
中的哪一个下标,我们还需要获取 x 在这个下标的哪一位!我们只需要将 x % 8 x \space \% \space 8 x % 8 即可。不妨记作:j
。
那要如何修改呢?只要让将i
下标的char
与1 << j
相或即可。
在这里你可能会想到大小端的问题,只能说我们这样做完全没有问题呢!大小端无非是高地址存储
char
的高位还是低位的问题。我们定义了将x
置为 1 的逻辑。只要查找和置零按照同样的逻辑来就行。只不过在内存上对应的位置并不会像上面的示意图那样按照顺序来罢了!
void reset(size_t x)
这个函数将参数 x 映射的那个位置在位图中标记为 0。
关于给定参数 x 怎么找到 x 在 vector
中的哪一个下标(不妨记作 i),在这个下标的哪一位(不妨记作 j),在 set 函数中就已经讲解完毕了。这里就不再赘述啦!
那怎么将 x 对应的那个位置的比特位置为 0 呢?其实很简单:(~(1 << j)) & _a[i]
。
如果真的不理解就可以举一个例子呢!
如下图我们想将橙色位置的比特位标记为 0。
bool test(size_t x)
这个函数可以检测 x 对应的比特位是否是 1,如果 x 对应的比特位为 1 返回 true;反之返回 false。
实现:同样的我们需要计算出 x 对应的比特位在 vector
的哪个下标(不妨记作 i ),在这个下标的哪一位(不妨记作 j)。我们只需要返回:_a[i] & (1 << j)
即可。如果 (1 << j) 与 _a[i] 向与的结果为 0,则说明 _a[i] 的第 j 为就是 0。否则就是 1。因此我们返回相与的结果就好了。
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _a[i] & (1 << j);
}
测试代码
#include<iostream>
#include<vector>
#include<cstddef>
using namespace std;
#include "bitmap.h"
int main()
{
Tchey::bitmap<10> bm;
bm.set(1);
bm.set(3);
bm.set(5);
bm.set(7);
bm.set(9);
for(int i = 0; i < 10; i++)
{
cout << bm.test(i) << " ";
}
cout << endl;
bm.reset(1);
bm.reset(3);
bm.reset(5);
bm.reset(7);
bm.reset(9);
for(int i = 0; i < 10; i++)
{
cout << bm.test(i) << " ";
}
cout << endl;
return 0;
}
我们看到输出结果是没有任何问题的哈!那么我们的 bitmap 就模拟实现完成啦!