哪个更有效:使用 removeAll() 或使用以下 HashMap 技术仅保留 ArrayList 中的更改记录
Posted
技术标签:
【中文标题】哪个更有效:使用 removeAll() 或使用以下 HashMap 技术仅保留 ArrayList 中的更改记录【英文标题】:Which is more efficient : using removeAll() or using the following HashMap technique to retain only changed records in an ArrayList 【发布时间】:2012-04-16 20:21:05 【问题描述】:我有 2 个 ArrayList
s A
和 B
具有相同的数据结构 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);
我认为这可能比上述解决方案复杂性低。对吗?
【问题讨论】:
忘掉效率吧,你原来的解决方案更具可读性。只有当它被证明是一个瓶颈时,你才应该考虑第二个。 【参考方案1】:是的,后一种算法比O(n^2)
更好,因为您有两个循环,一个在B
上,另一个在A
上,并且您在每个循环中(摊销)持续工作,您的新解决方案在@ 中运行987654324@.
我怀疑您没有任何重复的条目。如果是这种情况,您也可以通过HashSet
(如果您想保留A
中的订单,请更改为LinkedHashSet
):
HashSet<C> tmp = new HashSet<C>(A);
tmp.removeAll(B); // Linear operation
A = new ArrayList<C>(tmp);
(或者如果顺序对您来说不重要,您可以一直使用HashSet
s。)
正如@Daud 在下面的 cmets 中指出的那样,如果散列集的大小小于影响复杂性的集合(至少在 OpenJDK 中),HashSet.removeAll(Collection c)
实际上会重复调用c.contains
。这是因为实现总是选择迭代较小的集合。
【讨论】:
你的意思是性能差异吗?我不这么认为,因为在 java HashSet 是建立在 HashMap 之上的 :) 我看到了 HashSet 的源代码,似乎对于 removeAll(),它会遍历 tmp 并在传递给 removeAll 的参数上调用 contains() 方法,并将 tmp 的当前值作为参数。由于传递给 removeAll() 的参数是一个 ArrayList,它的 contains 方法需要 O(n)... 从而使整个操作 O(n^2) ? HashSet 的 contains 方法在恒定时间内运行(摊销)。 它不是调用 contains 方法的 HashSet。它是作为参数传递的 Collection 的(在这种情况下为 ArrayList)...也许 tmp 应该是一个 ArrayList 并且 removeAll 的参数是一个哈希集。 我查看了代码,您是对的。这让我非常惊讶。我会用你的发现更新答案。【参考方案2】:您可以节省的复杂性可能会在内存分配中丢失,因此不一定更有效。 Arraylist 使用类似于就地分区算法的东西来运行支持数组并针对比较进行测试。
在比较时,它只是查找与支持数组Object[]
匹配的第一次出现的索引。该算法维护两个索引,一个用于遍历后备数组,另一个作为匹配的占位符。在匹配的情况下,它只是移动后备数组上的索引并继续到下一个传入元素;这是相对便宜的。
如果它发现传入的集合不包含支持数组中当前索引处的值,它只会用当前索引处的元素覆盖最后一次匹配的元素,而不会导致新的内存分配。这种模式一直重复,直到 ArrayList 中的所有元素都与传入的集合进行了比较,因此您担心的复杂性。
例如: 考虑一个带有 1,2,4,5 的数组列表 A 和一个带有 4,1 的集合“C”,我们与之匹配;想要删除 4 和 1。这里是 for 循环上的每次迭代都会去 0 -> 4
迭代:r 是数组列表 a for (; r < size; r++)
上的 for 循环索引
r = 0(C是否包含1?是的,跳到下一个) 答:1、2、4、5w=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 到数组末尾的值清空并重置大小。
A: 2,5 大小为 2
内置的 removeAll 也认为 ArrayLists 可以包含 null。您可以在上面的解决方案中在 record.getStudentId() 上抛出 NPE。最后,removeAll 防止在 Collection.contains 的比较中出现异常。如果发生这种情况,它最终会使用本机内存复制,以高效的方式保护后备数组免受损坏。
【讨论】:
【参考方案3】:第二个“算法”肯定比第一个考虑摊销分析更好。这是最好的方法吗?你需要那个吗?它会在性能方面对用户造成任何明显的影响吗 列表中的项目数量是否增长得如此之多,以至于这成为系统的瓶颈?
第一种方法更具可读性,可以将您的意图传达给维护代码的人。此外,最好使用“经过测试”的 API,而不是重新发明***(除非绝对必要) 计算机已经变得如此之快,以至于我们不应该进行任何过早的优化。
如果有必要,我可能会使用 Set 的解决方案,类似于 @aioob 的
【讨论】:
【参考方案4】:在某些情况下,我在成员 removeAll 中遇到了性能瓶颈(与 EMF 模型操作相关)。对于上面提到的ArrayList
,只需使用标准removeAll
,但如果 A 是例如 EList,则可以遇到 n^2。
因此,避免依赖 List< T >
的特定实现的隐藏良好属性; Set.contains()
O(1) 是一个保证(如果你使用 HashSet
并且有一个不错的 hashCode,log2(n) for TreeSet
具有排序关系),用它来限制算法的复杂性。
我使用以下代码来避免无用的副本;目的是您正在扫描一个数据结构,找到您不想要的不相关元素并将它们添加到“todel”中。
出于某种原因,例如避免并发修改,您正在导航一棵树等...,您无法在执行此遍历时删除元素。因此,我们将它们累积成一个 HashSet “todel”。
在函数中,我们需要修改“container”,因为它通常是调用者的一个属性,但是在“container”上使用 remove(int index) 可能会因为元素的左移而导致复制。我们使用副本“内容”来实现这一点。
模板参数是因为在选择过程中,我经常会得到C的子类型,但可以随意使用
/**
* 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
将其放在实用程序类和静态导入中以方便使用。
【讨论】:
Set.contains() 根本不是 O(1) 保证的。首先,它只适用于基于散列的集合。但是糟糕的 hashCode() 函数会完全毁掉它。对于其他集合(如 TreeSet),它甚至不会是 O(1)。 同意,对于未设置的哈希集,O(1),具有半体面的 hashCode 函数。答案稍作修改。以上是关于哪个更有效:使用 removeAll() 或使用以下 HashMap 技术仅保留 ArrayList 中的更改记录的主要内容,如果未能解决你的问题,请参考以下文章