🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉哈希函数👈
- 👉位图👈
- 位图概念
- 位图的实现
- 位图的应用
- 👉布隆过滤器👈
- 布隆过滤器的提出
- 布隆过滤器概念
- 布隆过滤器的实现
- 1. 查找
- 2. 删除
- 布隆过滤器优点和缺点
- 👉总结👈
👉哈希函数👈
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数的设计原则
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 到 m-1 之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常见哈希函数
- 直接定址法(常用)
取关键字的某个线性函数为散列地址:Hash(Key) = A * Key + B
优点:简单、均匀且不存在哈希冲突
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
面试题:字符串中第一个只出现一次字符- 除留余数法(常用)
设散列表中允许的地址数为 m,取一个不大于 m,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:Hash(key) = key% p(p <= m),将关键码转换成哈希地址。除留余数法存在哈希冲突,重点解决哈希冲突。- 平方取中法(了解)
假设关键字为 1234,对它平方就是 1522756,抽取中间的 3 位 227 作为哈希地址;再比如关键字为 4321,对它平方就是 18671041,抽取中间的 3 位 671 (或710)作为哈希地址。平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。- 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况,只适用于整数。
5. 随机数法(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key),其中 random 为随机数函数。通常应用于关键字长度不等时采用此法。
6. 数学分析法(了解)
设有 n 个 d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况。例如:
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如 1234 改成 4321 )、右环位移(如 1234 改成 4123 )、左环移位、前两数与后两数叠加(如 1234 改成 12+34=46 )等方法。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。
👉位图👈
位图概念
- 面试题
给 40 亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这 40 亿个数中。
- 遍历,时间复杂度O(N)
- 排序(O(NlogN)),利用二分查找: logN
- 位图解决
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态。那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为 1,代表存在,为 0代表不存在。比如:
2. 位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。注意:位图所开空间要大于或等于整型的范围,因为有可能有些数字数,有些数字小。
位图的实现
位图最核心的三个节点是set
、reset
和test
,set
是将 x 对应的比特位设置为 1,reset
是将 x 对应的比特位设置为 0,test
是查看 x 在不在。
#pragma once
namespace Joy
{
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize(N / 8 + 1, 0); // 多开一个字节,防止越界
}
// 将比特位设置为1
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
// 将比特位设置为0
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= ~(1 << j);
}
// 查x在不在
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return (_bits[i] & (1 << j));
}
private:
vector<char> _bits;
};
void BitSetTest()
{
bitset<100> bs;
bs.set(8);
bs.set(9);
bs.set(20);
cout << bs.test(8) << endl;
cout << bs.test(9) << endl;
cout << bs.test(20) << endl;
bs.reset(8);
bs.reset(9);
bs.reset(20);
cout << bs.test(8) << endl;
cout << bs.test(9) << endl;
cout << bs.test(20) << endl;
}
void BitSetTest2()
{
// 开出42亿9千万个比特位
bitset<-1> bs1;
bitset<0xffffffff> bs2;
}
}
位图的应用
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
题目一:给定 100 亿个整数,设计算法找到只出现一次的整数? 这种题目是 KV 的统计次数搜索模型,一个数字出现次数有三种情况:出现零次、出现一次以及出现两次及以上,这三种情况只需要用两个比特位就可以表示。
template <size_t N>
class twobitset
{
public:
void set(size_t x)
{
bool inset1 = _bs1.test(x);
bool inset2 = _bs2.test(x);
// 00
if(inset1 == false && inset2 == false)
{
// -> 01
_bs2.set(x);
}
else if (inset1 == false && inset2 == true) // 01
{
// -> 10
_bs1.set(x);
_bs2.reset(x);
}
else if (inset1 == true && inset2 == false)
{
// -> 11
_bs2.set(x);
}
// else是x出现三次及三次以上
}
void print_once_num()
{
for (size_t i = 0; i < N; ++i)
{
if (_bs1.test(i) == false && _bs2.test(i) == true)
cout << i << " ";
}
cout << endl;
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
void BitSetTest3()
{
int a[] = { 3, 4, 5, 2, 3, 4, 4, 4, 4, 12, 77, 65, 44, 4, 44, 99, 33, 33, 33, 6, 5, 34, 12 };
twobitset<100> bs;
for (auto e : a)
{
bs.set(e);
}
bs.print_once_num();
}
题目二:给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
题目三:1 个文件有 100 亿个 int,1G内存,设计算法找到出现次数不超过2次的所有整数
注:位图只能处理整形。采用位图标记字符串时,必须先将字符串转化为整形的数字,找到位图中对应的比特 位,但是在字符串转整形的过程中,可能会出现不同字符串转化为同一个整形数字,即冲突,因此一般不会直接用位图处理字符串。
👉布隆过滤器👈
布隆过滤器的提出
我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
- 用哈希表存储用户记录,缺点:浪费空间。
- 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
- 将哈希与位图结合,即布隆过滤器。
布隆过滤器概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
布隆过滤器的实现
1. 查找
布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为 1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中。注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
布隆过滤器的应用场景
布隆过滤器拓展资料:链接 ,字符串哈希算法:链接。
布隆过滤器的公式
#pragma once
#include "BitSet.h"
namespace Joy
{
// 哈希函数
struct HashBKDR
{
// BKDR
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
struct HashAP
{
// AP
size_t operator()(const string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB
{
// DJB
size_t operator()(const string& key)
{
size_t hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
// N表示准备要映射N个值,布隆过滤器处理的类型通常是字符串
template<size_t N,
class K = string, class Hash1 = HashBKDR, class Hash2 = HashAP, class Hash3 = HashDJB>
class BloomFilter
{
public:
void set(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
_bits.set(hash1);
size_t hash2 = Hash2()(key) % (_ratio * N);
_bits.set(hash2);
size_t hash3 = Hash3()(key) % (_ratio * N);
_bits.set(hash3);
}
bool test(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
if (!_bits.test(hash1))
return false; // 准确的
size_t hash2 = Hash2()(key) % (_ratio * N);
if (!_bits.test(hash2))
return false; // 准确的
size_t hash3 = Hash3()(key) % (_ratio * N);
if (!_bits.test(hash3))
return false; // 准确的
return true; // 可能存在误判
}
// 能否支持删除? ->布隆过滤器一般不支持删除,如果要
// 删除的话,可以加上映射位置的引用计数。
void reset(const K& key);
private:
const static size_t _ratio = 5; // _ratio为倍率
bitset<_ratio* N> _bits; // 使用自己实现的bitset
};
void BloomFilterTest1()
{
BloomFilter<10> bf;
string arr1[] = { "苹果", "西瓜", "阿里", "美团", "苹果", "字节", "西瓜", "苹果", "香蕉", "苹果", "腾讯" };
for (auto& str : arr1)
{
bf.set(str);
}
string arr2[] = { "苹果111", "西瓜", "阿里2222", "美团", "苹果dadcaddxadx", "字节", "西瓜sSSSX", "苹果 ", "香蕉", "苹果$", "腾讯" };
for (auto& str : arr2)
{
cout << str << ":" << bf.test(str) << endl;
}
}
void BloomFilterTest2()
{
srand(time(0));
const size_t N = 1000000;
BloomFilter<N> bf;
cout << sizeof(bf) << endl;
std::vector<std::string> v1;
std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
for (size_t i = 0; i < N; ++i)
{
v1.push_back(url + std::to_string(1234 + i));
}
for (auto& str : v1)
{
bf.set(str);
}
// 相似
std::vector<std::string> v2;
for (size_t i = 0; i < N; ++i)
{
std::string url = "http://www.cnblogs.com/-clq/archive/2021/05/31/2528153.html";
url += std::to_string(rand() + i);
v2.push_back(url);
}
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(rand() + i);
v3.push_back(url);
}
size_t n3 = 0;
for (auto& str : v3)
{
if (bf.test(str))
{
++n3;
}
}
cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}
}
存储空间越大,布隆过滤器的误判率就会越低。注:库里的 bitset 是静态数组,空间太大容易栈溢出,可以在堆上开空间。
2. 删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给 k 个计数器(k 个哈希函数计算出的哈希地址)加一,删除元素时,给 k 个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
布隆过滤器优点和缺点
优点
- 增加和查询元素的时间复杂度为:O(K), (K 为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
布隆过滤器的题目
题目一:给两个文件,分别有 100 亿个 query,我们只有 1G 内存,如何找到两个文件交集?分别给出精确算法和近似算法。 query 是查询,常见的查询有:网络请求、SQL 语句等,它们都是字符串。近似算法允许一些误判,那么我们可以先将一个文件的 query 放入布隆过滤器中,再去查另一个文件的 query 在不在布隆过滤器中,就可以找到两个文件的交集了(还需要去重)。精确算法如下图所示:
题目二:如何扩展BloomFilter使得它支持删除元素的操作?布隆过滤器一般是不支持删除的,支持删除可能会影响其他值的查询。如果想要支持删除,可以添加引用计数。但是支持了删除,空间消耗就更多,优势就变小了。
哈希切分题目:给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
Linux 系统命令实现
注:哈希切分并不是平均切分,而是相同的哈希值的字符串等进入到相同的位置。
哈希的应用非常地广泛,服务器存储中也使用到了哈希。
👉总结👈
本篇博客主要讲解了常见的哈希函数,什么是位图、位图的实现和应用、什么是布隆过滤器、布隆过滤器的实现和优缺点以及哈希切分等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️