C++寻位映射的究极密码:哈希扩展

C++寻位映射的究极密码:哈希扩展

文章目录

位图和布隆过滤器是基于哈希的一种常见应用,通常用来处理大体量数据,提升查找数据的效率

1.位图

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中?

放在哈希表中去寻找?不,这并不现实,因为哈希表的存储也是需要空间消耗的,况且是 40 亿个数据,如此庞大的数据计算机一般是很难存储

因此就诞生了位图的概念,位图简单来说就是把每个数按照哈希函数的计算,存储到每个比特位上。数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为 1,代表存在,为 0 代表不存在

在这里插入图片描述

1.1 位图的结构

template<size_t N>classbitset{public:bitset(){ _a.resize(N /32+1);}private: vector<int> _a;};

开辟一个 vector 数组 _a,这里我们以 int 作为位图的基本单位,那么就是把每个数据存储到 int 的比特位上

🔥值得注意的是:resize 的时候无论如何都要加 1,比如 100 个数据,除以 32,等于 3,余 4,那么就需要多一个 int 空间来存储,不能说每次都卡好刚好 32 整除

1.2 位图映射的比特位标记成1

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

i 用于确定在第几个 int 里,j 用于确定在第几个 int 的第几位上

在这里插入图片描述

二进制位从右到左是最低位到最高位,所以左移即可

1.3 位图映射的比特位标记成0

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

同理,因为按位与是有 0 就是 0,直接计算即可

在这里插入图片描述

1.4 位图映射判断为1 or 0

booltest(size_t x){ size_t i = x /32; size_t j = x %32;return _a[i]&(1<< j);}

注意这里是 &,而不是 &=,因为只需要判断,而不是修改

位图通常不支持删除功能,因为没有必要删除

2.布隆过滤器

在这里插入图片描述

我们在存储字符串数据时,是通过计算这个字符串的ASC||码值之和,然后通过哈希函数计算存入的,但是这可能会产生哈希冲突,但是数据量太大了,无法通过常规的方法解决

那么最简单的方法就是降低误判率,通过多个不同哈希函数计算,将一个值映射多个位置,这样不至于每次查找都会产生冲突

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了
  3. 将哈希与位图结合,即布隆过滤器
在这里插入图片描述

再举个现实点的例子,就能理解布隆过滤器存在的必要:

在这里插入图片描述

比如我们在注册账号昵称时,会显示是否已经被取过,先在布隆过滤器中进行查找,若不在,那么成功注册;如果在,那么就到数据库中查询,这样能减少数据库查询次数,降低负载压力,提升整体查询效率

2.1 布隆过滤器的结构

template<size_t N,classK= string,classHash1= BKDRHash,classHash2= APHash,classHash3= DJBHash>classBloomFilter{private: bitset<N> _bs;};

Hash1Hash2Hash3 是用于计算 string 存储的哈希函数,stl 库里是有 bitset 使用的,直接开辟位图空间即可

传送门:字符串Hash函数对比

2.2 布隆过滤器的哈希函数

structBKDRHash{ size_t operator()(const string& str){ size_t hash =0;for(auto ch : str){ hash = hash *131+ ch;}//cout <<"BKDRHash:" << hash << endl;return hash;}};structAPHash{ 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)));}}//cout << "APHash:" << hash << endl;return hash;}};structDJBHash{ size_t operator()(const string& str){ size_t hash =5381;for(auto ch : str){ hash +=(hash <<5)+ ch;}//cout << "DJBHash:" << hash << endl;return hash;}};

选取了三种计算冲突较小的哈希函数算法进行计算,因为需要多处使用,以仿函数的形式更加方便快捷

2.3 布隆过滤器的插入

voidSet(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);/* cout << hash1 << endl; cout << hash2 << endl; cout << hash3 << endl << endl;*/}

由于是以仿函数的方式进行,Hash1() 是匿名对象,有了对象才能以函数的形式调用参数

2.4 布隆过滤器映射判断为true or false

boolTest(const K& key){ size_t hash1 =Hash1()(key)% N;if(_bs.test(hash1)==false)returnfalse; size_t hash2 =Hash2()(key)% N;if(_bs.test(hash2)==false)returnfalse; size_t hash3 =Hash3()(key)% N;if(_bs.test(hash3)==false)returnfalse;returntrue;}

2.5 布隆过滤器的优缺点

🚩优点:

  1. 增加和查询元素的时间复杂度为: O(K), ( K 为哈希函数的个数,一般比较小),与数据量大小无关
  2. 哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
  6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

🚩缺点:

  1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
  2. 不能获取元素本身
  3. 一般情况下不能从布隆过滤器中删除元素
  4. 如果采用计数方式删除,可能会存在计数回绕问题

3.常见面试题

3.1 哈希切割

3.1.1 问题一

给一个超过 100G 大小的 log filelog 中存着 IP 地址, 设计算法找到出现次数最多的 IP 地址?

🛜解决方法:

对于超过 100G 的日志文件,直接加载到内存是不可行的,既然大的不行,就把文件分割为小文件一个个进行

使用哈希函数计算将 IP 映射到不同的小文件中,确保相同 IP 进入同一个文件,对每个小文件,使用哈希表统计 IP 频率,合并所有小文件的统计结果,就能找出出现次数最多的 IP

3.1.2 问题二

与上题条件相同,如何找到 top KIP

🛜解决方法:

既然相同 IP 一定进入同一个小文件,用 map 去统计每个文件中 IP 出现的次数即可

3.2 位图应用

3.2.1 问题一

给定 100 亿个整数,设计算法找到只出现一次的整数?

🛜解决方法:

对于 100 亿个整数(约 40GB 数据),直接加载到内存显然不可行。我们可以使用 位图哈希分治 相结合的方法高效解决这个问题——双位图法

在这里插入图片描述


使用两个位图,每个整数对应两位:

  • 00:整数未出现
  • 01:整数出现 1
  • 10:整数出现 2 次及以上

假设计算出第一个数据映射第一个位置,且第一次出现,则上面的位图第一位设置为 0,下面位图的第一位设置为 1。其他情况依次类推

template<size_t N>classtwobitset{public:voidset(size_t x){// 00 -> 01if(!_bs1.test(x)&&!_bs2.test(x)){ _bs2.set(x);}// 01 -> 10elseif(!_bs1.test(x)&& _bs2.test(x)){ _bs1.set(x); _bs2.reset(x);}// 本身10代表出现2次及以上,就不变了}boolis_once(size_t x){return!_bs1.test(x)&& _bs2.test(x);}private: bitset<N> _bs1; bitset<N> _bs2;};

最后遍历位图对每一位进行 is_once 函数的判断,符合一次的存入哈希表即可

3.2.2 问题二

给两个文件,分别有 100 亿个整数,我们只有 1G 内存,如何找到两个文件交集?

🛜解决方法:

还是利用两个位图的方式解决,一个文件映射一个位图,对应的位置 & 按位与一下,两个位置都为 1,则这个位置是交集,注意存储的值应该放在 set 里去重

3.2.3 问题三

1 个文件有 100 亿个 int1G 内存,设计算法找到出现次数不超过 2 次的所有整数

🛜解决方法:

和问题一的方法是一样的,只不过改一下表示方法而已

  • 00:整数未出现
  • 01:整数出现 1
  • 10:整数出现 2
  • 11:整数出现 3 次及以上

3.3 布隆过滤器应用

3.3.1 问题一

给两个文件,分别有 100亿个 query,我们只有 1G 内存,如何找到两个文件交集?分别给出精确算法和近似算法

🛜解决方法:

近似算法:

用文件 A 的所有 query 构建布隆过滤器,遍历文件 B 的每个 query,判断是否可能在 A 中,对布隆过滤器返回 “可能存在” 的 query,再在文件 A 中精确验证,但是这种方法并不百分百准确,可能存在误判

精确算法:

在这里插入图片描述

AB 文件都分割为同样数量的小文件,都上好编号,因为经过相同哈希函数计算,所以 AB 中相同的 query 必定分别进入 AiBi 文件中,因此 A0B0 比较,A1B1 进行比较,以此类推即可

在这里插入图片描述

3.3.2 问题二

如何扩展 BloomFilter 使得它支持删除元素的操作?

🛜解决方法:

将标准布隆过滤器的每个二进制位扩展为一个小计数器(通常 4-8 位),当插入元素时增加计数器,删除时减少计数器。只有当计数器为 0 时,才表示该位置未被占用


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

Read more

Flutter 组件 http_retry 的适配 鸿蒙Harmony 实战 - 驾驭智能请求重试机制、实现鸿蒙端弱网环境下的协议层自愈方案

Flutter 组件 http_retry 的适配 鸿蒙Harmony 实战 - 驾驭智能请求重试机制、实现鸿蒙端弱网环境下的协议层自愈方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 http_retry 的适配 鸿蒙Harmony 实战 - 驾驭智能请求重试机制、实现鸿蒙端弱网环境下的协议层自愈方案 前言 在鸿蒙(OpenHarmony)生态的全场景移动办公、复杂工业现场巡检以及对数据传输可靠性有“零容忍”要求的各类政务应用开发中,“网络的不确定性”是架构师必须直面的头号天敌。面对在电梯间切换 Wi-Fi、在地下车库偶发的 5G 信号掉包或者是服务器短暂的 503 报错。如果仅仅依靠单次请求判定。那么不仅会导致用户在业务操作过程中频繁看到“连接超时”的生硬弹窗,更会严重损耗鸿蒙应用在极端环境下的可用性口碑。 我们需要一种“策略先行、自动回旋”的网络治理艺术。 http_retry 是一套专为标准化 HTTP 客户端设计的重试中间件插件。它通过引入科学的指数退避(Exponential Backoff)与极其灵活的条件过滤算子(Filter)

By Ne0inhk
Flutter 组件 fluent_assertions 的适配 鸿蒙Harmony 实战 - 驾驭流式语义断言语法、实现鸿蒙端单元测试高可读性与复杂逻辑自证方案

Flutter 组件 fluent_assertions 的适配 鸿蒙Harmony 实战 - 驾驭流式语义断言语法、实现鸿蒙端单元测试高可读性与复杂逻辑自证方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 fluent_assertions 的适配 鸿蒙Harmony 实战 - 驾驭流式语义断言语法、实现鸿蒙端单元测试高可读性与复杂逻辑自证方案 前言 在鸿蒙(OpenHarmony)生态的大型分布式系统开发中,随着业务逻辑复杂度的指数级增长,原本简单的单元测试逐渐演变为由数百行冗长、枯燥且难以通过阅读理解其意图的 expect(result, isA<T>()) 堆砌而成的“代码仓库”。面对一个需要同时验证“返回值不为空 且 包含特定前缀 且 响应时间小于 50ms”的复合业务断言。如果仅仅依靠传统的 JUnit 风格写法。不仅会导致测试代码本身产生严重的维护债务,更会由于在测试失败时生成的机械化、无逻辑上下文的错误报文,引发开发者极其低效的排查过程。 我们需要一种“自然语言化、逻辑链式”的测试审计艺术。 fluent_

By Ne0inhk
Linux 进阶指令实操指南:文件查看、时间管理、搜索压缩全场景覆盖(附高频案例)

Linux 进阶指令实操指南:文件查看、时间管理、搜索压缩全场景覆盖(附高频案例)

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 文件内容操作:查看与编辑 * 1.1 cat & tac:正序 / 倒序查看文件 * 1.2 nano:轻量级文本编辑器 * 1.3 more & less:分页查看大文件 * 1.3.1 more:只能前进不能后退 * 1.3.2 less:可进可退,更实用 * 1.4 head & tail:查看文件首尾内容

By Ne0inhk
【Linux】Linux安全与密钥登录指南

【Linux】Linux安全与密钥登录指南

在使用Linux服务器时,确保服务器的安全至关重要。本文将为你介绍一些关键的Linux安全措施,包括开启密钥登录、查看登录日志、限制登录IP以及查看系统中能够登录的账号。以下内容适合小白用户,通过简单的操作就能有效提升服务器的安全性。 目录 1. Linux安全概述 2. 密钥登录的配置 * 生成密钥对 * 配置SSH密钥登录 3. 查看登录日志 4. 限制IP访问 * 设置IP封禁 * 允许特定IP访问 5. 查看系统可登录的账号 1. Linux安全概述 Linux系统安全主要依赖于控制访问权限、监控异常行为以及进行安全配置。通过适当的登录方式和访问限制,可以有效避免未经授权的访问。密钥登录是一种更安全的认证方式,避免了明文密码的风险。而登录日志和IP限制则可以帮助我们识别和防御潜在的入侵。 2. 密钥登录的配置 密钥登录是一种比密码登录更安全的方式,通过生成一对公钥和私钥来验证用户身份。以下是配置步骤。 2.1 生成密钥对 在客户端(例如你的电脑)上生成密钥对: ssh-keygen -t rsa -b 4096 -C

By Ne0inhk