• 如何使用 arrayList.removeAll(Collection<?> c)?


    引言

    对于 Collection 集合及其实现类都有 removeAll(Collection c)

    对于ArrayList实例对象,在数据比较多的情况下,方法 removeAll() 的传参 c 的类型是 HashSet会比是 ArrayList 的情况快的多。

    原因

    我们来细看一下ArrayList类的removeAll()方法实现的伪代码。

    如:arrayList.removeAll(subList);
    
    // 遍历底层数组,将不需要删除的元素放在数组前面,后面的全部置为 null
    // w 为要删除和不删除的分界线
    int w = 0;
    for(var value in 该 arrayList 的底层数组){
        if(!subList.contains(value)){
            该 arrayList 的底层数组 [w] = value;
            w++;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里影响速率关键的一步是:subList.contains(value)

    这是因为contains()方法在不同类中的实现是存在差异的。

    对于 ArrayList.contains(),它的实现是调用 indexOf(),一个一个地遍历查找。最坏时间复杂度为O(总数据量)。

    而对于 HashSet.contains(),由于 HashSet 的底层是 HashMap,因此实际调用的是 HashMapcontainsKey()方法,该方法是通过哈希计算的方式去查询的,因此速度十分快。最坏的时间复杂度约为O(最长链表长度),而链表长度一般不会过大。

    使用方法

    在数据量比较大的的情况下,使用arrayList.removeAll(subList)时,可以将subList封装为HashSet

    arrayList.removeAll(new HashSet(subList));
    
    • 1

    速度实测:

    数据量ArrayListHashSetLinkedList
    10 万1094 毫秒6 毫秒1133 毫秒
    20 万4140毫秒8 毫秒4241 毫秒
    50 万51431毫秒30 毫秒34380 毫秒
    100 万140444 毫秒36 毫秒179465 毫秒
    500 万9130706 毫秒79 毫秒10549229 毫秒

    测试用的代码:

    public class RemoveAllTest {
        public static void main(String[] args) {
            ArrayList<Integer> arrayList = new ArrayList<>();
            for (int i = 0; i < 5000000; i++) {
                arrayList.add(i);
            }
            ArrayList<Integer> subList = new ArrayList<>();
            for (int i = 0; i < 5000000; i++) {
                subList.add(i);
                i += 2;
            }
            // 测试入参为 ArrayList 类型时 removeAll() 的性能
            long startTime = System.currentTimeMillis();
            arrayList.removeAll(subList);
            long endTime = System.currentTimeMillis();
            System.out.println("ArrayList 耗时:" + (endTime - startTime));
    
            // 测试入参为 HashSet 类型时 removeAll() 的性能
            ArrayList<Integer> arrayList2 = new ArrayList<>();
            for (int i = 0; i < 5000000; i++) {
                arrayList2.add(i);
            }
            startTime = System.currentTimeMillis();
            arrayList2.removeAll(new HashSet<>(subList));
            endTime = System.currentTimeMillis();
            System.out.println("HashSet 耗时:" + (endTime - startTime));
    
            // 测试将 ArrayList 类型转成 LinkedList 类型
            ArrayList<Integer> arrayList3 = new ArrayList<>();
            for (int i = 0; i < 5000000; i++) {
                arrayList3.add(i);
            }
            startTime = System.currentTimeMillis();
            new LinkedList(arrayList3).removeAll(subList);
            endTime = System.currentTimeMillis();
            System.out.println("LinkedList 耗时:" + (endTime - startTime));
        }
    }
    
    • 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

    HashSet 、LinkedList 中 removeAll() 方法的区别

    在这里插入图片描述

    不同类的 removeAll() 方法实现不同,可以看到对于 HashSetLinkedList,他们的 removeAll() 方法是通过父类或超父类的迭代器进行实现的,而 ArrayList 是自己通过 for 循环进行了实现。

    HashSet 内部实现

    依托于 AbstractSet 类的 removeAll(Collection c) 方法,实现的逻辑是:

    先调原集合对象 HashSetremoveAll(Collection c) 方法中传入的参数 c 的 size() 方法,用来判断谁包含的元素更多。

    • 如果原集合对象的元素数量 > c 中元素数量,那么调用 c 的代器去遍历 c ,查看元素是否包含在原集合中,并使用原集合的 remove() 方法去删除元素。时间复杂度为 O(n)。

    • 如果原集合对象的元素数量 < c 中元素数量,那么调用原集合对象的迭代器去遍历原集合,检查元素是否包含在 c 中,并调用原集合迭代器的 remove() 方法去删除元素。这里的时间复杂度与集合 c 的 contains() 方法的实现有关:

      • 如果 c 是一个 ArrayListcontains() 方法的时间复杂度是 O( m )。因此,从集合 HashSet 中删除 ArrayList 中存在的所有元素的总体时间复杂度为 O( n * m )。

      • 如果 c 再次是 HashSet,则 contains() 方法的时间复杂度为 O(1)。因此,从集合 HashSet 中删除 HashSet 中存在的所有元素的总体时间复杂度为 O( n )。

    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
    
        if (size() > c.size()) {
            for (Iterator<?> i = c.iterator(); i.hasNext(); )
                modified |= remove(i.next());
        } else {
            for (Iterator<?> i = iterator(); i.hasNext(); ) {
                if (c.contains(i.next())) {
                    i.remove();
                    modified = true;
                }
            }
        }
        return modified;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    LinkedList 内部实现

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    通过 contains() 方法来判断是否存在相同的元素,效率与 c 的类型有关。

    参考

  • 相关阅读:
    redis如何实现缓存预热
    语音相似度评价
    C++医院影像科PACS源码:三维重建、检查预约、胶片打印、图像处理、测量分析等
    RK3399驱动开发 | 06 - GT911触摸屏驱动调试及驱动浅析(Linux 5.4内核)
    【重铸Java根基】理解Java代理模式机制
    算法竞赛入门【码蹄集新手村600题】(MT1401-1450)
    系统架构设计师 2023年 论文
    HarmonyOS开发环境搭建
    单线程传奇Redis,为何引入多线程?
    MySQL中的死锁
  • 原文地址:https://blog.csdn.net/weixin_43987408/article/details/133801082