一、布隆过滤器介绍

1、布隆过滤器的起源,用途

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

2、布隆过滤器的概念

如果想要判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。

Hash面临的问题就是冲突。假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳m / 100个元素。显然这就不叫空间效率了(Space-efficient)了。解决方法也简单,就是使用多个Hash,如果它们有一个说元素不在集合中,那肯定就不在。如果它们都说在,虽然也有一定可能性它们在说谎,不过直觉上判断这种事情的概率是比较低的。

3、布隆过滤器的优缺点

1、优点

相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数。另外, Hash函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。

布隆过滤器可以表示全集,其它任何数据结构都不能。

2、缺点

但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。常见的补救办法是建立一个小的白名单,存储那些可能被误判的元素。但是如果元素数量太少,则使用散列表足矣。

另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。

在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。

4、应用场景

网页URL 去重
垃圾邮件识别
黑名单
查询加速【比如基于KV结构的数据】
集合元素重复的判断
缓存穿透

5、布隆过滤器的工作原理

布隆过滤器本身是一个很长的二进制向量,既然是二进制的向量,那么显而易见的,存放的不是0,就是1。

新建一个16位的布隆过滤器,如图

布隆过滤器_数组

有一个对象,我们通过

  • 方式一计算他的hash值,得到hash = 2
  • 方式二计算他的hash值,得到hash = 9
  • 方式三计算他的hash值,得到hash = 5

通过三个方法计算得到三个数值,我们把这三个数值对应的布隆过滤器向量值改为1,表明该位置有值。

第二个对象,加入得到值1 6 3,就把1 6 3 改为1

对于布隆过滤器本身来说,并没有存储任何数据,只是计算该数据的位置,然后存储向量值

那么,如果需要判断某个数据是否存在于布隆过滤器,就只需要判断计算出来的所有向量值是否都为1即可

但是:

当存储的数据向量不断增多,就可能会出现,2 9 5 向量值都为1,但是实际上没有这个数据的情况,这样就导致了,布隆过滤器只能判断某个数据一定不存在,但是不能保证某个数据一定存在。

另外,因为一个向量位置可能被多个对象映射,所以,布隆过滤器无法删除数据

6、布隆过滤器的设计

布隆过滤器思路比较简单,但是对于布隆过滤器的随机映射函数设计,需要计算几次,向量长度设置为多少比较合适,这个才是需要认真讨论的。

如果向量长度太短,会导致误判率直线上升。

如果向量太长,会浪费大量内存。

如果计算次数过多,会占用计算资源,且很容易很快就把过滤器填满。


二、Guava布隆过滤器的使用

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>32.1.3-jre</version>
        </dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
//tddBloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 10000000, 0.01);
        tddBloomFilter = BloomFilter.create(Funnels.longFunnel(), 10000000, 0.01);
        toBloomFilter = BloomFilter.create(Funnels.longFunnel(), 10000000, 0.01);

        tddBloomFilter.put(1L);
        tddBloomFilter.put(2L);
        tddBloomFilter.put(3L);
        toBloomFilter.put(1L);
        toBloomFilter.put(2L);
        toBloomFilter.put(3L);
        
        tddBloomFilter.mightContain(tddId);
        tddBloomFilter.mightContain(1);
        toBloomFilter.mightContain(toId);
        toBloomFilter.mightContain(1);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

完整代码

package com.cqsym.nbigscreen.bloomfilter;

import com.cqsym.nbigscreen.config.DataSourceRepository;
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class NbigscreenBloom {
    private static final Logger log = LoggerFactory.getLogger(NbigscreenBloom.class);
    private static final int BATCH_SIZE = 10000; // 每次查询的数据量
    private static final int BloomFilter_init_size = 1000000; // 初始化布隆过滤数据量,100万。
    private static final int BloomFilter_size = BloomFilter_init_size * 2; // 布隆过滤数据量大小,200万。
    public static BloomFilter tddBloomFilter;
    public static BloomFilter toBloomFilter;
    @Autowired
    @Qualifier(value="secondDataSourceRepository")
    private DataSourceRepository secondDataSourceRepository;



    public void initForDay() {
        //tddBloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 10000000, 0.01);
        tddBloomFilter = BloomFilter.create(Funnels.longFunnel(), BloomFilter_size, 0.01);
        toBloomFilter = BloomFilter.create(Funnels.longFunnel(), BloomFilter_size, 0.01);

        try {
            log.info("initForDay ---------- 睡5秒 开始 ... ");
            TimeUnit.SECONDS.sleep(5);
            log.info("initForDay ---------- 睡5秒 完成。 ");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        log.info("initForDay ---------- start initForTdd() ... ");
        new Thread(() -> {
            log.info("initForDay1 ---------- start initForTdd() ... ");
            initForTdd();
            log.info("initForDay1 ---------- end initForTdd(). ");
        },"initForTdd线程").start();
        log.info("initForDay ---------- end initForTdd(). ");

        log.info("initForDay ---------- start initForTo() ... ");
        new Thread(() -> {
            log.info("initForDay1 ---------- start initForTo() ... ");
            initForTo();
            log.info("initForDay1 ---------- end initForTo(). ");
        },"initForTo线程").start();
        log.info("initForDay ---------- end initForTo(). ");

    }

    public void initForTdd() {
        final String sql0 = "select count(1) from t_dispatch_detail ";
        final String sql = "select tdd_id from t_dispatch_detail limit ? , ? ";
        //int offset = 0;
        //int offset = 20000000;
        int offset = secondDataSourceRepository.queryForListSingleColumn(sql0, Integer.class).get(0)-BloomFilter_init_size;
        log.info("initForTdd ---------- First offset: " + offset);
        List<Long> rows;
        do {
            log.info("initForTdd ---------- offset: " + offset);
            rows = secondDataSourceRepository.queryForListSingleColumn(sql, Long.class, offset, BATCH_SIZE);
            for (Long row : rows) {
                tddBloomFilter.put(row);
            }
            offset += BATCH_SIZE;
        } while (rows.size() == BATCH_SIZE);
        log.info("initForTdd ---------- end");
    }

    public void initForTo() {
        final String sql0 = "select count(1) from t_order ";
        final String sql = "select to_id from t_order limit ? , ? ";
        //int offset = 0;
        //int offset = 12000000;
        int offset = secondDataSourceRepository.queryForListSingleColumn(sql0, Integer.class).get(0)-BloomFilter_init_size;
        log.info("initForTo ---------- First offset: " + offset);
        List<Long> rows;
        do {
            log.info("initForTo ---------- offset: " + offset);
            rows = secondDataSourceRepository.queryForListSingleColumn(sql, Long.class, offset, BATCH_SIZE);
            for (Long row : rows) {
                toBloomFilter.put(row);
            }
            offset += BATCH_SIZE;
        } while (rows.size() == BATCH_SIZE);
        log.info("initForTo ---------- end");
    }



}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
package com.fandf.test.redis;

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

/**
 * Guava
 */
public class GuavaBloomFilter {

    public static void main(String[] args) {
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000,0.01);
        bloomFilter.put("好好学技术");
        System.out.println(bloomFilter.mightContain("不好好学技术"));
        System.out.println(bloomFilter.mightContain("好好学技术"));
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.


三、Redis布隆过滤器的使用

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.20.0</version>
</dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
package com.fandf.test.redis;

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

/**
 * Redisson 实现布隆过滤器
 */
public class RedissonBloomFilter {

    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        //构造Redisson
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("name");
        //初始化布隆过滤器:预计元素为100000000L,误差率为1%
        bloomFilter.tryInit(100000000L,0.01);
        bloomFilter.add("好好学技术");

        System.out.println(bloomFilter.contains("不好好学技术"));
        System.out.println(bloomFilter.contains("好好学技术"));
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.


四、hutool布隆过滤器的使用

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.3</version>
</dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
package com.fandf.test.redis;

import cn.hutool.bloomfilter.BitMapBloomFilter;
import cn.hutool.bloomfilter.BloomFilterUtil;

/**
 * hutool
 */
public class HutoolBloomFilter {
    public static void main(String[] args) {
        BitMapBloomFilter bloomFilter = BloomFilterUtil.createBitMap(1000);
        bloomFilter.add("好好学技术");
        System.out.println(bloomFilter.contains("不好好学技术"));
        System.out.println(bloomFilter.contains("好好学技术"));
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.


五、java自定义实现布隆过滤器

package com.fandf.test.redis;

import java.util.BitSet;

/**
 * java布隆过滤器
 */
public class MyBloomFilter {

    /**
     * 位数组大小
     */
    private static final int DEFAULT_SIZE = 2 << 24;

    /**
     * 通过这个数组创建多个Hash函数
     */
    private static final int[] SEEDS = new int[]{4, 8, 16, 32, 64, 128, 256};

    /**
     * 初始化位数组,数组中的元素只能是 0 或者 1
     */
    private final BitSet bits = new BitSet(DEFAULT_SIZE);

    /**
     * Hash函数数组
     */
    private final MyHash[] myHashes = new MyHash[SEEDS.length];

    /**
     * 初始化多个包含 Hash 函数的类数组,每个类中的 Hash 函数都不一样
     */
    public MyBloomFilter() {
        // 初始化多个不同的 Hash 函数
        for (int i = 0; i < SEEDS.length; i++) {
            myHashes[i] = new MyHash(DEFAULT_SIZE, SEEDS[i]);
        }
    }

    /**
     * 添加元素到位数组
     */
    public void add(Object value) {
        for (MyHash myHash : myHashes) {
            bits.set(myHash.hash(value), true);
        }
    }

    /**
     * 判断指定元素是否存在于位数组
     */
    public boolean contains(Object value) {
        boolean result = true;
        for (MyHash myHash : myHashes) {
            result = result && bits.get(myHash.hash(value));
        }
        return result;
    }

    /**
     * 自定义 Hash 函数
     */
    private class MyHash {
        private int cap;
        private int seed;

        MyHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        /**
         * 计算 Hash 值
         */
        int hash(Object obj) {
            return (obj == null) ? 0 : Math.abs(seed * (cap - 1) & (obj.hashCode() ^ (obj.hashCode() >>> 16)));
        }
    }

    public static void main(String[] args) {
        String str = "好好学技术";
        MyBloomFilter myBloomFilter = new MyBloomFilter();
        System.out.println("str是否存在:" + myBloomFilter.contains(str));
        myBloomFilter.add(str);
        System.out.println("str是否存在:" + myBloomFilter.contains(str));
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
import java.util.BitSet;

public class MyBloomFilter {

    // 默认大小
    private static final int DEFAULT_SIZE = Integer.MAX_VALUE;

    // 最小的大小
    private static final int MIN_SIZE = 1000;

    // 大小为默认大小
    private int SIZE = DEFAULT_SIZE;

    // hash函数的种子因子
    private static final int[] HASH_SEEDS = new int[]{3, 5, 7, 11, 13, 17, 19, 23, 29, 31};

    // 位数组,0/1,表示特征
    private BitSet bitSet = null;

    // hash函数
    private HashFunction[] hashFunctions = new HashFunction[HASH_SEEDS.length];

    // 无参数初始化
    public MyBloomFilter() {
        // 按照默认大小
        init();
    }

    // 带参数初始化
    public MyBloomFilter(int size) {
        // 大小初始化小于最小的大小
        if (size >= MIN_SIZE) {
            SIZE = size;
        }
        init();
    }

    private void init() {
        // 初始化位大小
        bitSet = new BitSet(SIZE);
        // 初始化hash函数
        for (int i = 0; i < HASH_SEEDS.length; i++) {
            hashFunctions[i] = new HashFunction(SIZE, HASH_SEEDS[i]);
        }
    }

    // 添加元素,相当于把元素的特征添加到位数组
    public void add(Object value) {
        for (HashFunction f : hashFunctions) {
            // 将hash计算出来的位置为true
            bitSet.set(f.hash(value), true);
        }
    }

    // 判断元素的特征是否存在于位数组
    public boolean contains(Object value) {
        boolean result = true;
        for (HashFunction f : hashFunctions) {
            result = result && bitSet.get(f.hash(value));
            // hash函数只要有一个计算出为false,则直接返回
            if (!result) {
                return result;
            }
        }
        return result;
    }

    // hash函数
    public static class HashFunction {
        // 位数组大小
        private int size;
        // hash种子
        private int seed;

        public HashFunction(int size, int seed) {
            this.size = size;
            this.seed = seed;
        }

        // hash函数
        public int hash(Object value) {
            if (value == null) {
                return 0;
            } else {
                // hash值
                int hash1 = value.hashCode();
                // 高位的hash值
                int hash2 = hash1 >>> 16;
                // 合并hash值(相当于把高低位的特征结合)
                int combine = hash1 ^ hash1;
                // 相乘再取余
                return Math.abs(combine * seed) % size;
            }
        }

    }

    public static void main(String[] args) {
        Integer num1 = new Integer(12321);
        Integer num2 = new Integer(12345);
        MyBloomFilter myBloomFilter =new MyBloomFilter();
        System.out.println(myBloomFilter.contains(num1));
        System.out.println(myBloomFilter.contains(num2));

        myBloomFilter.add(num1);
        myBloomFilter.add(num2);

        System.out.println(myBloomFilter.contains(num1));
        System.out.println(myBloomFilter.contains(num2));

    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
package Bloomfilter;
 
import java.io.*;
import java.util.BitSet;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * Created by Intellij IDEA.
 * User:   LYX
 * Date:  2023/6/24
 */
public class BloomFilter {
        /**
         * 位数组,用于存储布隆过滤器的状态
         */
        private BitSet bitSet;
        /**
         * 位数组的长度
         */
        private int bitSetSize;
        /**
         * 预期元素数量
         */
        private int expectedNumberOfElements;
        /**
         * 哈希函数数量
         */
        private int numberOfHashFunctions;
        /**
         *  用于生成哈希种子的伪随机数生成器
         */
        private Random random = new Random();
 
        public BloomFilter(int bitSetSize, int expectedNumberOfElements) {
                this.bitSetSize = bitSetSize;
                this.expectedNumberOfElements = expectedNumberOfElements;
 
                // 根据公式计算哈希函数数量
                this.numberOfHashFunctions = (int) Math.round((bitSetSize / expectedNumberOfElements) * Math.log(2.0));
 
                // 创建位数组并初始化所有位为0
                this.bitSet = new BitSet(bitSetSize);
                }
 
        public void add(Object element) {
                // 对元素进行多次哈希,并将对应的位设置为1
                for (int i = 0; i < numberOfHashFunctions; i++) {
                long hash = computeHash(element.toString(), i);
                int index = getIndex(hash);
                bitSet.set(index, true);
                }
                }
 
        public boolean contains(Object element) {
                // 对元素进行多次哈希,并检查所有哈希值所对应的位是否都被设置为1
                for (int i = 0; i < numberOfHashFunctions; i++) {
                long hash = computeHash(element.toString(), i);
                int index = getIndex(hash);
 
                if (!bitSet.get(index)) {
                return false;
                }
                }
 
                return true;
                }
 
        private int getIndex(long hash) {
                // 将哈希值映射到位数组的下标(需要确保下标非负)
                return Math.abs((int) (hash % bitSetSize));
                }
 
        private long computeHash(String element, int seed) {
                // 使用伪随机数生成器生成不同的哈希种子
                random.setSeed(seed);
 
                // 将元素转换为字节数组,并计算其哈希值
                byte[] data = element.getBytes();
                long hash = 0x7f52bed27117b5efL;
 
                for (byte b : data) {
                hash ^= random.nextInt();
                hash *= 0xcbf29ce484222325L;
                hash ^= b;
                }
 
                return hash;
                }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.

三种手写的代码,自己手写可以借鉴。