哪个更有效:使用removeAll()或使用以下HashMap技术仅保留ArrayList中已更改的记录

时间:2012-04-03 07:27:04

标签: java arraylist hashmap performance removeall

我有2 ArrayListAB相同的数据结构C(hashCode()和equals()重写)。 C代表学生的记录。这两个列表大小相同,分别代表新的学生记录和旧学生记录(学生在两个列表中都是相同的,排序可能不同)。我希望只保留A中那些已被更改的记录。因此,我这样做:

 A.removeAll(B)

根据javadocs,这将获取A的每个记录并与B的每个记录进行比较,如果它们两者都相等,它将从A中删除记录。如果未发现A的记录等于B中的任何记录,由于A中的所有学生也在B中,这意味着A的记录已经改变。问题在于它容易达到n平方的复杂性。

另一种方法可以是:

Map<C> map = new HashMap<C>();
for (C record : B){
    map.add(record.getStudentId(),record);
}
List<C> changedRecords = new ArrayList<C>();
for (C record : A){
    if (record.equals(map.get(record.getStudentId())){
        changedRecords.add(record);
    }
}

我认为这可能比上述解决方案的复杂性低。这是对的吗?

4 个答案:

答案 0 :(得分:10)

是的,后一种算法优于O(n^2),因为你有两个循环,一个超过B,另一个超过A,你在每个循环中做(摊销)常量工作,您的新解决方案在O(|A| + |B|)中运行。

我怀疑你没有任何重复的条目。如果是这种情况,您也可以通过HashSet(如果您想要保留LinkedHashSet中的订单,请更改为A):

HashSet<C> tmp = new HashSet<C>(A);
tmp.removeAll(B);                     // Linear operation
A = new ArrayList<C>(tmp);

(或者,如果订单对您无关紧要,您可以一直使用HashSet。)


正如@Daud在下面的评论中所指出的,如果哈希集的大小小于影响复杂性的集合(至少在OpenJDK中),HashSet.removeAll(Collection c)实际上会反复调用c.contains。这是因为实现总是选择迭代较小的集合。

答案 1 :(得分:1)

您可以节省内存分配中可能丢失的复杂性,因此不一定更有效。 Arrraylist使用类似于就地分区算法的东西来运行支持数组并对比较进行测试。

比较时,它只是查找与支持数组Object[]匹配的第一个匹配项的索引。该算法维护两个索引,一个用于迭代后备数组,另一个用作匹配的占位符。在匹配的情况下,它只是移动后备阵列上的索引并继续到下一个传入元素;这相对便宜。

如果它发现传入集合不包含支持数组中当前索引的值,它只是用当前索引处的元素覆盖最后一次匹配的元素,而不会产生新内存分配。重复此模式,直到ArrayList中的所有元素都与传入集合进行比较,因此您需要考虑复杂性。

例如: 考虑一个带有1,2,4,5的arraylist A和一个我们匹配的带有4,1的集合'C';想要删除4和1.这里是for循环的每次迭代,它将变为0 - &gt; 4

迭代:r是arraylist上的for循环索引a for (; r < size; r++)

r = 0(C是否包含1?是的,跳到下一个) 答:1,2,4,5 w = 0

r = 1(C是否包含2?不,将r处的值复制到w ++指向的位置) 答:2,2,4,5 w = 1

r = 2(C是否包含4 ?,是跳过) 答:2,2,4,5 w = 1

r = 3(C是否包含5?不,将r处的值复制到w ++指向的位置)

A:2,5,4,5 w = 2

r = 4,停止

将w与4的后备阵列的大小进行比较。因为它们不相等所以将w on的值从数组中取出并重置大小。

A:2,5尺寸2

内置的removeAll也认为ArrayLists可以包含null。您可以在上面的解决方案中将record.getStudentId()抛出NPE。最后,removeAll可以防止Collection.contains上的比较中出现异常。如果发生这种情况,它最终会使用本机内存,以高效的方式保护后备阵列免受损坏。

答案 2 :(得分:1)

绝对第二个'算法'优于首先考虑摊销分析。这是最好的方式吗?你需要那个吗?它会在性能方面对用户造成任何明显的影响 列表中的项目数量是否会变得如此庞大,这会成为系统中的瓶颈吗?

第一种方法更具可读性,向维护代码的人传达您的意图。此外,最好使用'测试'API而不是重新发明轮子(除非绝对必要) 计算机变得如此之快,以至于我们不应该做任何过早的优化。

如果看到必要,我可能会使用Set来解决方案,类似于@ aioob的

答案 3 :(得分:1)

在某些情况下,我遇到了成员removeAll的性能瓶颈(与EMF模型操作相关)。对于上面提到的ArrayList,只需使用标准removeAll,但如果A例如是EList,则可能遇到n ^ 2。

因此,避免依赖于List< T >的特定实现的隐藏的良好属性; Set.contains() O(1)是一个保证(如果你使用HashSet并且有一个像样的hashCode,log2(n)用于具有排序关系的TreeSet),用它来约束算法的复杂性。

我使用以下代码来避免无用的副本;意图是您正在扫描数据结构,找到您不想要的不相关元素,并将它们添加到“todel”。

由于某些原因,例如避免并发修改,您正在导航树等...,您无法在执行此遍历时删除元素。所以,我们将它们累积成一个HashSet“todel”。

在函数中,我们需要修改“容器”,因为它通常是调用者的一个属性,但在“容器”上使用remove(int index)可能会因为元素的左移而导致复制。我们使用副本“内容”来实现这一目标。

模板参数是因为在选择过程中,我经常得到C的子类型,但可以随意使用&lt; T>无处不在。

/**
 * Efficient O (n) operation to removeAll from an aggregation.
 * @param container a container for a set of elements (no duplicates), some of which we want to get rid of
 * @param todel some elements to remove, typically stored in a HashSet.
 */
public static <T> void removeAll ( List<T> container, Set<? extends T> todel ) {
    if (todel.isEmpty())
        return;
    List<T> contents = new ArrayList<T>(container);
    container.clear();
    // since container contains no duplicates ensure |B| max contains() operations
    int torem = todel.size();
    for (T elt : contents) {
        if ( torem==0 || ! todel.contains(elt) ) {
            container.add(elt);
        } else {
            torem--;
        }
    }
}

因此,在您的情况下,您将调用:removeAll(A, new HashSet < C >(B)); 如果你真的不能累积到一个Set&lt; C>在选择阶段。

将其放在实用程序类和静态导入中以便于使用。