当前位置: 首页 > news >正文

【数据结构进阶】哈希的应用

在这里插入图片描述

🔥个人主页: Forcible Bug Maker
🔥专栏: C++ || 数据结构

目录

  • 🌈前言
  • 🔥位图
    • 位图的概念
    • 位图的实现
    • 位图的变体
  • 🔥布隆过滤器
    • 布隆过滤器的概念
    • 布隆过滤器的插入
    • 布隆过滤器的查找
    • 布隆过滤器的删除
    • 布隆过滤器的实现
    • 如何选择哈希函数个数和布隆过滤器长度
    • 小结
  • 🔥海量数据处理(哈希切分)
  • 🌈结语

🌈前言

本篇博客主要内容:哈希在数据存储和处理中的应用

本篇博客大致可以分为三部分,分别对应三个知识点:位图,布隆过滤器,以及海量数据处理。话不多说,开始我们本次的内容。

🔥位图

位图的概念

位图:所谓位图,即使用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常用来判断某个数据是否存在。

下面看一道腾讯曾出过的面试题:
给40亿个不重复的无符号整数,没排过序。给定一个无符号整数,如何快速判断一个数是否在这40亿个数中。

有三种思路供参考:

  1. 一个一个数遍历,时间复杂度 O ( N ) O(N) O(N)
  2. 排序 O ( N l o g ( N ) ) O(Nlog(N)) O(Nlog(N)) ,然后利用二分查找 O ( l o g N ) O(logN) O(logN)
  3. 位图解决⭐
    数据是否在给定的整型数据中,结果是在或者不在,刚好是两种状态,那么可以用一个二进制比特位来代表是否存在的信息,如果二进制比特位为1,代表存在,为0则代表不存在。如:
    在这里插入图片描述
    按照这样的方式进行设计,既能有 O ( 1 ) O(1) O(1)(位图也是一种哈希思想)的查找速度,又能节省空间来存下40亿个数的存在状态。

位图的实现

#pragma once
#include<vector>
namespace ForcibleBugMaker
{template<size_t N>class bitset{public:bitset():_bs(N / 32 + 1, 0){}// x映射的位标记为1void set(size_t x){size_t i = x / 32;size_t j = x % 32;_bs[i] |= (1 << j);}// x映射的位置标记为0void reset(size_t x){size_t i = x / 32;size_t j = x % 32;_bs[i] &= (~(1 << j));}// 判断x映射的位是0还是1bool test(size_t x){size_t i = x / 32;size_t j = x % 32;return _bs[i] & (1 << j);}private:std::vector<size_t> _bs;};
}

测试一下咱们的代码:

#include<iostream>
#include"MyBitSet.h"
using namespace std;
using ForcibleBugMaker::bitset;int main()
{bitset<UINT_MAX> bs; // 我们自己实现的可以开40多亿个for (int i = 0; i < 20; i++) bs.set(i);for (int i = 5; i < 15; i++) bs.reset(i);for (int i = 0; i < 20; i++)if (bs.test(i))cout << i << " ";cout << endl;return 0;
}

在这里插入图片描述
虽然我们自己实现了bitset,但库中有原生提供的,被包在头文件#include<bitset>里。
在这里插入图片描述
如果想了解细节可以取参考相关文档:std::bitset
不过库中提供的bitset有一个缺陷,因为库里面的bitset的底层是使用静态数组实现的,所以其值无法开到INT_MAX
如图:
在这里插入图片描述
库中bitset运行结果:卡了一会,什么都没打印最后崩掉。

位图的优缺点:

  • 优点:增删查改非常快,同时节省空间
  • 缺点:只适用于整型

位图的变体

题目:
一个文件中存着100亿个整数,找出出现次数不超过2次的所有整数。
既然要判断数据存在的次数,咱们可以创建两个位图,用两个比特位表示一个数据:

  • 00:表示数据出现0次
  • 01:表示数据出现1次
  • 10:表示数据出现2次
  • 11:表示数据出现3次及以上

用类实现此数据结构,嵌套之前实现的bitset:

template<size_t N>
class twobitset
{
public: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_bs1.set(x);_bs2.reset(x);}else if (bit1 && !bit2) { // 10->11_bs2.set(x);}}void reset(size_t x){bool bit1 = _bs1.test(x);bool bit2 = _bs2.test(x);if (bit1 && bit2) { // 11->10_bs2.reset(x);}else if (!bit1 && bit2) { // 01->00_bs2.reset(x);}else if (bit1 && !bit2) { // 10->01_bs1.reset(x);_bs2.set(x);}}int get_count(size_t x){bool bit1 = _bs1.test(x);bool bit2 = _bs2.test(x);if (bit1 && bit2) { // 11return 3;}else if (!bit1 && bit2) { // 01return 1;}else if (bit1 && !bit2) { // 10return 2;}else return 0;}private:bitset<N> _bs1;bitset<N> _bs2
};

这时候,将数据放入上面类创建的对象中,就可以快速找出出现次数不超过2次的数据了。

#include<iostream>
#include"MyBitSet.h"
using namespace std;
using ForcibleBugMaker::twobitset;int main()
{twobitset<100> tbs;int arr[] = { 1,33,33,33,33,55,42,42,27,27,27,24,24,24,24,5,2 };for (auto& e : arr) {tbs.set(e);}for (int i = 0; i < 100; i++) {int flag = tbs.get_count(i);if (flag == 1 || flag == 2)cout << i << ":不超过两次" << endl;else if (flag == 3)cout << i << ":大于两次" << endl;}return 0;
}

在这里插入图片描述

位图的应用:

  1. 快速查找某个数据是否在一个集合中
  2. 排序+去重
  3. 求两个集合的交集,并集等
  4. 操作系统中磁盘块标记

🔥布隆过滤器

布隆过滤器的概念

当我们在客户端刷视频的时候,它会不断给我们推荐新内容,在推荐之前,会经行去重操作。那么问题来了,这个去重操作是如何实现的?服务器记录了用户看过的内容的历史记录,当推荐视频时会从历史记录中进行筛选,过滤掉已经存在的记录。如何快速查找判呢?

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:位图一般只能处理整型,如果视频编号是字符串,就难以处理了
  3. 将哈希和位图结合,即布隆过滤器

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间

在这里插入图片描述

布隆过滤器的插入

在这里插入图片描述
向布隆过滤器中插入字符串:hello world
在这里插入图片描述
再向其中插入字符串:flower
在这里插入图片描述
一个字符串可以同时映射多个位,从而达到降低冲突的效果。

布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中

注:布隆过滤器如果判断某个元素为不存在时,该元素一定不存在;如果判断元素存在,则该元素可能存在,因为哈希函数仍然存在一定的误判,这时不可避免的。
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

布隆过滤器的删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素
比如:删除上图中flower元素,如果直接将该元素所对应的二进制比特位置0,hello world元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
然而使用这种设计方式依然有缺陷,不能确认元素是否真正在布隆过滤器中,还可能存在计数回绕。

布隆过滤器的实现

实现布隆过滤器并不困难,不过需要实现准备好哈希函数:

// 三种不同的字符串哈希函数
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 = std::string,class Hash1 = BKDRHash,class Hash2 = APHash,class Hash3 = DJBHash>
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){bool flag1 = Hash1()(key) % M;bool flag2 = Hash2()(key) % M;bool flag3 = Hash3()(key) % M;if (flag1 && flag2 && flag3)return true;else return false;}// 不支持删除,会影响其他值void reset(const K& key);
private:const size_t M = N * x;bitset<M> _bs;
};

如何选择哈希函数个数和布隆过滤器长度

如果布隆过滤器的长度不够,插入几个元素就满了,不管查找什么都会返回true,过滤器的效率此时就很低。
同时要考虑哈希函数的个数,哈希函数过多可能会导致效率低下,而哈希函数过少由可能导致哈希冲突率上升。
在这里插入图片描述

上图中:k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率

如何选择合适的k和m就很重要了,先看结果:
m = − n ∗ l n p ( l n 2 ) 2 m = -\text{\(\frac {n*lnp} {(ln2)^2}\)} m=(ln2)2nlnp
k = m n ∗ l n 2 k = \text{\(\frac m n\)}*ln2 k=nmln2

公式的推导,就使用来说并没有太大意义,这里可以简单推一下:
设k次哈希某一bit未置为1的概率是
( 1 − 1 m ) k (1-\text{\(\frac 1 m\)})^k (1m1)k
插入n个元素依然为0和为1的概率分别是
依然为0: ( 1 − 1 m ) n k (1-\text{\(\frac 1 m\)})^{nk} (1m1)nk
依然为1: 1 − ( 1 − 1 m ) n k 1-(1-\text{\(\frac 1 m\)})^{nk} 1(1m1)nk
标明某个元素是否在集合中所需的 k 个位置都按照如上的方法设置为 1,但是该方法可能会使算法错误的认为某一原本不在集合中的元素却被检测为在该集合中(False Positives),该概率由以下公式确定(当m取无穷小时,得到右式)
[ 1 − ( 1 − 1 m ) n k ] k ≈ ( 1 − e − k n / m ) k [1-(1-\text{\(\frac 1 m\)})^{nk}]^k\approx(1-e^{-kn/m})^k [1(1m1)nk]k(1ekn/m)k

注:极限公式,当 m l i m → ⁡ ∞ m\varinjlim\infin m lim时, ( 1 − 1 m ) m = e (1-\text{\(\frac 1 m\)})^m=e (1m1)m=e

小结

  • 优点:
    • 增加和查询元素的时间复杂度为: O ( K ) O(K) O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
    • 哈希函数相互之间没有关系,方便硬件并行运算
    • 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
    • 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
    • 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
    • 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
  • 缺陷:
    • 有误判率有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中
    • 不能获取元素本身
    • 一般情况下不能从布隆过滤器中删除元素
    • 如果采用计数方式删除,可能会存在计数回绕问题

🔥海量数据处理(哈希切分)

问题:
给两个文件,分别由100亿个query(查询:理解成字符串),我们只有1G内存,如何找两个文件的交集?
分析:假设每个query字符串50byte,100亿个query就是5000亿byte,约等于500G(1G约等于10亿多byte)。
这样的大小对于红黑树/哈希表来说是无能为力的。

解决方案1
使用一个布隆过滤器,将一个文件的query放进布隆过滤器,另一个文件依次查找,在的就是交集,问题是找到的交集不够准确,因为存在的值可能是误判的,但是交集一定被找到了。

解决方案2⭐
哈希切分,首先内存访问速度远远大于硬盘,大文件放到内存搞不定,我们可以考虑将大文件切成小份文件,再放入内存处理。
切分不能平均切分,因为平均切分以后,每个小文件都要依次进行暴力处理,效率还是会很低。
哈希切分,是依次读取文件中的query,i = HashFunc(query) % N,N为准备切分成的小文件的个数,使内存得以放下,query放进第i号小文件,这样A和B中相同的query算出的哈希值是一样的,就能放到相同的小文件当中,效率就提升了。
本质是相同的query在哈希切分的过程中,一定进入同一个小文件的Ai和Bi,不可能出现A中的query进入Ai,但是B中相同的query进入Bj的情况,所以对Ai和Bi求交集即可,不需要Ai和Bj求交集。(其中i和j是不同的整数)
在这里插入图片描述
哈希切分的问题是,每个小文件是不均分的,可能会导致某个小文件很大内存放不下。导致这种情况的原因有二:

  1. 小文件中大部分是同一query
  2. 这个小文件由很多不同的query组成(本质为哈希冲突过多)

针对问题一,我们可以将小文件的query放入set,进行去重。针对问题二,需要我们再换一个哈希函数对小文件进行二次切分。
所以当我们遇到大于1G的小文件时,可以放入set中找交集,如果set的insert抛出了异常(申请内存失败),那么就说明放不下的原因是情况二,对小文件进行二次切分后再对应找交集。

🌈结语

哈希的三个应用,分别是位图,布隆过滤器,以及海量数据处理。位图能以少量的空间快速存放数据的存在情况;布隆过滤器中需要注意k,m和n之间合适的比例关系;最后海量数据处理使用哈希切分的方式,将大文件分割成小文件,从而使计算机能够对其进行处理。本篇博客的内容到这里就结束了,感谢大家的支持♥

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Matlab-use-yalmip-and-cplex12-10/
  • Flink开发语言选择:Java vs Scala,哪种更适合你的项目?
  • RAG与LLM原理及实践(11)--- Milvus hybrid search 源码分析及思想
  • 操作符5 学习编程的第23天
  • vue项目名修改、webstorm和idea创建的项目重命名、重构项目、修改项目名称
  • 【海思SS626 | 内存管理】海思芯片的OS内存、MMZ内存设置
  • Web详解
  • 初识CSS(三)
  • redis超过内存大小是否会挂?
  • 怎么将mov视频转换成mp4?将mov视频转换成mp4的方法
  • 文心一言 VS 讯飞星火 VS chatgpt (323)-- 算法导论22.4 4题
  • 渗透测试实战-HFS远程RCE漏洞利用
  • Python爬虫——爬取bilibili中的视频
  • 数据结构(学习)2024.8.8(栈,队列)
  • 【物联网】(防水篇)电子产品如何做到IPX7级别的防水?
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • chrome扩展demo1-小时钟
  • flask接收请求并推入栈
  • JavaScript-Array类型
  • Java到底能干嘛?
  • java架构面试锦集:开源框架+并发+数据结构+大企必备面试题
  • js作用域和this的理解
  • 百度小程序遇到的问题
  • 从重复到重用
  • 官方新出的 Kotlin 扩展库 KTX,到底帮你干了什么?
  • 微信如何实现自动跳转到用其他浏览器打开指定页面下载APP
  • 项目管理碎碎念系列之一:干系人管理
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • 云栖大讲堂Java基础入门(三)- 阿里巴巴Java开发手册介绍
  • 掌握面试——弹出框的实现(一道题中包含布局/js设计模式)
  • ​LeetCode解法汇总2808. 使循环数组所有元素相等的最少秒数
  • #java学习笔记(面向对象)----(未完结)
  • #NOIP 2014# day.1 T3 飞扬的小鸟 bird
  • #我与Java虚拟机的故事#连载02:“小蓝”陪伴的日日夜夜
  • (附源码)SSM环卫人员管理平台 计算机毕设36412
  • (含笔试题)深度解析数据在内存中的存储
  • (离散数学)逻辑连接词
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (十三)Flink SQL
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (四)模仿学习-完成后台管理页面查询
  • (图)IntelliTrace Tools 跟踪云端程序
  • (一)十分简易快速 自己训练样本 opencv级联haar分类器 车牌识别
  • (转)winform之ListView
  • .Family_物联网
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .Net Core webapi RestFul 统一接口数据返回格式
  • .NET 设计模式—适配器模式(Adapter Pattern)
  • .Net(C#)自定义WinForm控件之小结篇
  • .net开源工作流引擎ccflow表单数据返回值Pop分组模式和表格模式对比
  • .net企业级架构实战之7——Spring.net整合Asp.net mvc
  • ??eclipse的安装配置问题!??
  • @Autowired多个相同类型bean装配问题
  • @RequestBody的使用
  • [2009][note]构成理想导体超材料的有源THz欺骗表面等离子激元开关——