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

news2024/11/15 4:19:30

文章目录

  • 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 通过哈希函数映射到布隆过滤器中,然后再遍历第二个文件,遍历的同时,在布隆过滤器中看该元素是否存在,存在则为交集。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在windows的控制台实现贪吃蛇小游戏

欢迎来到博主的文章 博主id&#xff1a;代码小豪 前言&#xff1a;看懂这篇文章需要具有C语言基础&#xff0c;还要对单链表具有一定的理解。如果你只是想要试玩这个游戏&#xff0c;可以直接在文章末尾找到源码 由于实现贪吃蛇需要调用Win32 API函数&#xff0c;这些函数我会…

vue3项目中的404页面

vue3项目中的404页面 春节前的最后一篇技术博客了 写了不少vue项目&#xff0c;发现一直没有正确处理404页面。404页面的出现有这么几种可能&#xff1a; 错误输入了页面地址路由连接跳转时&#xff0c;某些路由已经不存在了&#xff0c;而程序员并没有正确处理 也就是说40…

jvm问题自查思路

本文聊一下最近处理了一些jvm的问题上&#xff0c;将这个排查和学习过程分享一下&#xff0c;看了很多资料&#xff0c;最终都会落地到几个工具的使用&#xff0c;本文主要是从文档学习、工具学习和第三方技术验证来打开认知和实践&#xff0c;希望有用。 一、文档 不仅知道了…

假期刷题打卡--Day26

1、MT1212乘法表 请编写一个简单程序&#xff0c;输出九九乘法表。输入n&#xff0c;就输出乘法表到n的地方。 格式 输入格式&#xff1a; 输入整型 输出格式&#xff1a; 输出整型。形式如&#xff1a;1*11 样例 1 输入&#xff1a; 5输出&#xff1a; 1*11 2*12 …

编曲入门软件哪个好 编曲入门教程 Studio One哪个版本好 Studio One6.5正版多少钱 FL Studio下载

新手编曲软件推荐&#xff1f;新手学编曲要先熟悉编曲逻辑&#xff0c;因此需要选择编曲逻辑简明易懂的宿主软件。编曲新手应该做哪些准备&#xff1f;准备好编曲设备、宿主软件、基础乐理学习资料。 一、编曲入门软件哪个好 新手入门阶段还没有形成系统的编曲思维&#xff0…

整合 Axios

大家好我是苏麟 , 今天聊一下Axios . Axios Axios 是一个基于 promise 网络请求库&#xff0c;作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpReques…

七、Nacos源码系列:Nacos服务发现

目录 一、服务发现 二、getServices()&#xff1a;获取服务列表 2.1、获取服务列表 2.2、总结图 三、getInstances(serviceId)&#xff1a;获取服务实例列表 3.1、从缓存中获取服务信息 3.2、缓存为空&#xff0c;执行订阅服务 3.2.1、调度更新&#xff0c;往线程池中…

【算法】{画决策树 + dfs + 递归 + 回溯 + 剪枝} 解决排列、子集问题(C++)

文章目录 1. 前言2. 算法例题 理解思路、代码46.全排列78.子集 3. 算法题练习1863.找出所有子集的异或总和再求和47.全排列II17.电话号码的字母组合 1. 前言 dfs问题 我们已经学过&#xff0c;对于排列、子集类的问题&#xff0c;一般可以想到暴力枚举&#xff0c;但此类问题用…

基于Chrome插件的Chatgpt对话无损导出markdown格式(Typora完美显示)

刚刚提交插件到Chrome插件商店正在审核&#xff0c;想尝试的可以先使用&#xff1a; https://github.com/thisisbaiy/ChatGPT-To-Markdown-google-plugin/tree/main 我将源代码上传至了GitHub&#xff0c;欢迎star, IssueGoogle插件名称为&#xff1a;ChatGPT to MarkDown plus…

海外云手机——平台引流的重要媒介

随着互联网的飞速发展&#xff0c;跨境电商、短视频引流以及游戏行业等领域正经历着迅猛的更新换代。在这个信息爆炸的时代&#xff0c;流量成为至关重要的资源&#xff0c;而其中引流环节更是关乎业务成功的关键。海外云手机崭露头角&#xff0c;成为这一传播过程中的重要媒介…

【保姆级教程|YOLOv8改进】【7】多尺度空洞注意力(MSDA),DilateFormer实现暴力涨点

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

Ribbon全方位解析:构建弹性的Java微服务

第1章 引言 大家好,我是小黑,咱们今天聊聊Ribbon,这货是个客户端负载均衡工具,用在Spring Cloud里面能让咱们的服务调用更加灵活和健壮。负载均衡,听起来挺高大上的,其实就是把外界的请求平摊到多个服务器上,避免某个服务器压力太大,其他的却在那儿闲着。 Ribbon的牛…

Springboot整合JUnit5框架

目录 第一章、在pom文件中导入依赖第二章、新建测试类第三章、新建测试方法 友情提醒: 先看文章目录&#xff0c;大致了解文章知识点结构&#xff0c;点击文章目录可直接跳转到文章指定位置。 第一章、在pom文件中导入依赖 SpringBoot2.2x之后的版本中spring-boot-starter-te…

Python(21)正则表达式中的“元字符”

大家好&#xff01;我是码银&#x1f970; 欢迎关注&#x1f970;&#xff1a; CSDN&#xff1a;码银 公众号&#xff1a;码银学编程 获取资源&#xff1a;公众号回复“python资料” 在本篇文章中介绍的是正则表达式中一部分具有特殊意义的专用字符&#xff0c;也叫做“元…

基于51 单片机的交通灯系统 源码+仿真+ppt

主要内容&#xff1a; 1&#xff09;南北方向的绿灯、东西方向的红灯同时亮40秒。 2&#xff09;南北方向的绿灯灭、黄灯亮5秒&#xff0c;同时东西方向的红灯继续亮。 3&#xff09;南北方向的黄灯灭、左转绿灯亮&#xff0c;持续20秒&#xff0c;同时东西方向的红灯继续…

rust语言tokio库底层原理解析

目录 1 rust版本及tokio版本说明1 tokio简介2 tokio::main2.1 tokio::main使用多线程模式2.2 tokio::main使用单线程模式 3 builder.build()函数3.1 build_threaded_runtime()函数新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图…

前端JavaScript篇之对执行上下文的理解

目录 对执行上下文的理解创建执行上下文 对执行上下文的理解 当我们在执行JavaScript代码时&#xff0c;JavaScript引擎会创建并维护一个执行上下文栈来管理执行上下文。执行上下文有三种类型&#xff1a;全局执行上下文、函数执行上下文和eval函数执行上下文。 在写代码的时…

第十三、十四个知识点:用javascript获取表单的内容并加密

我们先来写一段代码&#xff1a; <body><form action"#" method"post">//写一个表单<span>用户名&#xff1a;</span><input type"text" id"username" name"username"><span>密码&a…

BGP 双归不同运营商并且客户之间互为主备的部署实验

一、拓朴&#xff1a; 要求&#xff1a; 1、双方 ISP 均不得将客户 AS 做为穿越 AS 2、对于客户业务的出流量&#xff1a;客户 AS100 和 200 访问 ISP 时&#xff0c;AS100优选从 Line-1 线路&#xff0c;AS200 优选从 Line-2 访问&#xff0c;但当 Line-1 和 …

Springboot+vue的社区智慧养老监护管理平台设计与实现(有报告),Javaee项目,springboot vue前后端分离项目

演示视频&#xff1a; Springbootvue的社区智慧养老监护管理平台设计与实现&#xff08;有报告&#xff09;&#xff0c;Javaee项目&#xff0c;springboot vue前后端分离项目 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的社区智慧养老监护管理平台设…