大数据去重-布隆过滤器

761 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

哈希表

对于数据去重存在很多算法,首先想到的就是哈希表。通过散列函数将可能出现的数据通过哈希值映射到表中的一个位置,通过哈希值可以加快查找速度。

一个典型的应用就是HashMap

存在以下特点

  • 如果哈希值不等,那么对应的数据也是不等的
  • 如果哈希值相等,那么对应的元素不一定不相等(存在碰撞的可能性)

hash表去重

利用hashMap快速查找的特性,我们可以利用其进行去重操作

/**
 * 对数组去重
 *
 * @param source
 */
public static void withoutRepeat(int[] source) {
    final int[] temp = new int[source.length];
    final Map<Integer, Boolean> map = new HashMap<>();
    int index = 0;
    for (int element : source) {
        if (!map.containsKey(element)) {
            map.put(element, true);
            temp[index++] = element;
        }
    }
    System.arraycopy(temp, 0, source, 0, temp.length);
}

缺点

对于大数据去重,哈希表就不再适用。

  • 哈希表不仅仅只能做标记一个数据是否出现过,可以存储一个数据的附属状态,比如计算一个数据出现的次数,那么使用哈希表来去重有点大材小用了
  • 哈希表的空间利用率只用50%,和其加载因子有关,之所以叫散列表是应为数据不连续,且哈希表也不是存满数据的,当然如此设计也有好处,就是减少碰撞,减少碰撞也是哈希表查询快的原因。

bitset

称为位集,是一种空间利用率很高的数据结构。而且所做的事情很存粹只是用于记录某个值是否出现过,通过1、0来判断。

原理

BitSet使用一个long[]数组来存储元素状态,内部频繁使用位运算来确定元素对应数组下标,再通过或运算来修改该元素对应状态为1,一个long可以存储64个元素状态。极致的压缩了空间。但也存在缺点如果说元素值分布不均匀值与值之间存在较大的跨度,就会导致空间的利用率没有想象的那么大。

image-20220709011717422.png

去重

public static int[] withoutRepeat(int[] source) {
    final BitSet bitSet = new BitSet();
    for (Integer element : source) {
        bitSet.set(element);
    }
    final int[] target = bitSet.stream().toArray();
    System.arraycopy(target, 0, source, 0, target.length);
    return target;
}

优缺点

优点:

bitset的空间复杂度不随原集合内个数的增加而增加

缺点

bitset的空间复杂度随原集合内最大元素大小增加而增加

什么意思呢? 就是如果待去重的数据,数据量不大但是数据跨度很大比如最小值100,最大值10万甚至更大,那么bitset就不是很适合。对于数据跨度大的问题,可以使用与运算解决,比如说我想让他控制在 2^10^内,那么就可以将数据与2^10^-1按位与。


布隆过滤器

对于一个对象判断是否重复使用BitSet的话,一般会使用BitSet存储对象对应的哈希值,但是就会有一个问题:如果说这个对象对应的哈希算法很糟糕,出现碰撞的概率很高的话,那么BitSet出现误判的几率就很高。

应运而生的布隆过滤器(Bloom Filter)就出现了,其核心思想就是使用多个哈希函数来降低误判概率

实现

  • 哈希算法使用加权哈希
  • 默认八次哈希,减少误判率
  • bitset位数固定2^24,避免数据过大,降低空间利用率
public class BloomFilter {

    /**
     * bitSet分配2^24位
     */
    private static final int DEFAULT_SIZE = 1 << 25;

    /**
     * 哈希种子,循环加权次数
     */
    private static final int[] SEEDS = new int[]{3, 5, 7, 11, 13, 31, 37, 61};
    /**
     * 位集,给定初始化范围,避免频繁扩容
     */
    private final BitSet bits = new BitSet(DEFAULT_SIZE);

    /**
     * 多次哈希的哈希函数
     */
    private final SimpleHash[] func = new SimpleHash[SEEDS.length];

    /**
     * 布隆过滤器,初始化哈希函数
     */
    public BloomFilter() {
        for (int i = 0; i < SEEDS.length; i++) {
            func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
        }
    }

    /**
     * 将字符串对应多次哈希结果记录到bitSet中
     */
    public void add(String value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    /**
     * 判断是否重复
     */
    public boolean contains(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (SimpleHash f : func) {
            ret = ret && bits.get(f.hash(value));
            //存在一次为0直接返回
            if (!ret) {
                break;
            }
        }
        return ret;
    }

    /**
     * 哈希函数类
     */
    @Data
    @AllArgsConstructor
    public static class SimpleHash {
        private final int cap;
        private final int seed;

        //采用简单的加权和hash
        public int hash(String value) {
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                result = seed * result + value.charAt(i);
            }
            //防止哈希过大
            return (cap - 1) & result;
        }
    }
}

使用

对1000万个url进行去重

生成10万个url

99的倍数则写两次,模拟重复数据

/**
 * 生成1000万个url
 */
@Test
public void test() {
    String filePath = "/Users/rolyfish/Desktop/MyFoot/testfile";
    String fileName = "urls.txt";
    int urlNum = 10000000;
    String baseUrl = "https://www.baidu/baidu/article/details/";
    try (FileWriter fileWriter = new FileWriter(new File(filePath, fileName), true)) {
        //生成100000个url
        for (int i = 0; i < urlNum; i++) {
            //写两次
            if (i % 99 == 0) {
                fileWriter.write(baseUrl + (urlNum + i));
                fileWriter.write(System.lineSeparator());
                fileWriter.flush();
            }
            fileWriter.write(baseUrl + (urlNum + i));
            fileWriter.write(System.lineSeparator());
            fileWriter.flush();
        }
        fileWriter.flush();
        fileWriter.close();
    } catch (IOException e) {
        System.out.println();
    }
}

这里是存在重复数据的,而且文件还是挺大的。 image-20220709043157614.png

image-20220709043225418.png

测试去重

读取url文件,一行一行读,判断是否重复,不重复写入新的文件中。

/**
 * 使用此布隆过滤器对10万个url去重
 */
public static void main(String[] args) {
    final long start = Calendar.getInstance().getTime().getTime();
    final BloomFilter bloomFilter = new BloomFilter();
    String filePath = "/Users/rolyfish/Desktop/MyFoot/testfile";
    String fileName = "urls.txt";
    String fileNameNew = "urlsNew.txt";
    final File fileUrl = new File(filePath, fileName);
    final File fileUtlNew = new File(filePath, fileNameNew);
    if (!fileUrl.exists()) {
        System.out.println("文件不存在");
    }
    String buffer = "";
    try (
            final FileReader fileReader = new FileReader(fileUrl);
            final BufferedReader bufferedReader = new BufferedReader(fileReader);
            final FileWriter fileWriter = new FileWriter(fileUtlNew)) {
        while ((buffer = bufferedReader.readLine()) != null) {
            if (!bloomFilter.contains(buffer)){
                fileWriter.write(buffer);
                fileWriter.write(System.lineSeparator());
                fileWriter.flush();
            }
            //添加到布隆过滤器
            bloomFilter.add(buffer);
        }
        fileWriter.close();
        fileReader.close();
    } catch (IOException e) {
        System.out.println("io异常");
    }
  	final long end = Calendar.getInstance().getTime().getTime();
  	System.out.println(end - start);
}

实现了去重,且速度是可观的。

image-20220709043424006.png

image-20220709043435754.png

image-20220709043713291.png

Google提供的布隆过滤器

Google为我们提供的布隆过滤器,位于com.google.common.hash包下。原理相似。

依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

使用:

public static void main(String[] args) {
    /**
     * 参数说明:
     *  - 参数漏斗
     *  - 预期插入多少次
     *  - 预期误判率
     */
    BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100, 0.01);

    /**
     * 将元素放入布隆过滤器
     * - 位集发生改变则一定是首次添加返回true
     * - 位集未发生改变,可能不是首次添加(存在误判),返回false
     */
    filter.put("123");

    /**
     * 判断元素是否存在于位集
     * - false 一定不存在
     * - true  可能存在(存在误判)
     */
    filter.test("123");
}