《C++ STL哈希容器全解析:哈希拓展位图以及布隆过滤等高阶应用》
前言:
unordered_map作为STL的高性能哈希容器,其平均O(1)时间复杂度的查找能力远超普通顺序容器(如vector)和树型容器(如map)。底层通过哈希表实现,其核心优势在于:
高频检索优化:对insert/find/erase操作提供常数级时间保证(理想情况下),特别适合缓存系统、词频统计等需要快速访问的场景
哈希特性应用:可巧妙解决LeetCode两数之和、分布式系统一致性哈希等需要快速匹配的问题

目录
一、位图
位图概念
位图的底层原理
腾讯面试题
位图的实现
二、布隆过滤器
布隆过滤器概念
布隆过滤器数据结构
布隆过滤器的查找
布隆过滤器删除
布隆过滤器优点
模拟代码
三、海量数据面试题拓展
哈希切割
位图应用
布隆过滤器

一、位图
位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用
来判断某个数据存不存在的。
位图的底层原理
位图的核心思想是 用1个比特(bit)代表1个状态(如是否存在),而不是传统的 bool(1字节=8bit)或 int(4字节=32bit),极大节省内存空间。
例如:
存储 [0, 1, 4, 7]:
bool[8] 占用 8字节
bitset<8> 占用 1字节(存储为 10010011)
腾讯面试题
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。【腾讯】
- 遍历,时间复杂度O(N)
- 排序(O(NlogN)),利用二分查找: logN
- 位图解决
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一
个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0
代表不存在。
比如:
#include <vector>
#include <iostream>class Bitmap {
private:std::vector<uint32_t> bits; // 每个uint32_t存储32位public:Bitmap() : bits((1ULL << 32) / 32, 0) {} // 512MB内存初始化void add(uint32_t num) {bits[num / 32] |= (1U << (num % 32));}bool contains(uint32_t num) {return (bits[num / 32] >> (num % 32)) & 1U;}
};int main() {Bitmap b;b.add(123456789); // 添加一个数std::cout << b.contains(123456789) << std::endl; // 输出1(存在)std::cout << b.contains(987654321) << std::endl; // 输出0(不存在)return 0;
}

核心思路:uint32_t 范围是 0 ~ 2³²-1(约42.9亿),所以需要 2³² bits ≈ 512MB 内存(远小于16GB)。这样查询时直接访问对应比特位,时间复杂度 O(1)。
位图的实现
class bitset
{
public:bitset(size_t bitCount): _bit((bitCount>>5)+1), _bitCount(bitCount){}// 将which比特位置1void set(size_t which){if(which > _bitCount)return;size_t index = (which >> 5);size_t pos = which % 32;_bit[index] |= (1 << pos);}// 将which比特位置0void reset(size_t which){if(which > _bitCount)return;size_t index = (which >> 5);size_t pos = which % 32;_bit[index] &= ~(1<<pos);}// 检测位图中which是否为1bool test(size_t which){if(which > _bitCount)return false;size_t index = (which >> 5);size_t pos = which % 32;return _bit[index] & (1<<pos);}// 获取位图中比特位的总个数size_t size()const{ return _bitCount;}// 位图中比特为1的个数size_t Count()const{int bitCnttable[256] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2,3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3,3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3,4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4,3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5,6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4,4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5,6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5,3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3,4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6,6, 7, 6, 7, 7, 8};size_t size = _bit.size();size_t count = 0;for(size_t i = 0; i < size; ++i){int value = _bit[i];int j = 0;while(j < sizeof(_bit[0])){unsigned char c = value;count += bitCntTable[c];++j;value >>= 8;}}return count;}
private:vector<int> _bit;size_t _bitCount;};
二、布隆过滤器
引言:我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间
- 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
- 将哈希与位图结合,即布隆过滤器
布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

布隆的使用就像下面这句话一般:
Data structures are nothing different. They are like the bookshelves of your application where you can organize your data. Different data structures will give you different facility and benefits. To properly use the power and accessibility of the data structures you need to know the trade-offs of using one.
大意是不同的数据结构有不同的适用场景和优缺点,你需要仔细权衡自己的需求之后妥善适用它们,布隆过滤器就是践行这句话的代表。
布隆过滤器数据结构
布隆过滤器是一个 bit 向量或者说 bit 数组

如果我们需要插入百度等词的话


这里值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。
这里如果我想判断一个csdn是否有存入,如果给出的是1.5.8,那么5为0说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 csdn 这个值不存在。
那么如果,我想查“chenyehao”这个词在不在,返回的是1.3.7的时候,一定存在吗,对比上图插入过程,显然不一定,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “chenyehao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断这个值存在。
布隆过滤器的查找
总结一下,如上,布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
布隆过滤器删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
缺陷:
1. 无法确认元素是否真正在布隆过滤器中,同查找。
2. 存在计数回绕
布隆过滤器优点
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
布隆过滤器缺陷
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中,,,(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
模拟代码
struct BKDRHash
{size_t operator()(const string& s){// BKDRsize_t value = 0;for (auto ch : s){value *= 31;value += ch;}return value;}
};
struct APHash
{size_t operator()(const string& s){size_t hash = 0;for (long i = 0; i < s.size(); i++){if ((i & 1) == 0){hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));}}return hash;}
};
struct DJBHash{size_t operator()(const string& s){size_t hash = 5381;for (auto ch : s){hash += (hash << 5) + ch;}return hash;}
};
template<size_t N,
size_t X = 5,
class K = string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash>
class BloomFilter
{
public:void Set(const K& key){size_t len = X*N;size_t index1 = HashFunc1()(key) % len;size_t index2 = HashFunc2()(key) % len;size_t index3 = HashFunc3()(key) % len;/* cout << index1 << endl;cout << index2 << endl;cout << index3 << endl<<endl;*/_bs.set(index1);_bs.set(index2);_bs.set(index3);}bool Test(const K& key){size_t len = X*N;size_t index1 = HashFunc1()(key) % len;if (_bs.test(index1) == false)return false;size_t index2 = HashFunc2()(key) % len;if (_bs.test(index2) == false)return false;size_t index3 = HashFunc3()(key) % len;if (_bs.test(index3) == false)return false;return true; // 存在误判的}// 不支持删除,删除可能会影响其他值。void Reset(const K& key);
private:bitset<X*N> _bs;
};
三、海量数据面试题拓展
哈希切割
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
位图应用
1. 给定100亿个整数,设计算法找到只出现一次的整数?
2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
布隆过滤器
1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出
精确算法和近似算法
2. 如何扩展BloomFilter使得它支持删除元素的操作

