如何使用Java最快捷的找出两个大量数据List集合的不同元素

7 篇文章 0 订阅

近期,花了一段时间开发生产环境中的自动补数工具,来替代我完成繁琐无趣的人肉运维工作.

这款补数工具有这样一个小的需求点:
快速的找出两个相似度非常高的List集合里的不同元素.
(每个集合大约有200W个元素,并且随着业务的扩展集合的数据量会越来越大,两个集合之间可能只有几个元素不同)
期间,踩了一点小坑,把入坑到脱坑的过程分享给大家,希望大家遇到类似的需求时能选取合适的方法,少走弯路

本篇博客相关代码已经上传到github,需要的请自行下载:

项目github链接

本篇博客涉及代码的github链接

本篇博客要点如下:

测试数据集准备

笨比操作一 ---- 使用Java自带的api比较不同

笨比操作二 ---- 使用List集合双层遍历比较不同

人类方法 ---- 借助Map集合找出不同

全量代码分享

一点思考

一.测试数据集准备

测试数据为A集合: 1千, 1万, 10万,1百万, 1千万的数据量.
B集合比A集合多十条数据.
测试数据使用空字符串 + 自然数的方式.

造数方法如下:

 	/**
     * 制造任意个元素的的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;
    }

我们采用以下方法打印出两个集合不同的元素,
以此来确保我介绍到的每种方法都可以正确找到两个集合中的不同元素

    /**
     * 遍历集合,打印出每个元素
     * @param list List集合
     */
    private static void traverse(List<String> list) {
        for (String str : list) {
            System.out.print(str + " ");
        }
        System.out.println();
    }

二.使用Java自带的api比较不同

这是我最初使用的办法,原因,代码量少,不用动脑.
核心代码只有以下四行:

		/*
			通过下面的四行代码,
			我们得到了以下三个集合
			ListA : 集合A与集合B不同的元素
			ListB : 集合B与集合A不同的元素
			ListABak : 集合A与集合B相同的元素
			乍一看是不是非常高效快捷?
			别急,请看后面的性能验证
		*/
		List<String> listABak = new ArrayList<>(listA); // 复制A集合作为备份
 		listB.removeAll(listA); // B集合与A集合的不同元素
        listABak.removeAll(listB); // A集合与B集合的相同元素
        listA.removeAll(listABak); // A集合与B集合的不同元素

数量级为1000条数据性能如下(对比耗时单位全部为毫秒,后续不再赘述) ,还算正常
在这里插入图片描述

数量级为10000条数据性能如下,也还可以接受
在这里插入图片描述

数量级为100000条数据性能如下,这里就已经比较慢了

在这里插入图片描述
数据量100W,1000W的性能结果,我就不再贴出来了
因为,100W的数据量,我跑了一下午没跑出来!!!
按照这个效率,1000W估计能跑到年底~

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

public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<?> it = iterator();
        while (it.hasNext()) {
            if (c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }

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

public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

继续追踪:indexOf源码:

 public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

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

三. 笨比操作二 ---- 使用双层遍历比较不同

	/*
	该方法实际上就是将removeAll的实现逻辑用自己的方式写出来
	所以执行效率,运行结果和上一种方法没什么区别,这里只贴代码出来,不再赘述
	*/
    private static void stupidMethod2(List<String> listA, List<String> listB) {
        System.out.println("数量级为 " + listA.size() + "集合的不同元素为");
        List<String> differList = new ArrayList<>();
        long startTime = System.currentTimeMillis();
        for (String str : listB) {
            if (!listA.contains(str)) {
                differList.add(str);
            }
        }
        traverse(differList);
        long endTime = System.currentTimeMillis();
        System.out.println("使用双层遍历方法 对比耗时: " + (endTime - startTime));
    }

四.人类方法 ---- 借助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) {
        System.out.println("数量级为 " + listA.size() + "集合的不同元素为");
        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());
            }
        }
        traverse(differList);
        long endTime = System.currentTimeMillis();
        System.out.println("使用map方式遍历, 对比耗时: " + (endTime - beginTime));
        return differList;
    }

程序运行情况如下:

集合元素个数为1000时:

在这里插入图片描述

集合元素个数为10000时,这里已经比上面的方法快不少了

在这里插入图片描述

集合元素个数为100000时,性能的提升显而易见

在这里插入图片描述

集合元素个数为1百万时,依然很快!

在这里插入图片描述
集合元素为1千万时,也只需要15S就能对比出结果

在这里插入图片描述
我们可以看到,使用map集合的方式寻找不同元素,时间增长基本上是线性的
它的时间复杂度为O(m)
而上面的remove方式和双层循环遍历的时间复杂度为O(m * n)
所以,选用这种方式带来的性能收益随着集合元素的增长而增长

全量代码分享

以上实例中涉及到的全量代码如下:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author xmr
 * @date 2020/3/5 8:18
 * @description
 */
public class ListTest {
    public static void main(String[] args) {
        List<String> listA = dataList(10000000);
        List<String> listB = dataList(10000000 + 10);
        // stupidMethod(listA, listB);
        // stupidMethod2(listA, listB);
        getDifferListByMap(listA, listB);
    }

    private static void stupidMethod2(List<String> listA, List<String> listB) {
        System.out.println("数量级为 " + listA.size() + "集合的不同元素为");
        List<String> differList = new ArrayList<>();
        long startTime = System.currentTimeMillis();
        for (String str : listB) {
            if (!listA.contains(str)) {
                differList.add(str);
            }
        }
        traverse(differList);
        long endTime = System.currentTimeMillis();
        System.out.println("使用双层遍历方法 对比耗时: " + (endTime - startTime));
    }

    /**
     * 遍历集合,打印出每个元素
     *
     * @param list List集合
     */
    private static void traverse(List<String> list) {
        for (String str : list) {
            System.out.print(str + " ");
        }
        System.out.println();
    }

    private static void stupidMethod(List<String> listA, List<String> listB) {
        System.out.println("数量级为 " + listA.size() + "集合的不同元素为");
        List<String> listABak = new ArrayList<>(listA); // 复制A集合作为备份
        long startTime = System.currentTimeMillis();
        listB.removeAll(listA); // B集合与A集合的不同元素
        traverse(listB);
        long endTime = System.currentTimeMillis();
        System.out.println("直接调用java api 方法 对比耗时: " + (endTime - startTime));
//        listABak.removeAll(listB); // A集合与B集合的相同元素
//        listA.removeAll(listABak); // A集合与B集合的不同元素
    }

    /**
     * 制造任意个元素的的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;
    }


    /**
     * 借助Map来获取listA、listB的不同元素集合
     *
     * @param listA 集合A
     * @param listB 集合B
     * @return list<String>
     */
    public static List<String> getDifferListByMap(List<String> listA, List<String> listB) {
        System.out.println("数量级为 " + listA.size() + "集合的不同元素为");
        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());
            }
        }
        traverse(differList);
        long endTime = System.currentTimeMillis();
        System.out.println("使用map方式遍历, 对比耗时: " + (endTime - beginTime));
        return differList;
    }

}

一点思考

如今,越来越多的公司在面试的时候考察程序员的数据结构与算法
曾经,我也一度觉得面试造火箭,工作拧螺丝.. 认为通过这种方式选拔人才有失偏颇
但实际上, 算法与数据结构,计算机基本知识这些基础性的东西,最能体现出功底和成长空间.
功能是个人百度一下都能实现.
但是实现的优雅,高效却是一门学问.
在复杂的生产环境中,可能一行糟糕的代码实现给用户带来的是灾难级别的体验.
因此,我们应该多问问自己代码这样写合不合适?
对自己写的代码心存敬畏,对自己严格要求

另外一篇:Java高效找出两个大数据量List集合中的不同元素 - 掘金 

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值