【数据结构与算法】位图 布隆过滤器 海量数据问题处理 哈希切分
文章目录
- Ⅰ. 位图
- 一、问题引入
- 二、位图概念
- 三、位图的模拟实现
- set 函数:将 x 位置处的比特位设为 1
- reset 函数:将 x 位置处的比特位设为 0
- test 函数:检查 x 位置处的比特位是否为 1
- 四、位图的应用
- 五、常见面试题
- ① 给定100亿个整数,设计算法找到只出现一次的整数?
- ② 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
- ③ 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
- Ⅱ. 布隆过滤器
- 二、布隆过滤器的提出
- 二、布隆过滤器的概念
- 三、布隆过滤器的结构
- 四、布隆过滤器的模拟实现
- ① 插入函数 Set
- ② 查找函数 Test
- ③ 布隆过滤器的删除
- 五、布隆过滤器的优点
- 六、布隆过滤器的缺点
- 七、布隆过滤器的应用场景
- 八、布隆过滤器的面试题
- Ⅲ. 布谷鸟过滤器
- 一、布谷鸟过滤器由来
- 二、布谷鸟哈希
- 三、布谷鸟哈希的问题
- 四、布谷鸟哈希的优化
- 五、布谷鸟过滤器
- 六、特殊的hash函数
- Ⅳ. 哈希切分
- 哈希切分的一道题
- Ⅳ. 拓展阅读

Ⅰ. 位图
一、问题引入
这里有道题:给 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 <