Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何判断一个元素在亿级数据中是否存在?

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

作者头像
五分钟学算法
发布于 2019-09-30 06:24:10
发布于 2019-09-30 06:24:10
1.9K00
代码可运行
举报
文章被收录于专栏:五分钟学算法五分钟学算法
运行总次数:0
代码可运行

作者 | crossoverJie

来源 | crossoverJie

前言

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

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

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

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

常规实现

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-Xms64m -Xmx64m -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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 数据试试:

执行后马上就内存溢出。

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

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

Bloom Filter

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

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

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

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

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

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

Bloom Filter 原理

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

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

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

如图所示:

  • 首先需要初始化一个二进制的数组,长度设为 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 实现了一个简单的雏形。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 ,则认为数据不存在。

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-Xms64m -Xmx64m -XX:+PrintHeapAtGC
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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));
    }

执行结果如下:

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


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

这也体现了 BloomFilter 的误报率。

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

Guava 实现

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
-Xms64m -Xmx64m -XX:+PrintHeapAtGC

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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 的数据,执行没有问题。

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

源码分析

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

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

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

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

put 写入函数

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

  • 根据 murmur3_128 方法的到一个 128 位长度的 byte[]
  • 分别取高低 8 位的到两个 hash 值。
  • 再根据初始化时的到的执行 hash 的次数进行 hash 运算。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);

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

重点是 bits.set() 方法。

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

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

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

  • set 之前先通过 get() 判断这个数据是否存在于集合中,如果已经存在则直接返回告知客户端写入失败。
  • 接下来就是通过位运算进行 位或赋值
  • get() 方法的计算逻辑和 set 类似,只要判断为 0 就直接返回存在该值。

mightContain 是否存在函数

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

总结

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

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

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

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

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

https://github.com/crossoverJie/JCSprout

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-09-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 五分钟学算法 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
如何判断一个元素在亿级数据中是否存在?
我想大多数想到的都是用 HashMap 来存放数据,因为它的写入查询的效率都比较高。
Java3y
2019/08/27
1.4K0
如何判断一个元素在亿级数据中是否存在?
布隆过滤器:原理与应用
这个时候,布隆过滤器(Bloom Filter)就派上了用场。 作为一种空间高效的概率型数据结构,布隆过滤器能够快速有效地检测一个元素是否属于一个集合。其应用广泛,从网络爬虫的网页去重,到数据库查询优化,乃至比特币网络的交易匹配,都离不开它的身影。
BookSea
2023/10/12
4890
布隆过滤器
  布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
OPice
2019/10/23
6610
布隆过滤器(亿级数据过滤算法)原理就是这么简单
我们以演进的方式来逐渐认识布隆过滤器。先抛出一个问题爬虫系统中URL是怎么判重的?你可能最先想到的是将URL放到一个set中,但是当数据很多的时候,放在set中是不现实的。
Java识堂
2020/04/22
1.4K0
布隆过滤器(亿级数据过滤算法)原理就是这么简单
BloomFilter怎么用?使用布隆过滤器来判断key是否存在?「建议收藏」
今天跟一个同事聊了一个问题,说最近在做推荐,如何判断用户是否看过这个片段呢?想了一下,正好可以使用布隆过滤器来完成这个需求。
全栈程序员站长
2022/11/16
1.3K0
BloomFilter怎么用?使用布隆过滤器来判断key是否存在?「建议收藏」
由散列表到BitMap的概念与应用(二)
在前一篇文章中我们介绍了散列表和BitMap的相关概念与部分应用。本文将会具体讲解BitMap的扩展:布隆过滤器(Bloom filter)。
aoho求索
2018/12/11
6420
由散列表到BitMap的概念与应用(二)
Reids(4)——神奇的HyperLoglog解决统计问题
上一次 我们学会了使用 HyperLogLog 来对大数据进行一个估算,它非常有价值,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是不是已经在 HyperLogLog 结构里面了,它就无能为力了,它只提供了 pfadd 和 pfcount 方法,没有提供类似于 contains 的这种方法。
我没有三颗心脏
2020/03/20
7550
Reids(4)——神奇的HyperLoglog解决统计问题
不了解布隆过滤器?一文给你整的明明白白!
海量数据处理以及缓存穿透这两个场景让我认识了布隆过滤器 ,我查阅了一些资料来了解它,但是很多现成资料并不满足我的需求,所以就决定自己总结一篇关于布隆过滤器的文章。希望通过这篇文章让更多人了解布隆过滤器,并且会实际去使用它!
乔戈里
2019/12/11
9830
不了解布隆过滤器?一文给你整的明明白白!
面试题,如何在千万级的数据中判断一个值是否存在?
当你看到这个标题的时候,你也许会想我可以使用hashmap之类的来存储值,然后get就是了。又或者把数据存在数据库里然后去判断就可以了。
ImportSource
2019/05/06
4.5K0
面试题,如何在千万级的数据中判断一个值是否存在?
场景题:假设有40亿QQ号,但只有1G内存,如何实现去重?
当数据量比较大时,使用常规的方式来判重就不行了。例如,使用 MySQL 数据库判重,或使用 List.contains() 或 Set.contains() 判重就不行了,因为数据量太大会导致内存放不下,或查询速度太慢等问题。
磊哥
2025/01/08
1710
场景题:假设有40亿QQ号,但只有1G内存,如何实现去重?
布隆过滤器 | 亿级数据处理原理与实战
布隆过滤器(英语:Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。主要用于判断一个元素是否在一个集合中。
Rude3Knife的公众号
2020/05/15
2.1K0
布隆过滤器:判断一定不存在或者可能存在的算法
布隆过滤器(BloomFilter)是由只存0或1的位数组和多个hash算法, 进行判断数据一定不存在或者可能存在的算法.
一个架构师
2022/06/20
1.3K0
布隆过滤器:判断一定不存在或者可能存在的算法
如何从10亿数据中快速判断是否存在某一个元素?今天总算知道了
当 Redis 用作缓存时,其目的就是为了减少数据库访问频率,降低数据库压力,但是假如我们某些数据并不存在于 Redis 当中,那么请求还是会直接到达数据库,而一旦在同一时间大量缓存失效或者一个不存在缓存的请求被恶意攻击访问,这些都会导致数据库压力骤增,这又该如何防止呢?
Java程序猿阿谷
2021/03/04
1.3K0
如何从10亿数据中快速判断是否存在某一个元素?今天总算知道了
如何快速判断某 URL 是否在 20 亿的网址 URL 集合中?
假设遇到这样一个问题:一个网站有 20 亿 url 存在一个黑名单中,这个黑名单要怎么存?若此时随便输入一个 url,你如何快速判断该 url 是否在这个黑名单中?并且需在给定内存空间(比如:500M)内快速判断出。
芋道源码
2019/08/21
2.1K0
详解布隆过滤器原理,及分布式运用方法_布隆过滤器最小误差
布隆过滤器是一个叫“布隆”的人提出的,本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure)。它本身是一个很长的二进制向量,特点是高效地插入和查询,可以用来确定 “某一条数据一定不存在或者可能存在一个集合中”。
全栈程序员站长
2022/11/08
1.4K0
详解布隆过滤器原理,及分布式运用方法_布隆过滤器最小误差
高频面试考点:解读布隆过滤器
布隆过滤器,起源于 20 世纪 70 年代,是一种高效的数据过滤算法。它基于二进制数组和多哈希函数的结合,以极低的空间占用和高效率著称。由于其底层采用位存储方式,使得存储成本大幅降低,例如,仅需 10KB 的空间便能存储高达百万个元素的数组。
写bug的高哈哈
2025/01/05
920
高频面试考点:解读布隆过滤器
20 亿的 URL 集合,如何快速判断其中一个?
假设遇到这样一个问题:一个网站有 20 亿 url 存在一个黑名单中,这个黑名单要怎么存?若此时随便输入一个 url,你如何快速判断该 url 是否在这个黑名单中?并且需在给定内存空间(比如:500M)内快速判断出。
Java技术栈
2019/11/05
1.3K0
使用Java实现布隆过滤器
布隆过滤器(Bloom Filter)是一种数据结构,可以快速、高效地判断一个元素是否存在于一个集合中,其特点是空间效率高且查询速度快。在日常的编程工作和项目开发中,布隆过滤器经常被用于缓存、防止缓存穿透等场景。
大盘鸡拌面
2024/03/02
5350
基于Guava布隆过滤器的海量字符串高效去重实践
使用Google Guava库来实现基于布隆过滤器的海量字符串去重是一个很好的选择。布隆过滤器是一种空间效率极高的概率型数据结构,它利用位数组表示集合,并使用哈希函数将元素映射到位数组的某些位置。布隆过滤器可以高效地检查一个元素是否可能属于某个集合,但有一定的误报率。
公众号:码到三十五
2024/03/19
2460
基于Guava布隆过滤器的海量字符串高效去重实践
Redis缓存雪崩、击穿、穿透解释及解决方法,缓存预热,布隆过滤器 ,互斥锁
大量缓存数据同一时间过期或者redis故障时,此时大量用户请求直接打到数据库,造成数据库宕机
javaNice
2023/11/19
3540
推荐阅读
相关推荐
如何判断一个元素在亿级数据中是否存在?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验