数据结构之布隆过滤器

布隆过滤器

如果要经常判断某个元素是否存在,你会怎么做?很容易想到使用哈希表(HashSet、HashMap),将元素作为key去查找。时间复杂度为O(1),但是空间利用率不高,需要占用比较多的内存资源。

如果需要编写一个网络爬虫去爬10亿个网站数据,为了避免爬到重复的网站,如何判断某个网站是否爬过?很显然,HashSet、HashMap并不是非常好的选择。

是否存在时间复杂度低、占用内存较少的方案?就是下面要学习的布隆过滤器。

布隆过滤器(Bloom Filter)

布隆过滤器(Bloom Filter)在1970年由布隆提出,它是一个空间效率高的概率型数据结构,可以用来告诉你:一个元素一定不存在或者可能存在。

它实质上是由一个很长的二进制向量和一系列随机映射函数(Hash函数)组成。

优点:空间效率和查询时间都远远超过一般的算法。

缺点:有一定的误判率、删除困难。

应用场景:网页黑名单系统、垃圾邮件过滤系统、爬虫的网址判重系统、解决缓存穿透问题。

开源实现:google的guava中com.google.common.hash.BloomFilter。

布隆过滤器的原理

假设布隆过滤器由20位二进制、3个哈希函数组成,每个元素经过哈希函数处理都能生成一个索引位置。

  • 添加元素(put):将每一个哈希函数生成的索引位置都设为1
  • 查询元素是否存在(contains):如果有一个哈希函数生成的索引位置不为1,就代表不存在(100%准确),如果每一个哈希函数生成的索引位置都为1,就代表可能存在(存在一定的误判率)。

在这里插入图片描述

扫描二维码关注公众号,回复: 11449123 查看本文章

添加、查询的时间复杂度都是:O(k),空间复杂度是:O(m)(其中k是哈希函数的个数,m是二进制位的个数)。

布隆过滤器的误判率

误判率p受3个因素影响:二进制位的个数m、哈希函数的个数k、数据规模n。

已知误判率p、数据规模n,求二进制位的个数m、哈希函数的个数k,可以使用下面的公式:

在这里插入图片描述

布隆过滤器的实现

数据结构

    private long[] bits; // 二进制位

    private int bitSize; // 二进制位数

    private int hashSize; // 哈希函数的个数

    /**
     * @param n 数据规模
     * @param p 误判率
     */
    public BloomFilter(int n, double p) {
        if (n <= 0 || p <= 0 || p >= 1) {
            throw new IllegalArgumentException("wrong n or p");
        }

        double ln2 = Math.log(2); // ln2

        bitSize = (int) (- (n * Math.log(p)) / ln2 / ln2); // 二进制位的个数

        // (bitSize + Long.SIZE - 1) / Long.SIZE = ceil(bitSize / Long.SIZE)
        bits = new long[(bitSize + Long.SIZE - 1) / Long.SIZE]; // 初始化二进制位

        hashSize = (int) (bitSize * ln2 / n); // 哈希函数的个数
    }

put

    /**
     * 添加一个value
     *
     * @param value
     * @return
     */
    public boolean put(T value) {
        nullCheck(value);

        int hash1 = value.hashCode();
        int hash2 = value.hashCode() >> 16;

        boolean result = false;
        for (int i = 1; i <= hashSize; i++) {
            int combinedHash = hash1 + (i * hash2);
            if (combinedHash < 0) {
                combinedHash = ~combinedHash;
            }

            int index = combinedHash % bitSize;
            if (set(index)) {
                result = true;
            }
        }

        return result;
    }

    public boolean set(int index) {

        int bitsIndex = index / Long.SIZE; // 定位到bits数组中long元素的索引

        long value = bits[bitsIndex]; // 定位到long元素

        int mask = 1 << (index % Long.SIZE);

        long bit = value & mask; // 取出第[index % Long.SIZE]位

        bits[bitsIndex] = value | mask; // 将第[index % Long.SIZE]位设置为1

        return bit == 0;
    }

    private void nullCheck(T value) {
        if (value == null) {
            throw new IllegalArgumentException("Value must not be null.");
        }
    }

contains

    /**
     * 判断value是否存在,如果返回false,表示一定不存在,如果返回true,表示可能存在
     *
     * @param value
     * @return
     */
    public boolean contains(T value) {
        nullCheck(value);

        int hash1 = value.hashCode();
        int hash2 = value.hashCode() >> 16;

        for (int i = 0; i < hashSize; i++) {
            int combinedHash = hash1 + (i * hash2);
            if (combinedHash < 0) {
                combinedHash = ~combinedHash;
            }

            int index = combinedHash % bitSize;
            if (!get(index)) {
                // 只要有一个为0,则返回false
                return false;
            }
        }

        return true;
    }

    private boolean get(int index) {
        long value = bits[index / Long.SIZE]; // 定位到long元素
        int mask = 1 << (index % Long.SIZE);
        long bit = value & mask; // 取出第[index % Long.SIZE]位
        return bit != 0;
    }

更多精彩内容关注本人公众号:架构师升级之路
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/u022812849/article/details/107335046