如何判断一个元素在亿级数据中是否存在?

Java架构/如何判断一个元素在亿级数据中是否存在? 前言 最近有朋友问我这么一个面试题目: 现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。 需求其实很清晰,只是要判断一个数据是否存在即可。 但这里有一个比较重要的前提:非常庞大的数据。 常规实现 先不考虑这个条件,我们脑海中出现的第一种方案是什么? 我想大多数想到的都是用 HashMap 来存放数据,因为它的写入查询的效率都比较高。 写入和判断元素是否存在都有对应的 API,所以实现起来也比较简单。 为此我写了一个单测,利用 Ha 阅读详情

来源公众号:crossoverJie   本文作者:crossoverJie

分享一篇技术好文,也是一道常见的面试题。相信大家看完也会有所收获

640

前言

最近有朋友问我这么一个面试题目:

现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。

需求其实很清晰,只是要判断一个数据是否存在即可。

但这里有一个比较重要的前提:非常庞大的数据。

常规实现

先不考虑这个条件,我们脑海中出现的第一种方案是什么?

我想大多数想到的都是用 HashMap 来存放数据,因为它的写入查询的效率都比较高。

写入和判断元素是否存在都有对应的 API,所以实现起来也比较简单。

为此我写了一个单测,利用 HashSet 来存数据(底层也是 HashMap );同时为了后面的对比将堆内存写死:

-Xms64m -Xmx64m -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError 

为了方便调试加入了 GC 日志的打印,以及内存溢出后 Dump 内存。

    @Test	
    public void hashMapTest(){	
        long star = System.currentTimeMillis();	
        Set<Integer> hashset = new HashSet<>(100) ;	
        for (int i = 0; i < 100; i++) {	
            hashset.add(i) ;	
        }	
        Assert.assertTrue(hashset.contains(1));	
        Assert.assertTrue(hashset.contains(2));	
        Assert.assertTrue(hashset.contains(3));	
        long end = System.currentTimeMillis();	
        System.out.println("执行时间:" + (end - star));	
    }

当我只写入 100 条数据时自然是没有问题的。

还是在这个基础上,写入 1000W 数据试试:

640

执行后马上就内存溢出。

640

可见在内存有限的情况下我们不能使用这种方式。

实际情况也是如此;既然要判断一个数据是否存在于集合中,考虑的算法的效率以及准确性肯定是要把数据全部 load 到内存中的。

Bloom Filter

基于上面分析的条件,要实现这个需求最需要解决的是 如何将庞大的数据load到内存中。

而我们是否可以换种思路,因为只是需要判断数据是否存在,也不是需要把数据查询出来,所以完全没有必要将真正的数据存放进去。

伟大的科学家们已经帮我们想到了这样的需求。

BurtonHowardBloom 在 1970 年提出了一个叫做 BloomFilter(中文翻译:布隆过滤)的算法。

它主要就是用于解决判断一个元素是否在一个集合中,但它的优势是只需要占用很小的内存空间以及有着高效的查询效率。

所以在这个场景下在合适不过了。

Bloom Filter 原理

下面来分析下它的实现原理。

官方的说法是:它是一个保存了很长的二级制向量,同时结合 Hash 函数实现的。

听起来比较绕,但是通过一个图就比较容易理解了。

640

如图所示:

  • 首先需要初始化一个二进制的数组,长度设为 L(图中为 8),同时初始值全为 0 。

  • 当写入一个 A1=1000 的数据时,需要进行 H 次 hash 函数的运算(这里为 2 次);与 HashMap 有点类似,通过算出的 HashCode 与 L 取模后定位到 0、2 处,将该处的值设为 1。

  • A2=2000 也是同理计算后将 4、7 位置设为 1。

  • 当有一个 B1=1000 需要判断是否存在时,也是做两次 Hash 运算,定位到 0、2 处,此时他们的值都为 1 ,所以认为 B1=1000 存在于集合中。

  • 当有一个 B2=3000 时,也是同理。第一次 Hash 定位到 index=4 时,数组中的值为 1,所以再进行第二次 Hash 运算,结果定位到 index=5 的值为 0,所以认为 B2=3000 不存在于集合中。

整个的写入、查询的流程就是这样,汇总起来就是:

对写入的数据做 H 次 hash 运算定位到数组中的位置,同时将数据改为 1 。当有数据查询时也是同样的方式定位到数组中。 一旦其中的有一位为 0 则认为数据肯定不存在于集合,否则数据可能存在于集合中。

所以布隆过滤有以下几个特点:

  1. 只要返回数据不存在,则肯定不存在。

  2. 返回数据存在,但只能是大概率存在。

  3. 同时不能清除其中的数据。

第一点应该都能理解,重点解释下 2、3 点。

为什么返回存在的数据却是可能存在呢,这其实也和 HashMap 类似。

在有限的数组长度中存放大量的数据,即便是再完美的 Hash 算法也会有冲突,所以有可能两个完全不同的 A、B 两个数据最后定位到的位置是一模一样的。

这时拿 B 进行查询时那自然就是误报了。

删除数据也是同理,当我把 B 的数据删除时,其实也相当于是把 A 的数据删掉了,这样也会造成后续的误报。

基于以上的 Hash 冲突的前提,所以 BloomFilter 有一定的误报率,这个误报率和 Hash 算法的次数 H,以及数组长度 L 都是有关的。

自己实现一个布隆过滤

算法其实很简单不难理解,于是利用 Java 实现了一个简单的雏形。

public class BloomFilters {	
    /**	
     * 数组长度	
     */	
    private int arraySize;	
    /**	
     * 数组	
     */	
    private int[] array;	
    public BloomFilters(int arraySize) {	
        this.arraySize = arraySize;	
        array = new int[arraySize];	
    }	
    /**	
     * 写入数据	
     * @param key	
     */	
    public void add(String key) {	
        int first = hashcode_1(key);	
        int second = hashcode_2(key);	
        int third = hashcode_3(key);	
        array[first % arraySize] = 1;	
        array[second % arraySize] = 1;	
        array[third % arraySize] = 1;	
    }	
    /**	
     * 判断数据是否存在	
     * @param key	
     * @return	
     */	
    public boolean check(String key) {	
        int first = hashcode_1(key);	
        int second = hashcode_2(key);	
        int third = hashcode_3(key);	
        int firstIndex = array[first % arraySize];	
        if (firstIndex == 0) {	
            return false;	
        }	
        int secondIndex = array[second % arraySize];	
        if (secondIndex == 0) {	
            return false;	
        }	
        int thirdIndex = array[third % arraySize];	
        if (thirdIndex == 0) {	
            return false;	
        }	
        return true;	
    }	
    /**	
     * hash 算法1	
     * @param key	
     * @return	
     */	
    private int hashcode_1(String key) {	
        int hash = 0;	
        int i;	
        for (i = 0; i < key.length(); ++i) {	
            hash = 33 * hash + key.charAt(i);	
        }	
        return Math.abs(hash);	
    }	
    /**	
     * hash 算法2	
     * @param data	
     * @return	
     */	
    private int hashcode_2(String data) {	
        final int p = 16777619;	
        int hash = (int) 2166136261L;	
        for (int i = 0; i < data.length(); i++) {	
            hash = (hash ^ data.charAt(i)) * p;	
        }	
        hash += hash << 13;	
        hash ^= hash >> 7;	
        hash += hash << 3;	
        hash ^= hash >> 17;	
        hash += hash << 5;	
        return Math.abs(hash);	
    }	
    /**	
     *  hash 算法3	
     * @param key	
     * @return	
     */	
    private int hashcode_3(String key) {	
        int hash, i;	
        for (hash = 0, i = 0; i < key.length(); ++i) {	
            hash += key.charAt(i);	
            hash += (hash << 10);	
            hash ^= (hash >> 6);	
        }	
        hash += (hash << 3);	
        hash ^= (hash >> 11);	
        hash += (hash << 15);	
        return Math.abs(hash);	
    }	
}
  1. 首先初始化了一个 int 数组。

  2. 写入数据的时候进行三次 hash 运算,同时把对应的位置置为 1。

  3. 查询时同样的三次 hash 运算,取到对应的值,一旦值为 0 ,则认为数据不存在。

实现逻辑其实就和上文描述的一样。

下面来测试一下,同样的参数:

-Xms64m -Xmx64m -XX:+PrintHeapAtGC
    @Test	
    public void bloomFilterTest(){	
        long star = System.currentTimeMillis();	
        BloomFilters bloomFilters = new BloomFilters(10000000) ;	
        for (int i = 0; i < 10000000; i++) {	
            bloomFilters.add(i + "") ;	
        }	
        Assert.assertTrue(bloomFilters.check(1+""));	
        Assert.assertTrue(bloomFilters.check(2+""));	
        Assert.assertTrue(bloomFilters.check(3+""));	
        Assert.assertTrue(bloomFilters.check(999999+""));	
        Assert.assertFalse(bloomFilters.check(400230340+""));	
        long end = System.currentTimeMillis();	
        System.out.println("执行时间:" + (end - star));	
    }

执行结果如下:

640

只花了 3 秒钟就写入了 1000W 的数据同时做出来准确的判断。


640

当让我把数组长度缩小到了 100W 时就出现了一个误报, 400230340 这个数明明没在集合里,却返回了存在。

这也体现了 BloomFilter 的误报率。

我们提高数组长度以及 hash 计算次数可以降低误报率,但相应的 CPU、内存的消耗就会提高;这就需要根据业务需要自行权衡。

Guava 实现

640

刚才的方式虽然实现了功能,也满足了大量数据。但其实观察 GC 日志非常频繁,同时老年代也使用了 90%,接近崩溃的边缘。

总的来说就是内存利用率做的不好。

其实 Google Guava 库中也实现了该算法,下面来看看业界权威的实现。

-Xms64m -Xmx64m -XX:+PrintHeapAtGC 

    @Test	
    public void guavaTest() {	
        long star = System.currentTimeMillis();	
        BloomFilter<Integer> filter = BloomFilter.create(	
                Funnels.integerFunnel(),	
                10000000,	
                0.01);	
        for (int i = 0; i < 10000000; i++) {	
            filter.put(i);	
        }	
        Assert.assertTrue(filter.mightContain(1));	
        Assert.assertTrue(filter.mightContain(2));	
        Assert.assertTrue(filter.mightContain(3));	
        Assert.assertFalse(filter.mightContain(10000000));	
        long end = System.currentTimeMillis();	
        System.out.println("执行时间:" + (end - star));	
    }

也是同样写入了 1000W 的数据,执行没有问题。

640

观察 GC 日志会发现没有一次 fullGC,同时老年代的使用率很低。和刚才的一对比这里明显的要好上很多,也可以写入更多的数据。

源码分析

那就来看看 Guava 它是如何实现的。

构造方法中有两个比较重要的参数,一个是预计存放多少数据,一个是可以接受的误报率。 我这里的测试 demo 分别是 1000W 以及 0.01。

640

Guava 会通过你预计的数量以及误报率帮你计算出你应当会使用的数组大小 numBits 以及需要计算几次 Hash 函数 numHashFunctions 。

这个算法计算规则可以参考维基百科。

put 写入函数

真正存放数据的 put 函数如下:

640

  • 根据 murmur3_128 方法的到一个 128 位长度的 byte[]。

  • 分别取高低 8 位的到两个 hash 值。

  • 再根据初始化时的到的执行 hash 的次数进行 hash 运算。

bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);

其实也是 hash取模拿到 index 后去赋值 1.

重点是 bits.set() 方法。

640

其实 set 方法是 BitArray 中的一个函数, BitArray 就是真正存放数据的底层数据结构。

利用了一个 long[]data 来存放数据。

所以 set() 时候也是对这个 data 做处理。

640

  • 在 set 之前先通过 get() 判断这个数据是否存在于集合中,如果已经存在则直接返回告知客户端写入失败。

  • 接下来就是通过位运算进行 位或赋值。

  • get() 方法的计算逻辑和 set 类似,只要判断为 0 就直接返回存在该值。

mightContain 是否存在函数

640

前面几步的逻辑都是类似的,只是调用了刚才的 get() 方法判断元素是否存在而已。

总结

布隆过滤的应用还是蛮多的,比如数据库、爬虫、防缓存击穿等。

特别是需要精确知道某个数据不存在时做点什么事情就非常适合布隆过滤。

这段时间的研究发现算法也挺有意思的,后续应该会继续分享一些类似的内容。

如果对你有帮助那就分享一下吧。

本文的示例代码参考这里:

https://github.com/crossoverJie/JCSprout

你的点赞与分享是对我最大的支持

640?wx_fmt=png

640?


更多推荐内容

聊聊Docker镜像

RPC太太太太太太太容易理解啦!

【算法】BloomFilter 如何判断一个元素在亿级数据中是否存在? 1.概述 转载: 如何判断一个元素在亿级数据中是否存在? 前言 最近有朋友问我这么一个面试题目: 现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。 需求其实很清晰,只是要判断一个数据是否存在即可。 但这里有一个比较重要的前提:非常庞大的数据。 常规实现 先不考虑这个条件,我们脑海中出现的第一种方案是什么? 我想大多数想到的都是用 HashMap 来存放数据,因为它的写入查询的效率都比较高。 写入和判断元素是否存在都有对应的 API,所以实现起. 阅读详情

相关推荐

昆仑通态屏幕制作(连载1)---接触篇

写在开始 背景:最近出了做QT、LORA和4G,还做了一个屏幕。以前做了一个迪文的串口屏幕,发现网上的教程很多,所以连载就不更新了,并且迪文屏幕的设置什么的比较简单,应该从网上都能找到对应的教程。而现在用的昆仑通态的屏幕属于嵌入式屏幕,至于为啥叫嵌入式,也不是很明白,反正就是需要用这个屏幕,嵌入式不嵌入式不重要了。重要的是能用,并且能实现串口屏幕不能实现的功能。所以选定昆仑通态的一款屏幕。 文章语言:我比较习惯用比较糙的语言介绍做的东西,有时候不太准备,但是为了方便理解,还请大家谅解。用...

weixin_45426095的博客 6736

如何判断一个元素在亿级数据中是否存在?布隆过滤器

如何判断一个元素在亿级数据中是否存在?常规实现哈希函数布隆过滤器介绍布隆过滤器原理布隆过滤器添加元素布隆过滤器查询元素 常规实现 1、数组 2、链表 3、树、平衡二叉树 4、Map(红黑树) 5、哈希表 上面的使用方式在结合常见的排序方式比如二分,可以快速的查找数据是否存在,但当集合数据元素非常大,比如1亿条,这个时候,数据结构问题就会凸显出来,数组,链表等 ,就会非常吃内存,内存的消耗会成指数级增长,最终达到瓶颈。对于哈希表,其实也是很占用内存,哈希表的常规做法是key值映射成一个8个字节的信息指纹,对于

bingshiwuyu的专栏 469

从编解码芯片看Tesla T4的隐藏实力:双NVDEC如何实现70路1080P解码?

本文深入剖析了英伟达Tesla T4显卡的隐藏实力,重点解读其独特的双NVDEC硬件解码器设计如何突破性能瓶颈。通过分析Turing架构、对比HEVC/H.264解码差异,并结合驱动与SDK优化,揭示了实现高达70路1080P视频流并发解码的原理与实战配置,为云游戏、直播转码等高并发场景提供高效解决方案。

weixin_29209801的博客 485

上千万或上亿数据(有重复),统计其中出现次数最多的钱N个数据

方案1:上千万或上亿的数据,现在的机器的内存应该能存下。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前N个出现次数最多的数据了,可以用第2题提到的堆机制完成。 ...

Java 852

价值投资/指标选股(akshare)

利用akshare的数据源,可以获取所有的股票数据以及公开的财务数据,可以进行条件筛选,获取满足条件的股票。 这套模板可以用于价值投资,指标选股。选股周期为日线级别。然后结果可以邮箱发送。 import time import akshare as ak ## A 股上市公司的实时行情数据 stock_zh_a_spot_df = ak.stock_zh_a_spot() #print(stock_zh_a_spot_df) ##取前300测试 ##取前300测试 df_stock =...

qq_26742269的博客 1万+

《UE4游戏开发》之 《音效系统的实现》

音效系统需要负责管理什么事情: 1.1 确定声音控制的分类:如背景音乐、技能音效、界面音效、角色配音等,不同游戏对声音有不同的分类 1.2 控制每个分类的声音的开启与关闭,以及调节每个分类声音的大小 解决如何分类: 2.1 首先需要知道ue4为声音分类提供了什么:SoundClass通常需要一个根分类【SC_RPG_Master】,便于总体控制音效,以及指定引擎默认启动的音效 打开SoundClass资产【SC_RPG_Master】,对根控制的设置,将逐级传递并按比例进行设置,以此达到控制一类声音的目的.

qq_21919621的博客 2126

ASP.NET MVC + EF 利用存储过程读取大数据,1亿数据测试很OK

 看到本文的标题,相信你会忍不住进来看看!   没错,本文要讲的就是这个重量级的东西,这个不仅仅支持单表查询,更能支持连接查询,   加入一个表10W数据,另一个表也是10万数据,当你用linq建立一个连接查询然后利用take,skip读取第N页数据的时候,   你的程序就挂了,因为,你很可能读取需要几十秒甚至几分钟以上。   下面来讲解一下,ASP.NET MVC + EF

代上老码的专栏 1809

Win10或Win11电脑找不到以太网网络适配器的解决方法

Win10或Win11电脑找不到以太网网络适配器的解决方法

Robot_PLC_HMI_Automation 655

asp.net怎么实现按条件查询_上亿数据怎么玩深度分页?兼容MySQL + ES + MongoDB

面试题 & 真实经历面试题:在数据量很大的情况下,怎么实现深度分页?大家在面试时,或者准备面试中可能会遇到上述的问题,大多的回答基本上是分库分表建索引,这是一种很标准的正确回答,但现实总是很骨感,所以面试官一般会追问你一句,<font color="red">现在工期不足,人员不足,该怎么实现深度分页?</font>这个时候没有实际经验的同学基本麻爪,So,请听我娓...

weixin_39639568的博客 225

【即插即用模块】Transformer篇 | MICCAI 2025 | ReLULA:Q/K双ReLU线性注意力,既轻又准!

本文介绍了一种基于线性注意力的多实例学习模块(Lin-MIL),适用于高分辨率视觉任务。该模块通过转置计算顺序和使用ReLU激活,将计算复杂度从O(n²d)降至O(nd²),同时提升特征判别性。核心创新包括Q/K双ReLU激活、可学习温度系数和数值稳定归一化。实验表明,该模块在轻量化视觉任务中优于传统线性注意力和Mamba架构。代码实现了模块化矩阵乘法,支持即插即用替换ViT自注意力层。该模块特别适合计算病理学等需要处理大量图像补丁的任务。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 140

Ascend开发包Acllite安装

设置环境变量,配置程序编译依赖的头文件,库文件路径。“$HOME/Ascend”请替换“Ascend-cann-toolkit”包的实际安装路径。将YOLOV7原始模型转换为适配昇腾310处理器的离线模型(*.om文件),放在model路径下。运行完成后,会在样例工程的out目录下生成推理后的图片,显示对比结果如下所示。可以使用以下两种方式下载,请选择其中一种进行源码准备。请从以下链接获取该样例的输入图片,放在data目录下。执行以下命令,执行编译脚本,开始样例编译。执行运行脚本,开始样例运行。

如果想成为中心,那么就到中心去吧。 318

如何在亿级数据中判断一个元素是否存在?

前言 在日常工作中,经常要判断一个元素是否在一个集合中。假设你要向浏览器添加一项功能,该功能可以通知用户输入的网址是否是恶意网址,此时你手上有大约 1000 万个恶意 URL 的数据集,你该如何实现该功能。按我之前的思维,要判断一个元素在不在当前的数据集中,首先想到的就是使用 hash table,通过哈希函数运行所有的恶意网址以获取其哈希值,然后创建出一个哈希表(数组)。这个方案有个明显的缺点,...

Meldoy_mgh的博客 606

FPGA入门200例(17):如何阅读RTL原理图(Schematic)?将Verilog代码与门级电路对应

本文针对FPGA开发新手常见误区,重点讲解了RTL原理图的重要性和阅读方法。文章指出,仅通过仿真验证代码会导致板级测试异常,而RTL原理图能直观呈现Verilog代码对应的硬件结构(门电路、寄存器、多路选择器等)。通过"识别基础元件→跟踪信号路径→验证逻辑关系"的三步法,新手可以建立代码与硬件的映射思维,排查冗余逻辑、意外锁存器等错误。文章还结合Vivado/Quartus工具操作指南和典型代码案例,帮助读者掌握从代码到硬件实现的转换过程,避免混淆RTL与门级原理图、忽略信号位宽等常见误

远程部署调试 运行安装 项目调试 二次开发 项目技术新 持续迭代 部分源码免费分享 200
上一篇: SpringBoot自动配置原理
下一篇: 负载均衡太太太太太太容易理解啦!
Java3y
Java3y 领域专家: 后端开发技术领域 领域专家: 后端开发技术领域
博客等级 码龄9年 1万+粉丝 509原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值