我有2 ArrayList
个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);
}
}
我认为这可能比上述解决方案的复杂性低。这是对的吗?
答案 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>在选择阶段。
将其放在实用程序类和静态导入中以便于使用。