
Ⅰ. 位图
一、问题引入
这里有道题:给 40
亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这 40
亿个数中。这里一共有三种方法:
- 遍历,时间复杂度
O(N)
- 先排序
O(NlogN)
,然后利用二分查找:O(logN)
很明显发现前两种方法虽然可行,但是对于 40
亿个整数来说,内存开销是十分的大的,因为 40
亿个整数相当于是 16GB
左右,这个时候也更不可能去使用红黑树这些数据结构来解决,毕竟红黑树这些数据结构本身就带有一些负载的消耗(旋转),所以我们这里给出了第三种方法:位图!
二、位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
比如上面的面试题,要我们开 40
亿个整数,并且判断一个数是否在这其中,那么我们只需要开辟 500MB
的空间大小,因为我们这里每个比特位代表一个数字存不存在的状态,比如 0
代表不存在,1
代表存在,那么一个比特位就足够啦,于是只需要开原本需要 16GB
的 1/32
即可,也就是 500MB
!这就很巧妙的解决了空间问题!
而至于时间问题,查找某个数字的时候,位图也是通过某个数字的对应映射关系查找的,也就是通过哈希!所以时间复杂度为 O(1)
。
三、位图的模拟实现
首先我们先把其框架搭出来:
// BitSet.h
#include <iostream>
#include <vector>
namespace liren
{
class BitSet
{
private:
std::vector<char> _bits;
};
}
❓ 这里很奇怪哦,用到的是 vector
里面存的 char
,为什么呢?
仔细想一想,c++
中最小的存储单位就是 char
,由于我们要的是按比特位来识别数字,所以我们这里就用 char
类型来表示 8
个比特位,也就是 8
个数字,这样子也方便我们后面的位运算!(用 int
类型来存储也可以,但是一共有 32
个比特位,相比于 char
的话会更难控制一些,所以这里选用 char
)
❓ 那么我们接下来的拷贝构造函数中如何来初始化呢?
这里我们用模板参数 N
来代表要初始化的比特位个数,那么问题又来了,数组开多大的空间呢❓
🌛 由于我们数组中存放的类型是 char
类型,所以我们要得到 N
个比特位的话,则要 resize(N / 8)
,这里还有一个细节,就是还要多加一,因为假设 N
是 10
的话,那么 N / 8
其实还是 1
,那么我们开出来的就只有一个 char
也就是 8
个比特位,但是 N
是 10
啊,所以为了保险起见,我们每次多开一个 char
的空间!也就是 resize(N / 8 + 1)
!
// BitSet.h
#include <iostream>
#include <vector>
namespace liren
{
// N代表要存放的比特位数量
template <size_t N>
class BitSet
{
public:
// 构造函数
BitSet()
{
// 每次多开一个字节,并初始化为0
_bits.resize(N / 8 + 1, 0);
}
private:
std::vector<char> _bits;
};
}
下面我们就来实现位图的几个主要的接口函数:(注意这里vs是小端,大端的实现略有不同)
set 函数:将 x 位置处的比特位设为 1
方法:先获取到 x
的位置,然后 按位或 上 x
处比特位为 1
的数即可。
// 将x位置处的比特位设为1
void set(size_t x)
{
// 先获取x的位置
size_t i = x / 8;
size_t j = x % 8;
// 接着让x处的比特位(按位或)上(只有x处为1的数)
// 注意这里vs是小端存储,注意方向
_bits[i] |= (1 << j);
}
reset 函数:将 x 位置处的比特位设为 0
方法:先获取到 x
的位置,然后让 x
处的比特位 按位与(&)上 x
处比特位为 0
其他位为 1
的数即可。
// 将x位置处的比特位设为0
void reset(size_t x)
{
// 先获取x的位置
size_t i = x / 8;
size_t j = x % 8;
// 接着让x处的比特位(按位与)上(只有x处为0的数)
// 注意这里vs是小端存储,注意方向
_bits[i] &= (~(1 << j));
}
test 函数:检查 x 位置处的比特位是否为 1
// 检查x位置处的比特位是否为1
bool test(size_t x)
{
// 先获取x的位置
size_t i = x / 8;
size_t j = x % 8;
// 接着让x处的比特位(按位与)上(只有x处为1的数)
return _bits[i] & (1 << j);
}
测试代码:
// test.cpp
#include "BitSet.h"
int main()
{
liren::BitSet<20> t;
t.set(10);
std::cout << t.test(10) << std::endl;
t.reset(10);
std::cout << t.test(10);
return 0;
}
接下来我们尝试着来解决上面遗留的面试题:
int main()
{
// 40亿个整数,我们只需要开500MB的比特位即可
// 但是为了保证int的范围都被包括,我们要开42亿整数空间的大小
// 也就是unsigned的最大值
// 但其实这里我们直接利用十六进制表示即可开unsigned的最大值
//liren::BitSet<-1> b; 这种写法会报错
liren::BitSet<0xffffffff> b;
b.set(1314);
std::cout << b.test(1314) << std::endl;
b.reset(1314);
std::cout <