写点什么

Java 高效找出两个大数据量 List 集合中的不同元素

作者:共饮一杯无
  • 2022-11-09
    浙江
  • 本文字数:5513 字

    阅读完需:约 18 分钟

Java高效找出两个大数据量List集合中的不同元素

本文将带你了解如何快速的找出两个相似度非常高的 List 集合里的不同元素。主要通过 Java API、List 集合双层遍历比较不同、借助 Map 集合查找三种方式,以及他们之间的执行效率情况,话不多说,开搞!集合初始化方法:


    /**     * 制造任意个元素的的List集合     * @param size List集合的size     * @return List<String>     */    private static List<String> dataList(int size) {        List<String> dataList = new ArrayList<>();        for (int i = 0; i < size; i++) {            dataList.add("" + i);        }        return dataList;    }
复制代码


测试数据为集合 A: 1 千, 1 万, 10 万,1 百万, 1 千万的数据量.


  • 集合 B 比集合 A 多初始化六条数据,集合 A 添加一条特有的数据。

  • 测试数据使用空字符串 + 自然数的方式。

JavaAPI 过滤(不推荐)

1 千数据量

        List<String> listA = dataList(1000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(1006);        Long startTime = System.currentTimeMillis();        // 复制集合A和集合B作为备份        List<String> listABak = new ArrayList<>(listA);        List<String> listBBak = new ArrayList<>(listB);        // 集合B存在,集合A不存在的元素        listB.removeAll(listA);        // 集合A存在,集合B不存在的元素        listA.removeAll(listBBak);        Long endTime = System.currentTimeMillis();        List<String> differentList = new ArrayList<>();        differentList.addAll(listB);        differentList.addAll(listA);        System.out.println("集合A和集合B不同的元素:"+differentList);        Long costTime = endTime-startTime;        System.out.println("比对耗时:"+costTime+"毫秒。");
复制代码


耗时:22 毫秒


1 万数据量

        List<String> listA = dataList(10000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(10006);        Long startTime = System.currentTimeMillis();        // 复制集合A和集合B作为备份        List<String> listABak = new ArrayList<>(listA);        List<String> listBBak = new ArrayList<>(listB);        // 集合B存在,集合A不存在的元素        listB.removeAll(listA);        // 集合A存在,集合B不存在的元素        listA.removeAll(listBBak);        Long endTime = System.currentTimeMillis();        List<String> differentList = new ArrayList<>();        differentList.addAll(listB);        differentList.addAll(listA);        System.out.println("集合A和集合B不同的元素:"+differentList);        Long costTime = endTime-startTime;        System.out.println("比对耗时:"+costTime+"毫秒。");
复制代码


耗时:613 毫秒


10 万数据量

        List<String> listA = dataList(100000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(100006);        Long startTime = System.currentTimeMillis();        // 复制集合A和集合B作为备份        List<String> listABak = new ArrayList<>(listA);        List<String> listBBak = new ArrayList<>(listB);        // 集合B存在,集合A不存在的元素        listB.removeAll(listA);        // 集合A存在,集合B不存在的元素        listA.removeAll(listBBak);        Long endTime = System.currentTimeMillis();        List<String> differentList = new ArrayList<>();        differentList.addAll(listB);        differentList.addAll(listA);        System.out.println("集合A和集合B不同的元素:"+differentList);        Long costTime = endTime-startTime;        System.out.println("比对耗时:"+costTime+"毫秒。");
复制代码



可以看出来十万数据量级别已经比较慢了,需要 77 秒

100 万数据量

emmm 估计挺慢,不继续验证了。😂😂😂


为什么在数据量增大的时候,这种方法性能下降的这么明显?我们不妨来看一下 removeAll 的源码:


    public boolean removeAll(Collection<?> c) {        // 先判空,然后执行批量remove        Objects.requireNonNull(c);        return batchRemove(c, false);    }
复制代码



通过源码我们可以看到,该方法是使用 for 循环对集合进行遍历第一层循环需要执行 listA.size()次,里面调用了 contains 方法来确定集合 B 是否含有该元素,再看 contains 方法的源码:



可以看到,indexOf 方法里又进行了一层遍历.平均每次遍历要进行 list.size() / 2 次计算,假设集合 A 的元素个数为 m,集合 B 的元素个数为 n 我们可以得到结论,运算次数为 m *(n/2)对于 100W 数据量来说,假设你的计算机每秒能够执行 1 千万次运算,也需要 13.8 个小时才能对比出来。所以大数据量不建议通过此方法


List 集合双层遍历比较不同(不推荐)

该方法实际上就是将 removeAll 的实现逻辑用自己的方式写出来,所以执行效率,运行结果和上一种方法没什么区别,这里只贴代码出来,不再赘述。


private static void doubleFor() {        List<String> listA = dataList(1000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(1006);        System.out.println("数量级为 " + listA.size() + "集合的不同元素为");        List<String> differentList = new ArrayList<>();        long startTime = System.currentTimeMillis();        for (String str : listB) {            if (!listA.contains(str)) {                differentList.add(str);            }        }        for (String str : listA) {            if (!listB.contains(str)) {                differentList.add(str);            }        }        long endTime = System.currentTimeMillis();        System.out.println("集合A和集合B不同的元素:"+differentList);        System.out.println("使用双层遍历方法 比对耗时: " + (endTime - startTime)+"毫秒。");    }
复制代码


🎨以上两个方法中我都做了 m*n 次循环,其实完全没有必要循环这么多次,我们的需求是找出两个 List 中的不同元素,那么我可以这样考虑:用一个 map 存放 lsit 的所有元素,其中的 key 为 lsit1 的各个元素,value 为该元素出现的次数,接着把 list2 的所有元素也放到 map 里,如果已经存在则 value 加 1,最后我们只要取出 map 里 value 为 1 的元素即可,这样我们只需循环 m+n 次,大大减少了循环的次数。

借助 Map 集合查找(推荐✅)

以 List 集合里的元素作为 Map 的 key,元素出现的次数作为 Map 的 Value,那么两个 List 集合的不同元素在 Map 集合中 value 值为 1,所对应的键。把所有 value 值为 1 的键找出来,我们就得到了两个 List 集合不同的元素。代码如下:


    /**     * 借助Map来获取listA、listB的不同元素集合     *     * @param listA 集合A     * @param listB 集合B     * @return list<String> 不同元素集合     */    public static List<String> getDifferListByMap(List<String> listA, List<String> listB) {        List<String> differList = new ArrayList<>();        Map<String, Integer> map = new HashMap<>();        long beginTime = System.currentTimeMillis();        for (String strA : listA) {            map.put(strA, 1);        }        for (String strB : listB) {            Integer value = map.get(strB);            if (value != null) {                map.put(strB, ++value);                continue;            }            map.put(strB, 1);        }
for (Map.Entry<String, Integer> entry : map.entrySet()) { //获取不同元素集合 if (entry.getValue() == 1) { differList.add(entry.getKey()); } } long endTime = System.currentTimeMillis(); System.out.println("集合A和集合B不同的元素:"+differList); System.out.println("使用map方式遍历, 对比耗时: " + (endTime - beginTime)+"毫秒。"); return differList; }
复制代码

1 千数据量

        List<String> listA = dataList(1000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(1006);        getDifferListByMap(listA,listB);
复制代码


耗时:7 毫秒


1 万数据量

        List<String> listA = dataList(10000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(10006);        getDifferListByMap(listA,listB);
复制代码


耗时:42 毫秒


10 万数据量

        List<String> listA = dataList(100000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(100006);        getDifferListByMap(listA,listB);
复制代码


耗时:130 毫秒


100 万数据量

        List<String> listA = dataList(1000000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(1000006);        getDifferListByMap(listA,listB);
复制代码


耗时:283 毫秒


1000 万数据量

        List<String> listA = dataList(10000000);        //集合A添加一个集合B没有的元素        listA.add("onlyA10086");        List<String> listB = dataList(10000006);        getDifferListByMap(listA,listB);
复制代码


耗时:6.6 秒



可以看出来这种方法相当高效,千万级数据比对也才用了 6.6 秒。使用 map 集合的方式寻找不同元素,时间增长基本上是线性的,它的时间复杂度为 O(m)。而上面的 remove 方式和双层循环遍历的时间复杂度为 O(m * n)。所以,选用这种方式带来的性能收益随着集合元素的增长而增长。

优化

上述通过 map 集合的方式效率很高,有没有可以优化的点呢?


  1. 两个集合如果数量差距较大时,可以把小的在后面添加,这样会减少循环里的判断,性能又有了一定的提升。

  2. 创建 map 集合的时候直接指定大小,防止再散列。


优化后代码如下:


    /**     * 找出两个集合中不同的元素     *     * @param collmax     * @param collmin     * @return     */    public static Collection getDifferListByMapPlus(Collection collmax, Collection collmin) {        //使用LinkedList防止差异过大时,元素拷贝        Collection csReturn = new LinkedList();        Collection max = collmax;        Collection min = collmin;        long beginTime = System.currentTimeMillis();        //先比较大小,这样会减少后续map的if判断次数        if (collmax.size() < collmin.size()) {            max = collmin;            min = collmax;        }        //直接指定大小,防止再散列        Map<Object, Integer> map = new HashMap<Object, Integer>(max.size());        for (Object object : max) {            map.put(object, 1);        }        for (Object object : min) {            if (map.get(object) == null) {                csReturn.add(object);            } else {                map.put(object, 2);            }        }        for (Map.Entry<Object, Integer> entry : map.entrySet()) {            if (entry.getValue() == 1) {                csReturn.add(entry.getKey());            }        }        long endTime = System.currentTimeMillis();        System.out.println("集合A和集合B不同的元素:"+csReturn);        System.out.println("使用map方式遍历, 对比耗时: " + (endTime - beginTime)+"毫秒。");        return csReturn;    }
复制代码

找出相同元素

同样找出相同元素可以使用如下代码:


    /**     * 找出两个集合中相同的元素     *     * @param collmax     * @param collmin     * @return     */    public static Collection getSameListByMap(Collection collmax, Collection collmin) {        //使用LinkedList防止差异过大时,元素拷贝        Collection csReturn = new LinkedList();        Collection max = collmax;        Collection min = collmin;        //先比较大小,这样会减少后续map的if判断次数        if (collmax.size() < collmin.size()) {            max = collmin;            min = collmax;        }        //直接指定大小,防止再散列        Map<Object, Integer> map = new HashMap<Object, Integer>(max.size());        for (Object object : max) {            map.put(object, 1);        }        for (Object object : min) {            if (map.get(object) != null) {                csReturn.add(object);            }        }        return csReturn;    }
复制代码


本文内容到此结束了,

如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。

如有错误❌疑问💬欢迎各位大佬指出。

主页共饮一杯无的博客汇总👨‍💻

保持热爱,奔赴下一场山海。🏃🏃🏃

发布于: 2022-11-09阅读数: 40
用户头像

鲜衣怒马意气风发,愿你归来仍是少年。 2018-10-19 加入

全栈开发者,CSDN博客专家,51CTO 专家博主,阿里云专家博主,华为云享专家,持续输出干货,欢迎关注。

评论

发布
暂无评论
Java高效找出两个大数据量List集合中的不同元素_Java_共饮一杯无_InfoQ写作社区