【问题标题】:Comparing iterables for same content, but not regarding order比较相同内容的迭代,但不考虑顺序
【发布时间】:2019-10-26 09:38:39
【问题描述】:

我正在尝试比较两个相同大小的 Java 中的 Iterables。我只需要知道内容是一样的。但是,像 [1, 2] 和 [1, 2, 2] 这样的东西不应该相等,而 [1, 2, 2, 4] 应该等于 [1, 2, 4, 2]。

boolean functionName() {
    boolean pvk;
    ... setup ...
    for(Edge e : pMST.edges()) {
      pvk = false;
      for(Edge f : kMST.edges()) {
        if(e == f) {
          pvk = true;
          System.out.println("True.");
        }
      }
      if(!pvk) return false;
    }
return true;
}

这是我最初的糟糕尝试,但这不仅总是返回 false,而且不能正确解释重复项。

【问题讨论】:

  • 边缘是一个对象。使用equals 而不是==
  • 您期待更多的元素吗?因为关于内存占用,我想不出不使用 n 元素的临时容器的解决方案(至少在某些时候)。
  • 就地解决方案显然是最好的,但额外的内存可能适合我的输入大小。
  • 实际上,我找到了this 的答案,对于带有重复项的可迭代对象,这只是稍微提高了内存效率。尽管如此,它应该已经帮助你了。
  • 您可以使用Map<Integer, Integer> 对其进行建模,其中键是数字,值是发生的次数。

标签: java collections iterator


【解决方案1】:

我会对它们进行排序。但首先我会在排序之前比较大小。您需要提供 Comparator<T> 以供 sort 方法使用。如果要对整数进行排序,可以使用:

      List<Integer> a = new ArrayList<>(List.of(1, 2, 3, 3, 3, 3, 4, 5, 6));
      List<Integer> b = new ArrayList<>(List.of(2, 3, 1, 3, 4, 5, 6, 3, 3));
      System.out.println(compareLists(a, b, Comparator.naturalOrder()));
   public static <T> boolean compareList(List<T> list1, List<T> list2,
         Comparator<T> comp) {

      if (list1 == list2) {
          return true;
      }
      if (list1.size() != list2.size()) {
         return false;
      }
      Collections.sort(list1, comp);
      Collections.sort(list2, comp);

      return list1.equals(list2);
   }

【讨论】:

  • @Holger 你是对的。而且可悲的是,就在几天前,我使用了这个功能,并思考了必须有位置一对一映射的要求。
【解决方案2】:

结合this answer 与this thread 的想法,特别是this answer 来创建一个高效但可读的解决方案,您可以使用

static boolean unorderedEquals(Collection<?> coll1, Collection<?> coll2) {
    if(coll1.size() != coll2.size()) return false;
    Map<Object, Integer> freq = new HashMap<>();
    for(Object o: coll1) freq.merge(o, 1, Integer::sum);
    for(Object o: coll2)
        if(freq.merge(o, -1, Integer::sum) < 0) return false;
    return true;
}

第一个循环像链接的答案一样创建一个频率图,但不是构建第二个图来执行昂贵的比较,而是第二个循环减少每次出现的计数,如果计数变为负数,则立即返回。 merge 方法可以顺利处理缺少键的情况。

由于在方法的开头已经检查了两个列表的大小相同,因此在增加和减少之后,总计数必须为零。由于我们已经证明不存在负数,因此我们立即为它们返回,因此也不可能存在正的非零值。所以我们可以在第二次循环之后返回true,而无需进一步检查。

支持任意的Iterables,它与Collection 的不同之处在于不一定有size() 方法,有点棘手,因为我们不能进行预检查,因此必须保持计数:

static boolean unorderedEquals(Iterable<?> iter1, Iterable<?> iter2) {
    Map<Object, Integer> freq = new HashMap<>();
    int size = 0;
    for(Object o: iter1) {
        freq.merge(o, 1, Integer::sum);
        size++;
    }
    for(Object o: iter2)
        if(--size < 0 || freq.merge(o, -1, Integer::sum) < 0) return false;
    return size == 0;
}

如果我们想避免装箱开销,我们必须为地图使用可变值,例如

static boolean unorderedEquals(Collection<?> coll1, Collection<?> coll2) {
    if(coll1.size() != coll2.size()) return false;
    Map<Object, int[]> freq = new HashMap<>();
    for(Object o: coll1) freq.computeIfAbsent(o, x -> new int[1])[0]++;
    int[] absent = { 0 };
    for(Object o: coll2) if(freq.getOrDefault(o, absent)[0]-- == 0) return false;
    return true;
}

但我认为他不会有回报。对于少量出现,装箱将重用 Integer 实例,而在使用可变值时,我们需要为每个不同元素使用不同的 int[] 对象。

但是使用compute 可能对Iterable 解决方案很有趣,像这样使用它时

static boolean unorderedEquals(Iterable<?> coll1, Iterable<?> coll2) {
    Map<Object, int[]> freq = new HashMap<>();
    for(Object o: coll1) freq.computeIfAbsent(o, x -> new int[1])[0]++;
    int[] absent = {};
    for(Object o: coll2)
        if(freq.compute(o, (key,c) -> c == null || c[0] == 0? absent:
                                      --c[0] == 0? null: c) == absent) return false;
    return freq.isEmpty();
}

当条目计数为零时从映射中删除条目,因此我们只需要在最后检查映射是否为空。

【讨论】:

    【解决方案3】:

    您可以对项目进行排序并比较结果列表,但这可能会很慢 O(n lg n) 并且它依赖于项目要么是 Comparable 要么由一个比较器。这可能是不可行的。

    other answer 建议使用 Guava Multiset。这是有道理的,因为它会跟踪元素和出现次数,这对您的问题很重要。对于诸如HashMultiset 之类的合理实现,它应该是 O(n)。 Apache Commons (MultiSet) 和 Eclipse Collections (Bag) 等其他库的集合实现在功能上等同于 Guava 的 Multiset。

    如果您不想包含对这些库中的任何一个的依赖项,您可以在 JDK 中单独执行此操作。不幸的是,Java 没有 Bag 实现,但为此目的,很容易使用 Map 从您的项目类型到整数或长整数来模拟它。

    如果你有列表,你可以这样做:

    boolean unorderedEquals(List<Item> list1, List<Item> list2) {
        Map<Item, Long> freq1 = list1.stream().collect(groupingBy(i -> i, counting()));
        Map<Item, Long> freq2 = list2.stream().collect(groupingBy(i -> i, counting()));
        return freq1.equals(freq2);
    }
    

    如果您有 Iterables,则需要使用 forEach 来构建地图:

    boolean unorderedEquals(Iterable<Item> iter1, Iterable<Item> iter2) {
        Map<Item, Integer> freq1 = new HashMap<>();
        iter1.forEach(it -> freq1.merge(it, 1, (a, b) -> a + b));
        Map<Item, Integer> freq2 = new HashMap<>();
        iter2.forEach(it -> freq2.merge(it, 1, (a, b) -> a + b));
        return freq1.equals(freq2);
    }
    

    【讨论】:

    • 很好的答案,但我不想为此使用外部库。
    • @SolidSnackDrive 这就是为什么这个答案显示了一个不使用第三方库的解决方案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-09-30
    • 1970-01-01
    • 1970-01-01
    • 2016-12-25
    • 1970-01-01
    • 1970-01-01
    • 2010-09-08
    相关资源
    最近更新 更多