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

简介: 一个面试题目:现在有一个非常庞大的数据,假设全是 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 数据试试:



执行后马上就内存溢出。



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


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


Bloom Filter


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


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


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


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


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


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


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. 只要返回数据不存在,则肯定不存在。


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


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


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


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


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


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


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


基于以上的 Hash 冲突的前提,所以 Bloom Filter 有一定的误报率,这个误报率和 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);
    }
}


相关文章
|
6月前
|
存储 容器
List,linkeedlist集合介绍,特点,二者区别,增长因子,去重复
List,linkeedlist集合介绍,特点,二者区别,增长因子,去重复
|
7月前
终于掌握append为切片添加元素的诀窍 切片动态增长看这里
终于掌握append为切片添加元素的诀窍 切片动态增长看这里
55 1
|
10月前
|
前端开发 Java 数据库
数据重复插入问题及解决方案
数据重复插入问题及解决方案
534 0
|
10月前
重生之我是孔乙己——查找数组缺失元素的几种方法
重生之我是孔乙己——查找数组缺失元素的几种方法
47 0
|
存储
数组模拟队列之深度解析
数组模拟队列之深度解析
78 0
数组模拟队列之深度解析
|
数据采集 缓存 算法
如何判断一个元素在亿级数据中是否存在?(下)
一个面试题目: 现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。 需求其实很清晰,只是要判断一个数据是否存在即可。 但这里有一个比较重要的前提:非常庞大的数据。
|
算法 前端开发
【前端算法】独一无二的出现次数,统计次数加去重
给你一个整数数组 arr,请你帮忙统计数组中每个数的出现次数。 如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false。
87 0
|
存储 安全 Java
记一次集合去重导致的线上问题
前言在工作中一次排查慢接口时,查到了一个函数耗时较长,最终定位到是通过 List 去重导致的。由于测试环境还有线上早期数据较少,这个接口的性能问题没有引起较大关注,后面频繁超时,才引起重视。之前看《阿里巴巴Java开发手册》里面有这样一段描述:你看,阿里前辈们都免费总结了,不过还是会看到有人会用List的contains函数来去重......不记得的,罚抄10万遍如果需要这本书资源的网上下载也行,私聊我发你也行今天我就结合源码聊聊Set是怎样保证数据的唯一性的,为什么两种去重方式性能差距这么大HashSet源码底层实现基于 HashMap,所以迭代时不能保证按照插入顺序,或者其它顺序进行迭代
阿里字节技术大佬都在用的List集合去重方案!
常规去重 碰到List去重的问题,除了遍历去重,我们常常想到利用Set集合不允许重复元素的特点,通过List和Set互转,来去掉重复元素。
215 0
阿里字节技术大佬都在用的List集合去重方案!
|
Java 数据库 索引
解决方案:如何防止数据重复插入?
为啥要解决数据重复插入? 解决方案实战 可落地小总结 一、为啥要解决数据重复插入? 问题起源,微信小程序抽风 wx.request() 重复请求服务器提交数据。
2402 0