1.位图
1.1 位图概念及实现
面试题 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯】
在不在的问题
1.排序+二分查找,排序是O(NlogN),二分查找是O(logN)
2.红黑树
3.set/unordered_set
2^10 k
2^20 M
2^30 G
1GB=2^30B大概是10亿B,40亿*4B=160亿B,也就是16G,而如果是红黑树或者set,红黑树要存储left,right,parent和color,也就是要存4B的int,每个结点是20B,那么就是16G *5=80G,没有这么大的内存,set也要额外开辟空间,如果是拉链法要存指针
但其实没必要排序和存起来
1GB=2 ^ 33b,我们可以使用位图来解决问题,40亿b=5亿B=0.5G,但是我们要开辟的空间是2^32因为无符号数是0~2 ^ 32-1,我们用位图的每一位来标识该位对应的数值是否存在,类似的计数排序,我们开辟空间不是根据数的数量,而是数的范围,必须要覆盖所有的数, 2 ^32/8=2 ^29B,也就是0.5GB

但是我们不需要考虑那么多,用vector来存储int,每个比特位是1,一个int就是32,
int i=x/32;
int j=x%32;
cpp
#pragma once
namespace diy {
template<size_t N>
class bitset {
public:
void set(size_t x) {
int i = x / 32;
int j = x % 32;
_t[i] |= (1 << j);//本来是0还是0,本来是1还是1,第j位置为1
}
void reset(size_t x) {
int i = x / 32;
int j = x % 32;
_t[i] &= ~(1 << j);//~是按位取反,除了第j位,本来是0还是0,本来是1,还是1
}
bool test(size_t x) {
int i = x / 32;
int j = x % 32;
return _t[i] & (1 << j);//bool 0为假,其余均为真
}
bitset() {
_t.resize(N / 32 + 1);
}
private:
vector<int> _t;
};
}
测试
cpp
diy::bitset<8000> bt;
diy::bitset<0xffffffff> bt1;
diy::bitset<-1> bt2;
bt.set(1);
bt.set(10);
bt.set(100);
cout << bt.test(1) << endl;
cout << bt.test(10) << endl;
cout << bt.test(100) << endl;
bt.reset(10);
bt.set(999);
cout << bt.test(1) << endl;
cout << bt.test(10) << endl;
cout << bt.test(100) << endl;
cout << bt.test(999) << endl;
C++标准库的bitset
https://cplusplus.com/reference/bitset/bitset/
常用的是test,set和reset三个接口

1.2 位图应用
1.给定 100 亿个整数,设计算法找到只出现一次的整数?
cpp
template<size_t N>
class twobitset {
public:
void set(size_t x) {
if (!_bt1.test(x) && !_bt2.test(x))//00->01
_bt2.set(x);
else if (!_bt1.test(x) && _bt2.test(x)) {//01->10
_bt1.set(x);
_bt2.reset(x);
}
//10不做处理 00出现0次,10出现1次,10出现两次及以上
}
bool is_once(size_t x) {
return !_bt1.test(x) && _bt2.test(x);//找01
}
private:
bitset<N> _bt1;
bitset<N> _bt2;
};
测试
cpp
int main() {
twobitset<10> tbt;
int arr[] = { 1,2,4,7,9,0,2,4,3,5 };
for (const int& e : arr)
tbt.set(e);
for (const int& e : arr) {//只出现一次,只会被遍历一次
if (tbt.is_once(e))
cout << e << endl;
}
return 0;
}
2.给两个文件,分别有 100 亿个整数,我们只有 1G 内存,如何找到两个文件交集?
如果用一个位图来标记文件A中存在的数字,再用位图去检测文件B的数据是否在位图中,这样可能会有重复值,比如文件B存在3个5,就会检测出三次,还要进行去重,因为集合是没有重复元素的,交集是没有重复元素的
所以我们用两个位图
cpp
diy::bitset<20> bt1;
diy::bitset<20> bt2;
int arr1[] = { 1,2,4,7,9,0,2,4,3,5 };
int arr2[] = { 4,13,2,5,7,2,6,9,1,0,15 };
for (const int& e : arr1)
bt1.set(e);
for (const int& e : arr2)
bt2.set(e);
for (size_t i = 0; i < 20;i++) {//如果遍历arr1或arr2来测试会出现重复结果
if (bt1.test(i) && bt2.test(i))
cout << i << endl;
}
如果出现负数,可以考虑映射,min映射为0,测试输出或者确认结果时映射回来
1的变形:1 个文件有 100 亿个 int,1G 内存,设计算法找到出现次数不超过 2 次的所有整数
不超过两次也就是1,2,
cpp
template<size_t N>
class lesstwobitset {
public:
void set(size_t x) {
if (!_bt1.test(x) && !_bt2.test(x))//00->01
_bt2.set(x);
else if (!_bt1.test(x) && _bt2.test(x)) {//01->10
_bt1.set(x);
_bt2.reset(x);
}
else if (_bt1.test(x) && !_bt2.test(x))//10->11
_bt2.set(x);
//11不做处理 00出现0次,10出现1次,10出现两次,11出现3次及以上
}
bool is_less_two(size_t x) {
return (!_bt1.test(x) && _bt2.test(x)) || (_bt1.test(x) && !_bt2.test(x));//找01,10
}
private:
bitset<N> _bt1;
bitset<N> _bt2;
};
cpp
diy::lesstwobitset<10> tbt;
int arr[] = { 1,1,1,3,3,3,1,2,4,7,6,5,5 };
for (const int& e : arr)
tbt.set(e);
for (size_t i = 0; i < 20; i++) {//直接遍历arr进行检测可能出现重复值
if (tbt.is_less_two(i))
cout << i << endl;
}
2.布隆过滤器
也是位图的应用,如果我们要确定的不是int而是字符串呢?比如设置昵称时可能会看到提示该昵称已存在,那么字符串->整型->映射到位图(比如取模等),但是会存在冲突,在哈希那里就遇到过,但不存在一定是准确的,存在是不一定准确的,比如"布隆"和"公主殿下"可能映射到一个位置,因为"布隆"在位图中,所以在检测不存在的"公主殿下"时结果是存在,但是只要该位置为0,"公主殿下"和"布隆"肯定都不存在
此时不能采用拉链法来解决冲突,因为位图都不存数据,更不用说存个链表了,我们可以不把鸡蛋放在一个篮子里,比如我们可以一个字符串不止映射到一个位置,可以映射到3个位置,在这种情况下,只有这三个位置都是1的情况下我们才认为存在,只要有一个为0就是不存在,set某个值的话,对应的三个位置都置为1,在这种情况下,不在是准确的,在还是不准确的,但和映射一个位置相比,之前一个位置被占比三个位置都被占的概率高啊,所以存在的误判率可以降低,可以应用于要求没那么精确的场景,比如昵称是否被占,就算误判存在其实也没太大影响,把效率降到5%~10%,但是如果误判率高,客户还是不满意;在这种情况下我们可以分情况处理,如果是不在,就是准确的,返回结果;如果在,那可以去服务器上的数据库找一下,这样可以降低服务器的负载,比每一次都去服务器上找负载少多了,一般服务器负载高,就会比较慢,负载均衡的情况下速度还可以。也是为什么称为布隆过滤器,过滤掉不在的场景。
牛刀小试
- 下面关于位图说法错误的是()
A.位图就是用比特比特位表示一个数据的状态信息
B.通过位图可以求两个集合的交集
C.位图实际是哈希变形思想的一种应用
D.位图可以很方便的进行字符串的映射以及查找
D
- 现有容量为10GB的磁盘分区,磁盘空间以簇(cluster)为单位进行分配,簇的大小为4KB,若采用位图法管理该分区的空闲空间,即用一位(bit)标识一个簇是否被分配,则存放该位图所需簇的个数为 ()
A.80
B.320
C.80K
D.320K
A
10GB/4KB=2.5*2^20b, 2.5 *2 ^20b/8=2.5 *2^17B=5 *2^4 *4KB =80簇