目录
[1. 位图](#1. 位图)
[1.1 概念](#1.1 概念)
[1.2 模拟实现](#1.2 模拟实现)
[1.3 位图的使用](#1.3 位图的使用)
[2. 布隆过滤器](#2. 布隆过滤器)
[2.1 应用场景](#2.1 应用场景)
[2.2 模拟实现](#2.2 模拟实现)
[2.3 误判率测试](#2.3 误判率测试)
[2.4 哈希切分](#2.4 哈希切分)
1. 位图
1.1 概念
概念:用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常用来判断某个数据存不存在
面试题【腾讯】:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中
set、排序+二分查找都不行
1G = 10 亿+Byte
40 亿整数,160 亿 Byte,16G
开空间和数据个数无关,和类型范围有关
无符号整数范围在 0-42 亿,开 42亿(2^32) bit,标记对应值在不在
1Byte = 8bit 实际开 2^29Byte = 0.5G

1.2 模拟实现

是否要考虑大小端呢?
不需要,大小端是底层的事情,用户在使用层,不论大小端,1 都是 00 00 00 01
与 &:有 0 为 0,同时为 1 才为 1
或 | :有 1 为 1,同时为 0 才为 0
异或 ^:相同为 0,相异为 1

cpp
// x映射的那个标记成1
void set(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_a[i] |= (1 << j); // 左移
}

cpp
// x映射的那个标记成0
void reset(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_a[i] &= (~(1 << j));
}

cpp
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
return _a[i] & (1 << j);
}
要开空间,要比 40 位数,就开 40/32+1 个,最多浪费 4Byte
BitSet.h
cpp
namespace qtw
{
template<size_t N> // 非类型模板参数
class bitset
{
public:
bitset()
{
_a.resize(N / 32 + 1);
}
// x映射的那个标记成1
void set(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_a[i] |= (1 << j);
}
// x映射的那个标记成0
void reset(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_a[i] &= (~(1 << j));
}
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
return _a[i] & (1 << j);
}
private:
vector<int> _a;
};
}
Test.cpp
cpp
int main()
{
qtw::bitset<1000> bs;
bs.set(1), bs.set(10), bs.set(100);
cout << bs.test(1) << endl; // 1
cout << bs.test(10) << endl; // 1
cout << bs.test(100) << endl; // 1
cout << bs.test(999) << endl<<endl; // 0
bs.set(999), bs.reset(10);
cout << bs.test(1) << endl; // 1
cout << bs.test(10) << endl; // 0
cout << bs.test(100) << endl; // 1
cout << bs.test(999) << endl << endl; // 1
//qtw::bitset<-1> bs1;
//bit::bitset<0xffffffff> bs2;
return 0;
}
库里面也实现了 bitset:set、reset、test 这三个接口用的多

1.3 位图的使用
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
给定100亿个整数,设计算法找到只出现一次的整数
思路 1:一个位图,2 个比特位标识一个值,有 00 01 10 11 四种结果,这里只用前三种就行
分别对应:没有出现、出现 1 次、出现 2 次及以上

思路 2:两个位图

BitSet.h
cpp
namespace qtw
{
template<size_t N> // 非类型模板参数
class bitset
{
public:
bitset()
{
_a.resize(N / 32 + 1);
}
// x映射的那个标记成1
void set(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_a[i] |= (1 << j);
}
// x映射的那个标记成0
void reset(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_a[i] &= (~(1 << j));
}
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
return _a[i] & (1 << j);
}
private:
vector<int> _a;
};
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
// 00 -> 01
if (!_bs1.test(x) && !_bs2.test(x))
{
_bs2.set(x);
} // 01 -> 10
else if (!_bs1.test(x) && _bs2.test(x))
{
_bs1.set(x);
_bs2.reset(x);
}
// 本身10代表出现2次及以上,就不变了
}
bool is_once(size_t x)
{
return !_bs1.test(x) && _bs2.test(x);
}
private:
bitset<N> _bs1; // 自定义类型自己调用构造函数
bitset<N> _bs2;
};
}
Test.cpp
cpp
int main()
{
int a[] = { 1,2,3,3,4,4,4,4,4,2,3,6,3,1,5,5,8,9};
qtw::twobitset<10> tbs;
for (auto e : a)
{
tbs.set(e);
}
for (auto e : a)
{
if (tbs.is_once(e))
{
cout << e << " "; // 6 8 9
}
}
cout << endl;
}
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集
错误方法:一个文件所有值映射到一个位图、另一个文件判断在不在
比如,文件1:3 文件2:3 3 3
出来的交集有重复,要去重
正确方法:两个文件分别映射到两个位图,对应位置 &
对应位置都为 1,那么这个值就是交集
Test.cpp
cpp
int main()
{
int a1[] = { 1,2,3,3,4,4,4,4,4,2,3,6,3,1,5,5,8,9 };
int a2[] = { 8,4,8,4,1,1,1,1 };
qtw::bitset<10> bs1;
qtw::bitset<10> bs2;
// 去重
for (auto e : a1) { bs1.set(e); }
for (auto e : a2) { bs2.set(e); }
for (int i = 0; i < 10; i++)
{
if (bs1.test(i) && bs2.test(i))
{
cout << i << " "; // 1 4 8
}
}
cout << endl;
}
位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
类似于问题 1,两个位图解决
00:没有出现 01:出现 1 次 10:出现 2 次 11:出现 3 次及以上
2. 布隆过滤器
上面是针对整形的办法,字符串呢?

数组不可能无限长,"哈哈"公司字符串变整形肯定和其他三个公司不一样,但取模后可能存在冲突导致误判
判断结果,在:可能误判 不在:一定准确
如何解决?
无法解决,哈希是把字符串挂到下面,但这里肯定挂不了,只能降低误判率
如何降低误判率(无法消除误判):布隆过滤器
我们取 3 个字符串哈希算法,每个字符串映射 3 个值

查找时,若 3 个映射值都能找到,则存在;有一个找不到,则不存在
判断结果,在:可能误判 (低概率) 不在:一定准确
2.1 应用场景
不需要精确的场景(允许误判)
eg:快速判断昵称是否注册过

需要精确的场景(不允许误判)

昵称在布隆过滤器没有找到(此结果一定为真),就直接返回
昵称在布隆过滤器找到(此结果可能为假),再去数据库里查找,再返回
降低数据库查询负载压力,提高效率
2.2 模拟实现
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素
一种支持删除的方法:多个位标识一个值,使用引用计数
将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计 数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作
缺陷:1. 无法确认元素是否真正在布隆过滤器中 2. 存在计数回绕
BloomFilter.h
cpp
#pragma once
#include<bitset>
#include<string>
struct BKDRHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for (auto ch : str)
{
hash = hash * 131 + ch;
}
return hash;
}
};
struct APHash
{
size_t operator()(const string& str)
{
size_t hash = 0;
for (size_t i = 0; i < str.size(); i++)
{
size_t ch = str[i];
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& str)
{
size_t hash = 5381;
for (auto ch : str)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N, class K = string,
class Hash1 = BKDRHash,
class Hash2 = APHash,
class Hash3 = DJBHash>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % N; // 匿名对象
_bs.set(hash1);
size_t hash2 = Hash2()(key) % N;
_bs.set(hash2);
size_t hash3 = Hash3()(key) % N;
_bs.set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % N;
if (_bs.test(hash1) == false)
return false;
size_t hash2 = Hash2()(key) % N;
if (_bs.test(hash2) == false)
return false;
size_t hash3 = Hash3()(key) % N;
if (_bs.test(hash3) == false)
return false;
return true;
}
private:
bitset<N> _bs;
};
Test.cpp
cpp
#include<iostream>
using namespace std;
#include "BloomFilter.h"
void TestBloomFilter()
{
BloomFilter<100> bf;
bf.Set("孙悟空");
bf.Set("猪八戒");
bf.Set("牛魔王");
bf.Set("二郎神");
cout << bf.Test("孙悟空") << endl; // 1
cout << bf.Test("猪八戒") << endl; // 1
cout << bf.Test("沙悟净") << endl; // 0
}
int main()
{
TestBloomFilter();
return 0;
}
2.3 误判率测试
如何选择哈希函数个数和布隆过滤器长度
k:哈希函数个数 m:布隆过滤器长度 n:插入的元素个数 p:误报率

Test.cpp
cpp
void TestBloomFilter2()
{
srand(time(0));
const size_t N = 100000;
BloomFilter<N * 8> bf;
std::vector<std::string> v1;
//std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
std::string url = "猪八戒";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(i));
}
for (auto& str : v1)
{
bf.Set(str);
}
// v2跟v1是相似字符串集(前缀一样),但是不一样
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
{
std::string urlstr = url;
urlstr += std::to_string(9999999 + i);
v2.push_back(urlstr);
}
size_t n2 = 0;
for (auto& str : v2)
{
if (bf.Test(str)) // 误判
{
++n2;
}
}
cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;
// 不相似字符串集
std::vector<std::string> v3;
for (size_t i = 0; i < N; ++i)
{
string url = "zhihu.com";
url += std::to_string(i + rand());
v3.push_back(url);
}
size_t n3 = 0;
for (auto& str : v3)
{
if (bf.Test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}
2.4 哈希切分
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
近似算法:把一个文件放到布隆过滤器,另一个文件判断在不在,在就是交集
精确算法:哈希切分
假设平均一个 query 是 30byte,100 亿 query 是 3000 亿 byte,是 300G
均等切分:假设把每个文件切成 1000 份,A 文件的 1000 份小文件,要和 B 文件的 1000 分小文件一一找,麻烦
哈希切分:i = Hash(query) % 1000 i 是多少,query 就进入第 i 个小文件
用同一个哈希算法,A 和 B 中相同的 query(交集) 一定会分别进入 Ai 和 Bi 编号相同的小文件
找交集,Ai 读出来放到一个 set,再依次读取 Bi 的 query,看在不在,在就是交集并且删掉。就可以找出 Ai 和 Bi 的交集

问题:平均切是 300M,但是我们是哈希切分,若冲突太多,会导致某个Ai文件太大,甚至超过1G,怎么办 ?
两个场景,比如 Ai 有 5G
-
4G 都是相同 query,1G 是不同的 query
-
大多数都是不同的 query
解决方案:
-
先把 Ai 的 query 读到一个 set,如果 set 的 insert 报错抛异常(bad alloc),那么就说明是大多数 query 都是不同的。如果能够全部读出来,insert 到 set 里面,那么说明 Ai 有大量相同的 query
-
如果抛异常,说明有大量冲突,再换一个哈希函数,再进行二次切分。
给一个超过 100G 大小的 log file,log 中存着 IP 地址,设计算法找到出现次数最多的 IP 地址?
给一个超过 100G 大小的日志文件,日志文件中存着 IP 地址,设计算法找到出现次数最多的 IP 地址?
设计算法找到出现次数最多的 k 个 IP 地址

相同 ip 一定进入了同一个小文件,用 map 去分别统计每个小文件中 ip 出现次数即可
位图的使用那里的第 2 个问题也可以用哈希切分直接取模,分成不同的文件......,太麻烦
本篇的分享就到这里了,感谢观看 ,如果对你有帮助,别忘了点赞+收藏+关注 。
小编会以自己学习过程中遇到的问题为素材,持续为您推送文章