用Java比较两个集合的最快方法是什么?


102

我正在尝试优化一段比较列表元素的代码。

例如。

public void compare(Set<Record> firstSet, Set<Record> secondSet){
    for(Record firstRecord : firstSet){
        for(Record secondRecord : secondSet){
            // comparing logic
        }
    }
}

请考虑到记录集中的记录数量会很高。

谢谢

舍哈尔


7
在不了解(和修改)比较逻辑的情况下不可能优化循环。您可以显示更多代码吗?
josefx

Answers:


161
firstSet.equals(secondSet)

这实际上取决于您要在比较逻辑中执行的操作……即,如果您在一个集合中找到一个元素而不在另一个集合中找到一个元素,会发生什么?您的方法具有void返回类型,因此我假设您将在此方法中做必要的工作。

如果需要,可以进行更细粒度的控制:

if (!firstSet.containsAll(secondSet)) {
  // do something if needs be
}
if (!secondSet.containsAll(firstSet)) {
  // do something if needs be
}

如果您需要获取一组中的元素而不是另一组中的元素。
编辑:set.removeAll(otherSet)返回布尔值,而不是集合。要使用removeAll(),您必须复制该集合然后使用它。

Set one = new HashSet<>(firstSet);
Set two = new HashSet<>(secondSet);
one.removeAll(secondSet);
two.removeAll(firstSet);

如果内容onetwo都是空的,那么你知道这两组都是平等的。如果不是,那么您就有使集合不相等的元素。

您提到记录的数量可能很高。如果基础实现是a,HashSet那么每个记录的获取都将O(1)及时完成,因此您无法真正做到比这更好。TreeSetO(log n)


3
在Set上调用equals()时,Record类的equals()和hashcode()的实现同样重要。
Vineet Reynolds 2010年

1
我不确定removeAll()示例是否正确。removeAll()返回一个布尔值,而不是另一个Set。实际上,secondSet中的元素已从firstSet中移除,并且如果进行了更改,则返回true。
理查德·科菲尔德

4
removeAll示例仍然不正确,因为您尚未制作副本(设置一个= firstSet;设置两个= secondSet)。我会使用复制构造函数。
Michael Rusch 2013年

1
实际上,在最坏的情况下,默认的实现equals快于两次调用containsAll。看我的答案。
斯蒂芬·C

6
您需要设置Set = new HashSet(firstSet),否则firstSet和secondSet中的项目将被删除。
Bonton255

61

如果只想知道集合是否相等,则equalson方法AbstractSet大致实现如下:

    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;
        Collection c = (Collection) o;
        if (c.size() != size())
            return false;
        return containsAll(c);
    }

注意它如何优化以下常见情况:

  • 这两个对象是相同的
  • 另一个对象根本不是一个集合,并且
  • 两组尺寸不同。

之后,只要在另一个集合中找到一个不在该集合中的元素,containsAll(...)就会返回false。但是,如果两个集合中都存在所有元素,则需要测试所有元素。

因此,当两组相同但不相同的对象时,发生最坏情况的性能。该费用通常等于O(N)O(NlogN)取决于的实现this.containsAll(c)

如果集合很大并且仅在很小比例的元素上有差异,那么您将获得最差的案例性能。


更新

如果您愿意花时间在定制集实现上,则可以采用一种方法来改善“几乎相同”的情况。

这个想法是,您需要为整个集合预先计算并缓存一个哈希,以便可以在中获取集合的当前哈希码值O(1)。然后,您可以比较两个集合的哈希码作为加速。

您如何实现这样的哈希码?好吧,如果设置的哈希码是:

  • 空集为零,并且
  • 非空集的所有元素哈希码的异或,

那么您每次添加或删除元素时都可以廉价地更新集合的缓存哈希码。在这两种情况下,您只需将元素的哈希码与当前设置的哈希码进行异或。

当然,这假设元素哈希码是稳定的,而元素是集合的成员。它还假定元素类的哈希码函数具有良好的扩展性。这是因为,当两个设置的哈希码相同时,您仍然必须退回O(N)所有元素的比较。


至少从理论上讲,您可以进一步推广这个想法。

警告 -这是高度投机的。如果您愿意,可以进行“思想实验”。

假设您的set元素类具有一种返回该元素的加密校验和的方法。现在,通过对元素返回的校验和进行XOR来实现集合的校验和。

这能给我们带来什么?

好吧,如果我们假设没有发生任何意外情况,则任何两个不相等的集合元素具有相同的N位校验和的概率为2 -N。2个不等集具有相同N位校验和的概率也为2 -N。所以我的想法是,您可以实现equals为:

    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;
        Collection c = (Collection) o;
        if (c.size() != size())
            return false;
        return checksums.equals(c.checksums);
    }

在上述假设下,这只会在2 -N次内给您一次错误的答案。如果您使N足够大(例如512位),那么错误答案的可能性就可以忽略不计(例如大约10 -150)。

缺点是计算元素的加密校验和非常昂贵,尤其是随着位数的增加。因此,您确实需要一种有效的机制来记住校验和。那可能是有问题的。

另一个缺点是,无论概率多么小,错误概率都可能为零,这不可接受的。(但是如果是这样的话……您如何处理宇宙射线翻转临界位的情况?或者在冗余系统的两个实例中它同时翻转相同的位?)


应该是(checksumsDoNotMatch(0))返回false;否则返回doHeavyComparisonToMakeSureTheSetsReallyMatch(o);
Esko Piirainen

不必要。如果两个校验和对于不相等的集合匹配的概率很小,那么我认为可以跳过比较。算一算。
斯蒂芬·C

17

番石榴中有一种方法Sets可以在这里提供帮助:

public static <E>  boolean equals(Set<? extends E> set1, Set<? extends E> set2){
return Sets.symmetricDifference(set1,set2).isEmpty();
}

5

您可以从https://www.mkyong.com/java/java-how-to-compare-two-sets/获得以下解决方案

public static boolean equals(Set<?> set1, Set<?> set2){

    if(set1 == null || set2 ==null){
        return false;
    }

    if(set1.size() != set2.size()){
        return false;
    }

    return set1.containsAll(set2);
}

或者,如果您更喜欢使用单个return语句:

public static boolean equals(Set<?> set1, Set<?> set2){

  return set1 != null 
    && set2 != null 
    && set1.size() == set2.size() 
    && set1.containsAll(set2);
}

或者也许只是使用equals()from 的方法AbstractSet(与JDK一起提供),该方法与此处的解决方案几乎相同,除了附加的null检查。Java-11 Set接口
Chaithu Narayana

4

对于以下特定情况,有一个O(N)解决方案:

  • 集合都被排序
  • 两者以相同顺序排序

下面的代码假定两组都基于可比较的记录。类似的方法可以基于比较器。

    public class SortedSetComparitor <Foo extends Comparable<Foo>> 
            implements Comparator<SortedSet<Foo>> {

        @Override
        public int compare( SortedSet<Foo> arg0, SortedSet<Foo> arg1 ) {
            Iterator<Foo> otherRecords = arg1.iterator();
            for (Foo thisRecord : arg0) {
                // Shorter sets sort first.
                if (!otherRecords.hasNext()) return 1;
                int comparison = thisRecord.compareTo(otherRecords.next());
                if (comparison != 0) return comparison;
            }
            // Shorter sets sort first
            if (otherRecords.hasNext()) return -1;
            else return 0;
        }
    }

3

如果您使用的是Guava库,则可以执行以下操作:

        SetView<Record> added = Sets.difference(secondSet, firstSet);
        SetView<Record> removed = Sets.difference(firstSet, secondSet);

然后根据这些结论。


2

我将在比较之前将secondSet放入HashMap中。这样,您可以将第二个列表的搜索时间减少到n(1)。像这样:

HashMap<Integer,Record> hm = new HashMap<Integer,Record>(secondSet.size());
int i = 0;
for(Record secondRecord : secondSet){
    hm.put(i,secondRecord);
    i++;
}
for(Record firstRecord : firstSet){
    for(int i=0; i<secondSet.size(); i++){
    //use hm for comparison
    }
}

或者,您可以将数组而不是哈希图用于第二个列表。
Sahin Habesoglu,2015年

并且,该解决方案假定未对集合进行排序。
Sahin Habesoglu,2015年

1
public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;

        Set<String> a = this;
        Set<String> b = o;
        Set<String> thedifference_a_b = new HashSet<String>(a);


        thedifference_a_b.removeAll(b);
        if(thedifference_a_b.isEmpty() == false) return false;

        Set<String> thedifference_b_a = new HashSet<String>(b);
        thedifference_b_a.removeAll(a);

        if(thedifference_b_a.isEmpty() == false) return false;

        return true;
    }

-1

我认为可以使用带有equals方法的方法引用。我们假定毫无疑问的对象类型具有其自己的比较方法。简单明了的例子在这里,

Set<String> set = new HashSet<>();
set.addAll(Arrays.asList("leo","bale","hanks"));

Set<String> set2 = new HashSet<>();
set2.addAll(Arrays.asList("hanks","leo","bale"));

Predicate<Set> pred = set::equals;
boolean result = pred.test(set2);
System.out.println(result);   // true

1
这是一种复杂的说法set.equals(set2)
Alex
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.