在日常开发和面试中,我们总会遇到各类 “海量数据” 处理问题:40 亿个整数快速查重、100 亿个 URL 去重、1G 内存找两个 500G 文件的交集…… 常规的暴力遍历、哈希表解法在这些场景下要么效率拉胯,要么直接撑爆内存。而哈希扩展技术 —— 位图(Bitset)和布隆过滤器(Bloom Filter),正是为解决这类 “内存不够、效率要高” 的问题而生。本文将从面试题切入,手把手拆解位图和布隆过滤器的原理、实现、应用,让你彻底吃透这两大海量数据处理神器!下面就让我们正式开始吧!
先看一道腾讯和百度都出过的高频面试题:
给 40 亿个不重复的无符号整数(未排序),给定一个无符号整数,如何快速判断它是否在这 40 亿个数中?
拿到这个问题,很多人第一反应是 “暴力遍历” 或 “排序 + 二分查找”,但我们先算一笔账:
既然 “存整数本身” 内存不够,那我们换个思路:判断一个数 “在或不在”,本质是两种状态,刚好可以用1 个二进制比特位表示(1 = 存在,0 = 不存在)。这种用比特位映射数据状态的结构,就是位图(Bitset)。

无符号整数的范围是 0~2³²-1(约 42 亿),我们只需要开辟 2³² 个比特位的空间,就能映射所有无符号整数的状态:
位图的核心操作是设置位(set)、清空位(reset)、检测位(test),以下是完整实现:
#include <iostream>
#include <vector>
using namespace std;
namespace bit
{
// N是需要的比特位总数
template<size_t N>
class bitset
{
public:
bitset()
{
// 初始化数组大小:N/32 + 1(向上取整),用右移5位代替除以32,效率更高
_bits.resize((N >> 5) + 1, 0);
}
// 标记x存在(将对应位设为1)
void set(size_t x)
{
if (x >= N) return; // 超出范围直接返回
size_t i = x / 32; // 找到对应的int块
size_t j = x % 32; // 找到对应的比特位
_bits[i] |= (1 << j); // 位或操作置1
}
// 标记x不存在(将对应位设为0)
void reset(size_t x)
{
if (x >= N) return;
size_t i = x / 32;
size_t j = x % 32;
_bits[i] &= ~(1 << j); // 位与取反操作置0
}
// 检测x是否存在(返回对应位的值)
bool test(size_t x)
{
if (x >= N) return false;
size_t i = x / 32;
size_t j = x % 32;
return _bits[i] & (1 << j); // 位与操作检测
}
private:
vector<int> _bits; // 存储比特位的数组,每个int占32位
};
}
// 测试位图功能
int main()
{
bit::bitset<100> bs;
// 设置几个数存在
bs.set(50);
bs.set(30);
bs.set(90);
// 遍历检测0~99的数
cout << "第一次检测结果:" << endl;
for (size_t i = 0; i < 100; ++i)
{
if (bs.test(i))
{
cout << i << " -> 存在" << endl;
}
else
{
cout << i << " -> 不存在" << endl;
}
}
// 修改状态:清空90,设置91
bs.reset(90);
bs.set(91);
cout << "\n修改后检测结果:" << endl;
for (size_t i = 0; i < 100; ++i)
{
if (bs.test(i))
{
cout << i << " -> 存在" << endl;
}
else
{
cout << i << " -> 不存在" << endl;
}
}
// 处理40亿个数时,只需初始化bit::bitset<UINT_MAX> bs;
return 0;
} C++ 标准库也提供了std::bitset,核心接口和我们实现的一致,还扩展了更多功能:
接口 | 功能 |
|---|---|
set() | 设置指定位为 1(默认全部) |
reset() | 清空指定位为 0(默认全部) |
test() | 检测指定位的值 |
count() | 统计为 1 的位的数量 |
any() | 判断是否有位为 1 |
none() | 判断是否所有位为 0 |
flip() | 翻转指定位(0 变 1,1 变 0) |
operator[] | 像数组一样访问指定位 |
需要注意:std::bitset的大小是编译期确定的(模板参数),如果需要动态大小,可使用vector<bool>(本质也是位图实现)。
其技术文档链接如下:https://legacy.cplusplus.com/reference/bitset/bitset/

位图不仅能解决 “查重” 问题,还能处理更复杂的海量数据场景:
思路:依旧开辟 2³² 个位的位图,遍历所有数,第一次遇到设为 1,第二次遇到设为 0(重复),最终值为 1 的位对应的数就是只出现一次的数。
思路:
// 位图找交集示例
void test_bitset_intersection()
{
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::bitset<100> bs1, bs2;
// 第一个数组存入bs1
for (auto e : a1) bs1.set(e);
// 第二个数组存入bs2
for (auto e : a2) bs2.set(e);
// 找交集
cout << "两个数组的交集:" << endl;
for (size_t i = 0; i < 100; ++i)
{
if (bs1.test(i) && bs2.test(i))
{
cout << i << " ";
}
}
cout << endl;
}思路:每个数用2 个比特位标记次数(00=0 次,01=1 次,10=2 次,11=2 次以上),最终筛选出 01 和 10 对应的数:
namespace bit
{
// 双位图:每个数用2位标记出现次数
template<size_t N>
class twobitset
{
public:
// 标记数x出现一次
void set(size_t x)
{
bool bit1 = _bs1.test(x); // 高位
bool bit2 = _bs2.test(x); // 低位
if (!bit1 && !bit2) // 00 -> 01(第一次出现)
{
_bs2.set(x);
}
else if (!bit1 && bit2) // 01 -> 10(第二次出现)
{
_bs2.reset(x);
_bs1.set(x);
}
else if (bit1 && !bit2) // 10 -> 11(第三次及以上)
{
_bs2.set(x);
}
// 11则不处理
}
// 获取数x的出现次数
int get_count(size_t x)
{
bool bit1 = _bs1.test(x);
bool bit2 = _bs2.test(x);
if (!bit1 && !bit2) return 0;
else if (!bit1 && bit2) return 1;
else if (bit1 && !bit2) return 2;
else return 3; // 2次以上
}
private:
bitset<N> _bs1; // 高位
bitset<N> _bs2; // 低位
};
}
// 测试双位图
void test_twobitset()
{
bit::twobitset<100> tbs;
int a[] = {5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6,6,6,6,7,9};
// 标记所有数的出现次数
for (auto e : a) tbs.set(e);
// 找出现次数1或2次的数
cout << "出现次数不超过2次的数:" << endl;
for (size_t i = 0; i < 100; ++i)
{
int cnt = tbs.get_count(i);
if (cnt == 1 || cnt == 2)
{
cout << i << " ";
}
}
cout << endl;
}位图的局限性很明显 —— 只能处理整形,而实际开发中,我们常需要判断 “字符串(如 URL、手机号)是否存在”,这时候就需要布隆过滤器登场了。
布隆过滤器是 1970 年由 Burton Howard Bloom 提出的概率型数据结构,核心思路:

关键特性:不存在是 100% 准确的,存在可能误判(哈希冲突导致),但误判率可通过参数调控。
布隆过滤器的误判率和三个参数相关:

;

;

;

;

;

。
这里的推导实际上是很复杂的,大家如果感兴趣的话,这里推荐两篇博客供大家参考:
布隆过滤器(Bloom Filter)- 原理、实现和推导、[布隆过滤器BloomFilter] 举例说明+证明推导。

时,误判率最低;

。
我们实现一个支持字符串的布隆过滤器,使用 3 种经典哈希函数(BKDR、AP、DJB)降低冲突率:
#include <iostream>
#include <vector>
#include <string>
#include <cmath>
#include <ctime>
using namespace std;
// 位图实现(复用之前的bitset)
namespace bit
{
template<size_t N>
class bitset
{
public:
bitset() { _bits.resize((N >> 5) + 1, 0); }
void set(size_t x) { if(x<N) _bits[x/32] |= (1 << (x%32)); }
void reset(size_t x) { if(x<N) _bits[x/32] &= ~(1 << (x%32)); }
bool test(size_t x) { return x<N && (_bits[x/32] & (1 << (x%32))); }
private:
vector<int> _bits;
};
}
// 哈希函数1:BKDR(Java字符串哈希同款)
struct HashFuncBKDR
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (char ch : s)
{
hash *= 31; // 31是经典素数,减少冲突
hash += ch;
}
return hash;
}
};
// 哈希函数2:AP
struct HashFuncAP
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (size_t i = 0; i < s.size(); ++i)
{
if (i % 2 == 0) // 偶数位
{
hash = ((hash << 7) ^ s[i]) ^ (hash >> 3);
}
else // 奇数位
{
hash ^= (~((hash << 11) ^ s[i]) ^ (hash >> 5));
}
}
return hash;
}
};
// 哈希函数3:DJB
struct HashFuncDJB
{
size_t operator()(const string& s)
{
size_t hash = 5381; // 初始值,经典素数
for (char ch : s)
{
hash = hash * 33 ^ ch; // 33=32+1,位运算高效
}
return hash;
}
};
// 布隆过滤器实现
template<size_t N, size_t X = 6,
class K = string,
class Hash1 = HashFuncBKDR,
class Hash2 = HashFuncAP,
class Hash3 = HashFuncDJB>
class BloomFilter
{
public:
// 插入元素
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % M;
size_t hash2 = Hash2()(key) % M;
size_t hash3 = Hash3()(key) % M;
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
}
// 查询元素(可能存在/一定不存在)
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % M;
if (!_bs.test(hash1)) return false;
size_t hash2 = Hash2()(key) % M;
if (!_bs.test(hash2)) return false;
size_t hash3 = Hash3()(key) % M;
if (!_bs.test(hash3)) return false;
return true; // 可能误判
}
// 计算理论误判率
double GetFalseProbability()
{
double p = pow((1.0 - pow(2.71828, -3.0 / X)), 3.0);
return p;
}
private:
static const size_t M = X * N; // 位图总长度
bit::bitset<M> _bs; // 底层位图
};
// 测试布隆过滤器
void TestBloomFilter()
{
// 测试基础功能
BloomFilter<10> bf1;
string strs[] = {"百度", "字节", "腾讯"};
for (auto& s : strs) bf1.Set(s);
cout << "基础查询测试:" << endl;
for (auto& s : strs)
{
cout << s << " -> " << (bf1.Test(s) ? "可能存在" : "一定不存在") << endl;
}
// 测试不存在的元素
cout << "摆渡 -> " << (bf1.Test("摆渡") ? "可能存在" : "一定不存在") << endl;
cout << "百渡 -> " << (bf1.Test("百渡") ? "可能存在" : "一定不存在") << endl;
// 测试误判率
cout << "\n误判率测试(1000万个URL):" << endl;
const size_t N = 10000000;
BloomFilter<N> bf2;
vector<string> v1;
string base_url = "https://www.cnblogs.com/clq/archive/2012/05/31/2528153.html";
// 插入1000万个不同的URL
for (size_t i = 0; i < N; ++i)
{
v1.push_back(base_url + to_string(i));
}
for (auto& s : v1) bf2.Set(s);
// 测试相似URL的误判率(前缀相同,后缀不同)
v1.clear();
size_t false_cnt1 = 0;
for (size_t i = 0; i < N; ++i)
{
string s = base_url + to_string(9999999 + i);
v1.push_back(s);
if (bf2.Test(s)) false_cnt1++;
}
cout << "相似URL误判率:" << (double)false_cnt1 / N << endl;
// 测试不相似URL的误判率
v1.clear();
size_t false_cnt2 = 0;
srand(time(0));
for (size_t i = 0; i < N; ++i)
{
string s = "孙悟空" + to_string(i + rand());
v1.push_back(s);
if (bf2.Test(s)) false_cnt2++;
}
cout << "不相似URL误判率:" << (double)false_cnt2 / N << endl;
cout << "理论误判率:" << bf2.GetFalseProbability() << endl;
}
int main()
{
TestBloomFilter();
return 0;
}布隆过滤器默认不支持删除,原因:
给每个位分配多个比特位(如 4 位),记录该位被映射的次数:

缺点:
优化思路:定期重建布隆过滤器,清空无效计数。
布隆过滤器凭借 “高效、省空间” 的特性,广泛应用于海量数据场景:
爬虫爬取网页时,需要避免重复爬取相同 URL:
缓存穿透:恶意请求不存在的数据,导致请求直达数据库,压垮数据库。
例如判断手机号是否注册:
除了位图和布隆过滤器,海量数据处理还有一些通用技巧,核心是 “分而治之”。
经典解法:小根堆
query 是字符串,每个约 50 字节,总大小约 500G,无法直接加载到内存。
核心思路:将大文件切分为小文件,让相同的 query 进入同一个小文件,再逐小文件找交集:
思路:哈希切分 + 哈希表统计
哈希扩展技术是处理海量数据的 “利器”,核心是 “用空间换时间”,并通过巧妙的设计极致压缩空间。掌握这些技术,不仅能轻松应对面试中的海量数据问题,也能在实际开发中解决内存不足、效率低下的痛点。