【问题标题】:What is the fastest way to compare two sets in Java?在 Java 中比较两组的最快方法是什么?
【发布时间】:2023-03-29 18:39:01
【问题描述】:

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

例如。

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

请注意集合中的记录数会很高。

谢谢

谢卡尔

【问题讨论】:

  • 在不知道(和修改)比较逻辑的情况下无法优化循环。你能展示更多你的代码吗?

标签: java performance set


【解决方案1】:
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 的内容都是空的,那么你知道这两个集合是相等的。如果不是,那么你已经得到了使集合不相等的元素。

您提到记录的数量可能很高。如果底层实现是HashSet,那么每条记录的获取都是在O(1) 时间内完成的,所以你真的没有比这更好的了。 TreeSetO(log n)

【讨论】:

  • 在 Set 上调用 equals() 时, Record 类的 equals() 和 hashcode() 的实现同样重要。
  • 我不确定 removeAll() 示例是否正确。 removeAll() 返回一个布尔值,而不是另一个 Set。 secondSet 中的元素实际上是从 firstSet 中删除的,如果进行了更改,则返回 true。
  • removeAll 示例仍然不正确,因为您还没有制作副本(设置一个 = firstSet;设置两个 = secondSet)。我会使用复制构造函数。
  • 其实equals的默认实现比最坏情况下两次调用containsAll要快;看我的回答。
  • 你需要做 Set one = new HashSet(firstSet),否则 firstSet 和 secondSet 中的项目将被删除。
【解决方案2】:

如果你只是想知道集合是否相等,AbstractSet 上的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 containsAll(c);
    }

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

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

之后,containsAll(...) 将在另一组中找到不在此组中的元素时立即返回false。但如果所有元素都存在于两个集合中,则需要对所有元素进行测试。

因此,当两个集合相等但对象不同时,性能最差。该成本通常为O(N)O(NlogN),具体取决于this.containsAll(c) 的实现。

如果集合很大并且仅在很小比例的元素上有所不同,您将获得接近最差情况的性能。


更新

如果您愿意在自定义集实现上投入时间,有一种方法可以改进“几乎相同”的情况。

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

您如何实现这样的哈希码?那么如果设置的哈希码是:

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

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

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


你可以把这个想法更进一步......至少在理论上。

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

假设您的设置元素类有一个方法可以返回元素的加密校验和。现在通过对元素返回的校验和进行异或来实现集合的校验和。

这对我们有什么好处?

好吧,如果我们假设没有任何秘密发生,那么任何两个不相等的集合元素具有相同的 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)。

缺点是计算元素的加密校验和非常昂贵,尤其是随着位数的增加。所以你真的需要一个有效的机制来记忆校验和。这可能是有问题的。

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

【讨论】:

  • 应该是 if (checksumsDoNotMatch(0)) return false;否则返回 doHeavyComparisonToMakeSureTheSetsReallyMatch(o);
  • 不一定。如果两个校验和匹配不相等的集合的概率足够小,我认为您可以跳过比较。算一下。
  • 对于实现,使用instanceof而不是getClass()和==是否有具体原因?
  • 好吧instanceof Set 表示任何Set。但是getClass() == ... 将测试特定的Set 实现类。语义不同。
  • Set.equals 的 javadoc 说:"如果指定的对象也是一个集合,则返回 true,这两个集合具有相同的大小,并且指定集合的​​每个成员都包含在此set(或者等效地,这个集合的每个成员都包含在指定的集合中)。这个定义确保 equals 方法在 set 接口的不同实现中正常工作。“所以@987654338 @ 必须用于实现指定的行为。
【解决方案3】:

Guava Sets 中有一个方法可以在这里提供帮助:

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

【讨论】:

    【解决方案4】:

    对于非常特殊的情况,有一个 O(N) 解决方案:

    • 集合都已排序
    • 两者排序相同

    以下代码假定两个集合都基于可比较的记录。类似的方法可以基于 Comparator。

        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;
            }
        }
    

    【讨论】:

      【解决方案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);
      }
      

      【讨论】:

      • 或者可能只是使用来自AbstractSet(JDK 附带)的equals() 方法,除了额外的null 检查之外,这与这里的解决方案几乎相同。 Java-11 Set Interface
      【解决方案6】:

      如果您使用Guava 库,可以这样做:

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

      然后根据这些做出结论。

      【讨论】:

        【解决方案7】:

        在比较之前,我会将 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
            }
        }
        

        【讨论】:

        • 或者你可以使用数组而不是第二个列表的哈希图。
        • 而且,此解决方案假定集合未排序。
        【解决方案8】:
        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;
            }
        

        【讨论】:

          【解决方案9】:

          我认为可以使用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
          

          【讨论】:

          • 这是一种复杂的表达方式set.equals(set2)
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-05-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-09-01
          • 1970-01-01
          相关资源
          最近更新 更多