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

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

本文将带你了解如何快速的找出两个相似度非常高的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毫秒
image.png

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毫秒
image.png

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+"毫秒。");

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

100万数据量

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

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

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

image.png
通过源码我们可以看到,该方法是使用for循环对集合进行遍历
第一层循环需要执行 listA.size()次,里面调用了contains方法来确定集合B是否含有该元素,
再看contains方法的源码:
image.png
可以看到,indexOf方法里又进行了一层遍历.
平均每次遍历要进行list.size() / 2次计算,
假设集合A的元素个数为m,集合B的元素个数为n
我们可以得到结论,运算次数为 m *(n/2)
对于100W数据量来说,假设你的计算机每秒能够执行1千万次运算,也需要13.8个小时才能对比出来。所以大数据量不建议通过此方法
image.png

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毫秒
image.png

1万数据量

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

耗时:42毫秒
image.png

10万数据量

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

耗时:130毫秒
image.png

100万数据量

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

耗时:283毫秒
image.png

1000万数据量

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

耗时:6.6秒
image.png
可以看出来这种方法相当高效,千万级数据比对也才用了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;
    }
相关实践学习
基于MaxCompute的热门话题分析
Apsara Clouder大数据专项技能认证配套课程:基于MaxCompute的热门话题分析
相关文章
|
6月前
|
Java 大数据 API
Java Stream API:现代集合处理与函数式编程
Java Stream API:现代集合处理与函数式编程
339 100
|
6月前
|
Java API 数据处理
Java Stream API:现代集合处理新方式
Java Stream API:现代集合处理新方式
352 101
|
6月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
5月前
|
存储 算法 安全
Java集合框架:理解类型多样性与限制
总之,在 Java 题材中正确地应对多样化与约束条件要求开发人员深入理解面向对象原则、范式编程思想以及JVM工作机理等核心知识点。通过精心设计与周密规划能够有效地利用 Java 高级特征打造出既健壮又灵活易维护系统软件产品。
158 7
|
5月前
|
存储 Java 索引
(Python基础)新时代语言!一起学习Python吧!(二):字符编码由来;Python字符串、字符串格式化;list集合和tuple元组区别
字符编码 我们要清楚,计算机最开始的表达都是由二进制而来 我们要想通过二进制来表示我们熟知的字符看看以下的变化 例如: 1 的二进制编码为 0000 0001 我们通过A这个字符,让其在计算机内部存储(现如今,A 字符在地址通常表示为65) 现在拿A举例: 在计算机内部 A字符,它本身表示为 65这个数,在计算机底层会转为二进制码 也意味着A字符在底层表示为 1000001 通过这样的字符表示进行转换,逐步发展为拥有127个字符的编码存储到计算机中,这个编码表也被称为ASCII编码。 但随时代变迁,ASCII编码逐渐暴露短板,全球有上百种语言,光是ASCII编码并不能够满足需求
238 4
|
6月前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
172 4
|
7月前
|
存储 NoSQL Java
Java Stream API:集合操作与并行处理
Stream API 是 Java 8 提供的集合处理工具,通过声明式编程简化数据操作。它支持链式调用、延迟执行和并行处理,能够高效实现过滤、转换、聚合等操作,提升代码可读性和性能。
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
1632 1
|
运维 关系型数据库 Java
PolarDB产品使用问题之使用List或Range分区表时,Java代码是否需要进行改动
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
Java API Apache
怎么在在 Java 中对List进行分区
本文介绍了如何将列表拆分为给定大小的子列表。尽管标准Java集合API未直接支持此功能,但Guava和Apache Commons Collections提供了相关API。
519 1

热门文章

最新文章