【高阶数据结构】位图布隆过滤器

文章目录

  • 1. 位图
    • 1.1什么是位图
    • 1.2为什么会有位图
    • 1.3 实现位图
    • 1.4 位图的应用
  • 2. 布隆过滤器
    • 2.1 什么是布隆过滤器
    • 2.2 为什么会有布隆过滤器
    • 2.3 布隆过滤器的插入
    • 2.4 布隆过滤器的查找
    • 2.5 布隆过滤器的模拟实现
    • 2.6 布隆过滤器的优点
    • 2.7 布隆过滤器缺陷
  • 3. 海量数据面试题
    • 3.1 哈希切割
    • 3.2 位图
    • 3.3 布隆过滤器

1. 位图

1.1什么是位图

位图(Bitmap)是一种基于位操作的数据结构,用于表示一组元素的集合信息。它通常是一个仅包含0和1的数组,其中每个元素对应集合中的一个元素。位图中的每个位(或者可以理解为数组的元素)代表一个元素是否存在于集合中。当元素存在时,对应位的值为1;不存在时,对应位的值为0。

位图常用于判断某个元素是否属于某个集合,或者对多个集合做交集、并集或差集等集合运算。它的优点在于速度快,内存空间占用小,能表示大范围的数据。由于它的高效性和节省空间的特性,位图在很多场景中都有广泛的应用。

1.2为什么会有位图

给大家举个例子,假设存在 40 亿个不重复的无符号整数,也就是正数,没排过序,那么给一个无符号的整数,如何判断这个数是否在那 40 亿个数之中呢?

很多人第一想法就是直接遍历这 40 亿个整数,时间复杂度为 O(N),每次遍历都判断是否等于这个给定的整数就可以了,这个想法对于少量数据是可实行的,但是这里数据有 40 亿个整数,换算成内存就是:40亿 * 4 = 160亿个字节,160亿 * 8 = 1240亿个比特位,1240亿 / 1024 / 1024 / 1024 ≈ 16GB,也就是说通过遍历这 40 亿个整数的话需要使用 16 GB的内存,那么这对于运行内存大的勉强可以实现,对于我们普通的电脑来说,几乎是不可能的。所以通过遍历这 40 亿个整数然后查找的想法是行不通的。

那么又有人会说了,我先将这 40 亿个数字进行排序,然后查找的时候使用二分查找的方式来查询不就可以了吗?我们来看看排序后再而二分查找的时间复杂度为多少:排序的时间复杂度为 O(NlogN),二分查找的时间复杂度为 O(logN),总体时间复杂度为 O((N+1)logN),也就接近于 O(N),所以这个也是行不通的。

而通过我们的位图实现的话,因为一个数字是否存在只需要使用一个比特位就可以实现,那么这 40 亿个数字总共需要:40亿 / 8 / 1024 / 1024 ≈ 512MB,这样就极大的节省了内存空间。

在这里插入图片描述

1.3 实现位图

首先我们的位图类中需要存在一个字节数组和计数器用来计算数组中的元素:

public class MyBitSet {
    private byte[] elem;
    private int usedSize;

    public MyBitSet() {
        //默认给一个字节
        elem = new byte[1];
    }

    public MyBitSet(int n) {
        //根据给定的整数的最大值来创建数组
        elem = new byte[n/8 + 1];  //这里只开辟n/8个字节是不够的,需要多一个
    }
}

然后就是插入操作,我们应该如何标记指定位置为 1 呢?因为一个字节的大小是 8 个比特位,所以数组的下标就可以用 n/8 来表示,这是知道了该元素在数组的哪个下标,再通过 n%8 可以知道该元素在该字节的哪一个比特位。假设我们要存储 13,13 / 8 = 1,那么该元素就存储在数组的 1 下标处,然后将一个字节从右开始的第 13 % 8 = 5 个位置设置为 1,也就是 arr[13/8] |= (1<<(13%8))。

public void set(int val) {
    //如果给的数字为负数的话,我们这里直接抛出异常
    //这里也可以不抛出异常,如果我们知道给定的数据中的最小的负数,那么我们可以在插入的时候每个数都加上一个值
    //使得每个数字都是正数
    if (val < 0) throw new ArrayIndexOutOfBoundsException();
    int arrayIndex = val/8;
    int bitIndex = val%8;
    elem[arrayIndex] |= (1<<bitIndex);
    usedSize++;
}

当查看指定数据是否存在的时候,还是通过相同的方法,查看 arr[arrIndex]位置的从右往左的第 bitIndex 上的位置是否为 1:

public boolean get(int val) {
    if (val < 0) throw new ArrayIndexOutOfBoundsException();
    int arrayIndex = val/8;
    int bitIndex = val%8;
    if ((elem[arrayIndex] & (1<<bitIndex)) != 0) return true;
    return false;
}

如果我们想要将已经插入的数据删除的话,也是将对应的比特位设置为 0 就可以了:

public void reSet(int val) {
    if (val < 0) throw new ArrayIndexOutOfBoundsException();
    int arrayIndex = val/8;
    int bitIndex = val%8;
    elem[arrayIndex] &= ~(1<<bitIndex);
    usedSize--;
}

查看当前位图存在多少数据:

public int getUsedSize() {
        return this.usedSize;
    }

上面是我们自己实现的位图,其实 Java 为我们提供了位图 BitSet

在这里插入图片描述
只不过,我们这里数组使用的是 byte,而 BitSet 使用的是 Long:

在这里插入图片描述
这里的初始化,数组中元素的个数也是1:

在这里插入图片描述

1.4 位图的应用

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

局限性:位图只能操作整数,对于小数的字符串无法处理,所以就出现了布隆过滤器。

2. 布隆过滤器

2.1 什么是布隆过滤器

布隆过滤器(Bloom Filter)是1970年由布隆提出的,它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

布隆过滤器的基本原理是将一个元素通过多个哈希函数映射到一个位数组中的多个位置,然后将这些位置置为1。在查询时,检查这些位置是否都是1,如果是,则认为元素可能存在于集合中。需要注意的是,布隆过滤器有可能产生误判(false positive),即认为某个元素存在于集合中,但实际上并不存在;但不会产生误判(false negative),即认为某个元素不存在于集合中,但实际上存在。

布隆过滤器的应用场景包括但不限于防止垃圾邮件、搜索引擎、数据库缓存、数据安全等。例如,在Redis数据库中,可以使用布隆过滤器解决缓存穿透问题,即当查询一个不存在的数据时,直接返回空,而不是再次从数据库中查询。这样可以避免对数据库的过多压力,提高系统的性能和稳定性。

2.2 为什么会有布隆过滤器

对于海量数据的处理,使用普通的方法是无法做到的,虽然位图可以处理大量的数据,但是位图只能处理整数,对于一些字符串,位图是无法处理的,那么有人就会想到使用哈希表来存储,哈希表虽然可以存储多种数据类型的数据,但是存储在哈希表中也需要占用大量的空间。那么如何做到即可以存储整数之外的数据类型,也可以节省空间呢?那就是布隆过滤器,布隆过滤器结合了位图和哈希表,使得布隆过滤器可以应用多种场景。

2.3 布隆过滤器的插入

布隆过滤器的插入其实和位图的插入类似,只不过在布隆过滤器插入之前,会通过多个哈希函数来得到不同的结果,为什么会需要多个哈希函数呢?我们都知道哈希冲突,当我们进行哈希操作的时候,很容易就会发生哈希冲突,通过多个哈希函数计算出来的哈希函数可以大大降低哈希冲突。

在这里插入图片描述

在这里插入图片描述

2.4 布隆过滤器的查找

布隆过滤器的查找就是将需要查找的元素,通过多个哈希函数的计算,然后根据计算的值去位图中寻找,如果计算的多个哈希值中某一个位置为 0,那么该元素一定不存在,但是如果所有位置都为 1,也不能一定确定该元素就在布隆过滤器中。假设 baidu 通过哈希函数计算出来的哈希值为1、3、7,tencent 计算出来的哈希值为3、4、8,alibaba 计算出来的哈希值为 2、5、6,而我们要查找的 zijietiaodong 计算出来的哈希值为 1、4、6,那么就不能说 zijietiaodong 一定存在于布隆过滤器中。

2.5 布隆过滤器的模拟实现

首先我们需要构建出几个哈希函数,那么构建多少个哈希函数才合适呢?这里有公式:

  • 设bitarray大小为m,样本数量为n,失误率为p
  • 使用样本数量n和失误率p可以算出m,公式为:
    在这里插入图片描述
  • 所使用哈希函数个数k可以由以下公式求得:
    在这里插入图片描述
  • 通过 bitarray 的大小m和哈希函数的个数可以计算出失误率p:
    在这里插入图片描述
public class SimpleHash {
    //容量
    private int cap;
    //随机种子
    private int seed;

    public SimpleHash(int cap, int seed) {
        this.cap = cap;
        this.seed = seed;
    }

    /**
     * 将当前的字符串转换为哈希值
     * @param val
     * @return
     */
    public int hash(String val) {
        int result = 0;
        for (int i = 0; i < val.length(); i++) {
            result = seed * result + val.charAt(i);
        }
        return (cap - 1) & result;
    }
}

布隆过滤器的初始化:

public class BloomFilter {
    private static final int DEFAULT_SIZE = 1 << 24;
    private static final int[] seeds = new int[]{5,7,11,13,31,37,61};
    private BitSet bitSet;  //位图用来存储元素
    private SimpleHash[] func;  //存放多个哈希函数
    private int size;

    public BloomFilter() {
        bitSet = new BitSet(DEFAULT_SIZE);
        func = new SimpleHash[seeds.length];
        for (int i = 0; i < seeds.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE,seeds[i]);
        }
    }
}

布隆过滤器的插入:

/**
 * 布隆过滤器的插入
 * @param val
 */
public void set(String val) {
    if (val == null) return;
    for (SimpleHash f : func) {
        bitSet.set(f.hash(val));
    }
    size++;
}

布隆过滤器的查找:

/**
 * 布隆过滤器中查找某个元素是否存在
 * @param val
 * @return
 */
public boolean contains(String val) {
    if (val == null) return false;
    for (SimpleHash f : func) {
        if (!bitSet.get(f.hash(val))) return false;
    }
    return true;  //有误判
}

布隆过滤器不建议进行删除操作,因为在删除一个元素的时候可能会影响其他元素。

2.6 布隆过滤器的优点

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

2.7 布隆过滤器缺陷

  1. 误判率:这是布隆过滤器最主要的缺陷。由于哈希函数的随机性和冲突的可能性,即使位数组中的某些位置被置为1,也不一定表示元素一定存在于集合中。因此,布隆过滤器有可能产生误判(false positive),即认为某个元素存在于集合中,但实际上并不存在。
  2. 不能删除元素:一旦将元素加入布隆过滤器中,就不能将其删除。这是因为删除操作会破坏位数组中已经记录的信息,导致查询的准确性下降。这也是布隆过滤器的一个主要限制。
  3. 不能获取元素本 身
  4. 如果采用计数

3. 海量数据面试题

3.1 哈希切割

1. 给定一个超过 100G 大小的log file,log 中存着 IP 地址,设计算法找到出现次数最多的 IP 地址,同样那如果是出现次数 topK 的IP呢?

如果忽略大小的话,我们可以使用 <K,V> 模型来统计每个 IP 出现的次数,但问题就是这里的数据非常多,使用 <K,V> 模型的话,一次性是无法都加载到内存当中的。那么我们将这个大文件分成多个小文件不就可以了?可以是可以,可是如何划分呢?有人会说均分不就可以了?均分可以吗?均分不可以,因为你均分之后,你其中一个文件中的出现次数最多的 IP 地址并不是所有文件中 IP 地址出现次数最多的,那么我们应如何将 IP 地址相同的划分到一个文件呢?

因为 IP 地址本质上也是一个字符串,所以我们可以使用哈希函数先将 IP 地址转换为一个整数,然后将得到的一样的哈希值给放到一个文件中,那么这样相同的 IP 地址最终就会被分到同一个文件中,这种思路叫做 哈希分割

当完成哈希分割之后,我们统计每个文件中 IP 地址出现的次数,最后得到出现次数最多的 IP 地址。

3.2 位图

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

这道题目有两种思路:

  1. 哈希切割
  2. 位图

首先是哈希切割,我们将出现的所有的相同的整数给分割到一个文件中,然后遍历每个文件,统计文件中整数出现的整数的次数,最终得到只出现了一次的整数。

然后第二种思路就是通过位图来解决。但是位图不是只能判断某一个元素是否存在吗?这道题目不是要求出现了一次的整数吗?那么使用位图该如何解决呢?

是的,一个位图只能判断某个元素是否存在,但是两个位图就可以判断某个元素出现的次数了,两个位图的相同位置可能的结果是 00、01、10和11,我们使用 00 表示该元素未出现,01 表示该元素出现了一次,10 表示出现了两次,11表示出现的次数超过 2 次。

在这里插入图片描述
这是使用了两位位图的情况,如果我就想只用一个位图解决可以吗?可以的,之前位图一个比特位表示一个元素,这里我们可以使用两个比特位来表示一个元素。一个字节之前可以表示 8 个元素,现在我只表示 4 个元素,那么 arrIndex 就为 n / 4,bitIndex 就为 2*(n % 4),这样每两个比特位可以表示的结果就有 00、01、10、11,这样就可以判断出只出现了一次的整数了。

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

同样是两种思路:

  1. 哈希切割
  2. 位图

我们两个文件都使用相同的哈希函数对文件中的数据进行切割,切割完成之后,分别遍历两个相同下标的文件,看这两个文件中是否有相同的元素。

第二种思路,使用位图,分别使用一个位图,只用 0 和 1 标识某个元素是否存在,都存入位图之后,再分别遍历这两个位图,如果相同位置上的数据都为 1 的话,该位置表示的整数就是两个文件中的交集。

3.3 布隆过滤器

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

既然提到精确算法和近似算法,那么这个问题就有两种思路可以解决:

  1. 哈希切割(精确算法)
  2. 布隆过滤器(近似算法)

这个做法和上面类似,分别遍历两个文件,将文件分割成 n 个大小的文件,然后再分别遍历对应的文件,找两个文件中存在的 query。

第二种思路是布隆过滤器,先遍历一个文件,将该文件中的 query 通过哈希函数映射到布隆过滤器中,然后再遍历第二个文件,遍历的同时,在布隆过滤器中看该元素是否存在,存在则为交集。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/379229.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C++11】右值引用 | 移动构造赋值 | 万能引用 | 完美转发

文章目录 一、引言二、左值和右值什么是左值什么是右值 三、左值引用和右值引用左值引用右值引用左值引用与右值引用的比较 四、右值引用的使用场景和意义左值引用的使用场景左值引用的短板用右值引用和移动语义解决上述问题移动构造移动赋值 右值引用引用左值 - std::move()ST…

spring boot学习第十二篇:mybatis框架中调用存储过程控制事务性

1、MySQL方面&#xff0c;已经准备好了存储过程&#xff0c;参考&#xff1a;MYSQL存储过程&#xff08;含入参、出参&#xff09;-CSDN博客 2、pom.xml文件内容如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"…

3.4-媒资管理之视频处理+xx-job分布式任务

文章目录 媒资管理6 视频处理6.1 需求6.1.1 总体需求6.7.3 FFmpeg 的基本使用6.7.4 视频处理工具类 6.2 分布式任务处理6.2.1 什么是分布式任务调度6.2.2 XXL-JOB介绍6.2.3 搭建XXL-JOB6.2.3.1 调度中心6.2.3.2 执行器6.2.3.3 执行任务 6.2.4 分片广播 6.3 技术方案6.3.1 作业分…

Optimism为 CQT提供价值 20 万美元的生态系统资助,以表彰其支持

Covalent Network&#xff08;CQT&#xff09; 是 Web3 生态系统中关键的“数据可用性”层&#xff0c;在与 Optimism Collective 多年的合作中取得了骄人的成果。Covalent Network&#xff08;CQT&#xff09;对于 Optimism 跨链数据的增长产生了直接的影响&#xff0c;而这一…

Java并发基础:Deque接口和Queue接口的区别?

核心概念 Deque&#xff08;double ended queue&#xff0c;双端队列&#xff09;和Queue&#xff08;队列&#xff09;都是Java集合框架中的接口&#xff0c;它们用于处理元素的排队和出队&#xff0c;但是它们之间存在一些重要的区别&#xff0c;如下&#xff1a; 1、Queue…

C语言——oj刷题——调整数组使奇数全部都位于偶数前面

题目&#xff1a; 输入一个整数数组&#xff0c;实现一个函数&#xff0c;来调整该数组中数字的顺序使得数组中所有的奇数位于数组的前半部分&#xff0c;所有偶数位于数组的后半部分。 一、实现方法&#xff1a; 当我们需要对一个整数数组进行调整&#xff0c;使得奇数位于数…

Git详细讲解

文章目录 一、Git相关概念二、本地分支中文件的添加 、提交2.1 文件状态2.2 创建Git仓库2.2.1 git init2.2.2 git clone 2.3 添加操作(git add)2.4 提交操作&#xff08;git commit&#xff09;2.5 撤销操作2.5.1 撤销 add操作2.5.2 撤销 commit操作2.5.3 覆盖上一次的commit操…

机器学习系列——(十八)K-means聚类

引言 在众多机器学习技术中&#xff0c;K-means聚类以其简洁高效著称&#xff0c;成为了数据分析师和算法工程师手中的利器。无论是在市场细分、社交网络分析&#xff0c;还是图像处理等领域&#xff0c;K-means都扮演着至关重要的角色。本文旨在深入解析K-means聚类的原理、实…

EF Core 模型优先——根据类对象创建数据表

需要的nuget包&#xff1a; Microsoft.EntityframeworkCore.SqlServer &#xff08;根据自己的数据库类型选择对应的nuget包&#xff09; Microsoft.EntityframeworkCore.Tools Microsoft.VisualStudio.Web.CodeGeneration.Design 说明&#xff1a; &#xff08;1&#xf…

排序算法---插入排序

原创不易&#xff0c;转载请注明出处。欢迎点赞收藏~ 插入排序是一种简单直观的排序算法&#xff0c;它的基本思想是将待排序的元素分为已排序和未排序两部分&#xff0c;每次从未排序部分中选择一个元素插入到已排序部分的合适位置&#xff0c;直到所有元素都插入到已排序部分…

考研数据结构笔记(3)

顺序表存储结构 存储结构顺序结构定义基本操作的实现静态分配问题 动态分配代码功能 顺序表的特点: 顺序表小结顺序表的插入删除插入删除小结 顺序表的查找按位查找按值查找小结 存储结构 顺序结构 定义 线性表是具有相同数据类型的n(n>0)个数据元素的有限序列(每个数据元素…

大数据 - Spark系列《五》- Spark常用算子

Spark系列文章&#xff1a; 大数据 - Spark系列《一》- 从Hadoop到Spark&#xff1a;大数据计算引擎的演进-CSDN博客 大数据 - Spark系列《二》- 关于Spark在Idea中的一些常用配置-CSDN博客 大数据 - Spark系列《三》- 加载各种数据源创建RDD-CSDN博客 大数据 - Spark系列《…

vue3 之 组合式API—模版引用

模版引用的概念 通过ref标识获取真实的dom对象或者组件实例对象 如何使用&#xff08;以获取dom为例 组件同理&#xff09; 1️⃣调用ref函数生成一个ref对象 2️⃣通过ref标识绑定ref对象到标签 dom中使用 父组件中可以看到打印出来proxy里面只有一个属性&#xff0c;其他…

Vue中 常用的修饰符有哪些

Vue是一款建立在JavaScript框架上的开源前端库&#xff0c;已经成为当今前端开发人员最喜爱的选择之一。它的简洁语法和强大的功能使得开发者可以轻松地构建交互性的网页应用程序。在Vue中&#xff0c;修饰符是一个重要的概念&#xff0c;它们可以帮助我们更好地控制和定制DOM元…

wyh的迷宫

涉及知识点&#xff1a;求迷宫能否到达终点的&#xff0c;而不是求路径数的&#xff0c;用bfs时可以不用重置状态数组&#xff08;回溯&#xff09;。 题目描述 给你一个n*m的迷宫&#xff0c;这个迷宫中有以下几个标识&#xff1a; s代表起点 t代表终点 x代表障碍物 .代…

【多模态】27、Vary | 通过扩充图像词汇来提升多模态模型在细粒度感知任务(OCR等)上的效果

文章目录 一、背景二、方法2.1 生成 new vision vocabulary2.1.1 new vocabulary network2.1.2 Data engine in the generating phrase2.1.3 输入的格式 2.2 扩大 vision vocabulary2.2.1 Vary-base 的结构2.2.2 Data engine2.2.3 对话格式 三、效果3.1 数据集3.2 图像细粒度感…

数据库管理-第147期 最强Oracle监控EMCC深入使用-04(20240207)

数据库管理147期 2024-02-07 数据库管理-第147期 最强Oracle监控EMCC深入使用-04&#xff08;20240207&#xff09;1 发现Exadata2 Exadata监控计算节点&#xff1a;存储节点RoCE交换机管理交换机PDU 总结 数据库管理-第147期 最强Oracle监控EMCC深入使用-04&#xff08;202402…

网工内推 | 物流、航空业信息安全工程师,CISP认证优先,带薪年假

01 盛辉物流 招聘岗位&#xff1a;网络安全工程师 职责描述: 1、对机房内的网络、系统进行安全扫描和安全防护&#xff0c;上报安全评估报告、日常安全作业计划报告&#xff1b; 2、负责防火墙等安全设备、漏洞扫描工具的维护管理和使用&#xff0c;负责日常安全运维工作及安…

JavaScript相关(一)——作用域

本篇将从JS的执行上下文开始&#xff0c;去理解&#xff1a;变量提升、 栈式调用、作用域和闭包。 参考&#xff1a; 浏览器工作原理与实践 JS执行上下文 执行上下文是 JavaScript 执行一段代码时的运行环境&#xff0c;比如调用一个函数&#xff0c;就会生成这个函数的执行…

【MySQL】_JDBC编程

目录 1. JDBC原理 2. 导入JDBC驱动包 3. 编写JDBC代码实现Insert 3.1 创建并初始化一个数据源 3.2 和数据库服务器建立连接 3.3 构造SQL语句 3.4 执行SQL语句 3.5 释放必要的资源 4. JDBC代码的优化 4.1 从控制台输入 4.2 避免SQL注入的SQL语句 5. 编写JDBC代码实现…