Java ArrayList - 如何判断两个列表是否相等,不考虑顺序?

175

我有两个类型为AnswerArrayList(自定义类)。

我想比较这两个列表,以确定它们是否包含相同的内容,但不考虑顺序。

例如:


//These should be equal.
ArrayList<String> listA = {"a", "b", "c"}
ArrayList<String> listB = {"b", "c", "a"}

List.equals声明,如果两个列表包含相同大小、内容和顺序的元素,则它们相等。我想实现相同的功能,但不考虑顺序。

有没有简单的方法可以实现这个功能?还是我需要使用嵌套的for循环,并手动检查两个列表的每个索引?

注意:我不能将它们从ArrayList更改为其他类型的列表,它们必须保持不变。


4
请参见此问题的答案:https://dev59.com/dnNA5IYBdhLWcg3wI6R4#1075699 - David Kroukamp
请参考Java中的List.containsAll(list)函数。 - Sahil Jain
20个回答

183

对于任何列表来说,可能最简单的方法是:

listA.containsAll(listB) && listB.containsAll(listA)

5
好的,我会尽力进行翻译。“那样有什么乐趣呢?但说真的,这可能是更好的解决方案。” - Jacob Schoen
81
取决于是否认为"[a,b,c]"和"[c,b,a,b]"具有相同的内容。这个答案会说它们是相同的,但对于提问者来说可能不是这样(因为一个包含重复项,而另一个没有)。更不用提效率问题了。 - yshavit
4
根据评论进行改进 - System.out.println(((l1.size() == l2.size())&&l2.containsAll(l1)&&l1.containsAll(l2)));根据评论改进 - System.out.println(((l1.size() == l2.size()) && l2.containsAll(l1) && l1.containsAll(l2))); (该代码段为Java代码,输出结果为布尔值) - Nrj
9
@Nrj,[1,2,2]和[2,1,1]怎么样? - ROMANIA_engineer
5
这种方法的时间复杂度是O(n^2)。考虑两个逆序的列表,例如:[1,2,3]和[3,2,1]。对于第一个元素,必须扫描n个元素,对于第二个元素,必须扫描n-1个元素,依此类推。因此,时间复杂度将是n的平方阶。我认为更好的方法是先排序,然后再使用equals。这将具有O(n * log(n))的时间复杂度。 - puneet
显示剩余7条评论

143

你可以使用Collections.sort()对这两个列表进行排序,然后使用equals方法进行比较。稍微更好的解决方案是在排序之前先检查它们的长度是否相同,如果不同,则它们不相等,然后再排序,最后再使用equals方法比较。例如,如果你有两个字符串列表,代码如下:

public  boolean equalLists(List<String> one, List<String> two){     
    if (one == null && two == null){
        return true;
    }

    if((one == null && two != null) 
      || one != null && two == null
      || one.size() != two.size()){
        return false;
    }

    //to avoid messing the order of the lists we will use a copy
    //as noted in comments by A. R. S.
    one = new ArrayList<String>(one); 
    two = new ArrayList<String>(two);   

    Collections.sort(one);
    Collections.sort(two);      
    return one.equals(two);
}

22
请记住不要破坏原始列表的顺序(就像Collections.sort一样),即传递一个副本。 - arshajii
3
你可以添加 one = new ArrayList<String>(one); two = new ArrayList<String>(two); 以避免破坏参数。 - arshajii
@jschoen,也许你希望将签名更改为:public static <T extends Comparable<? super T>> boolean equalLists (List<T> one, List<T> two),这样它就是通用的了。 - Marcus Junius Brutus
one.equals(two)替换为if(one.get(0) == two.get(0) && one.get(one.size()-1) == two.get(one.size()-1)) return true; else return false;会给出更优化的代码吗? - Shivang Doshi
5
函数内的第二个“if”语句可以简化为if(one == null || two == null || one.size() != two.size()) { return false; },因为您已经检查了one和two是否都为空。 - Hugo
显示剩余9条评论

110

Apache Commons Collections再次拯救了我们:

List<String> listA = Arrays.asList("a", "b", "b", "c");
List<String> listB = Arrays.asList("b", "c", "a", "b");
System.out.println(CollectionUtils.isEqualCollection(listA, listB)); // true

List<String> listC = Arrays.asList("a", "b", "c");
List<String> listD = Arrays.asList("a", "b", "c", "c");
System.out.println(CollectionUtils.isEqualCollection(listC, listD)); // false

文档:

org.apache.commons.collections4.CollectionUtils

public static boolean isEqualCollection(java.util.Collection a,
                                        java.util.Collection b)
当且仅当给定的集合以及元素完全相同(包括基数)时,返回`true`。
也就是说,对于每个元素e在集合ab中,a中的e的基数等于b中的e的基数。 参数:
  • a - 第一个集合,不能为空
  • b - 第二个集合,不能为空
返回值:当且仅当两个集合中的元素和基数都相同时,返回`true`。

实现看起来与DiddiZ的答案差不多。 - user227353
好的...但是如果答案是“false”,那么如何获取罪犯(不属于两个列表共同部分的元素)?请参阅我的答案。 - mike rodent
implementation 'org.apache.commons:commons-collections4:4.3' 给我留下了一个错误 Caused by: com.android.builder.dexing.DexArchiveBuilderException: 无法处理 **路径**. - Aliton Oliveira

17
// helper class, so we don't have to do a whole lot of autoboxing
private static class Count {
    public int count = 0;
}

public boolean haveSameElements(final List<String> list1, final List<String> list2) {
    // (list1, list1) is always true
    if (list1 == list2) return true;

    // If either list is null, or the lengths are not equal, they can't possibly match 
    if (list1 == null || list2 == null || list1.size() != list2.size())
        return false;

    // (switch the two checks above if (null, null) should return false)

    Map<String, Count> counts = new HashMap<>();

    // Count the items in list1
    for (String item : list1) {
        if (!counts.containsKey(item)) counts.put(item, new Count());
        counts.get(item).count += 1;
    }

    // Subtract the count of items in list2
    for (String item : list2) {
        // If the map doesn't contain the item here, then this item wasn't in list1
        if (!counts.containsKey(item)) return false;
        counts.get(item).count -= 1;
    }

    // If any count is nonzero at this point, then the two lists don't match
    for (Map.Entry<String, Count> entry : counts.entrySet()) {
        if (entry.getValue().count != 0) return false;
    }

    return true;
}

哇,真的很惊讶发现这个比其他所有解决方案都要快。而且它还支持提前退出。 - DiddiZ
如果count变成负数,这也可能在第二个循环中短路,从而简化循环体为if(!counts.containsKey(item) || --counts.get(item).count < 0) return false;。此外,第三个循环可以简化为for(Count c: counts.values()) if(c.count != 0) return false; - Holger
@Holger 我曾经考虑过类似于第一个方案(在计数器为零时移除它,这将产生相同的效果,但也将在结尾处将检查转换为“返回counts是否为空”),但我不想掩盖主要观点:使用映射基本上将其转变为O(N+M)问题,并且是您可能获得的最大提升。 - cHao
@cHao,谢谢你提供了一个时间复杂度比之前更好的解决方案,应该把功劳归于你。我之所以想到这个方法,是因为最近有类似的可迭代问题。由于我们现在也有了Java 8,值得重新思考它。如果在第二个循环中进行短路操作,当数字变为负数时,第三个循环就变得多余了。此外,避免装箱可能是一把双刃剑,使用包装整数对于大多数用例来说可能更简单和更高效,尤其是通过新的Map.merge方法。请参见此答案... - Holger
与 Holger 的短路技巧一起使用(当计数变为负数时返回),您实际上可以完全省略第三个循环。如果集合具有相同的大小,并且在第二个循环中减去“size times” 1而不会变成负数或找到不在第一个集合中的元素,则所有东西都必须再次为零。当然,这并没有改变复杂性,但它完全消除了创建任何地图内容视图的需求。 - RcCookie

14

我认为这些答案都忽略了一个技巧。

在他的经典著作《Effective Java》中,Bloch在第47条中写道,“知道并使用库”,“简而言之,不要重复造轮子”。他给出了几个非常清晰的原因。

这里有一些答案建议使用Apache Commons Collections库中的CollectionUtils方法,但没有人注意到回答这个问题最美丽、优雅的方式

Collection<Object> culprits = CollectionUtils.disjunction( list1, list2 );
if( ! culprits.isEmpty() ){
  // ... do something with the culprits, i.e. elements which are not common

}
罪魁祸首: 即不共同存在于Lists中的元素。使用CollectionUtils.intersection( list1, culprits )CollectionUtils.intersection( list2, culprits )相对简单地确定哪些罪犯属于list1,哪些属于list2
然而,在类似{ "a", "a", "b" } disjunction with { "a", "b", "b" }的情况下,它往往会崩溃... 但这并不是软件的缺陷,而是所需任务的微妙/模糊本质所固有的。

您可以随时检查源代码 (l.287)以了解此类任务的生成方式,由Apache工程师制作。使用他们的代码的一个好处是,它将经过彻底的尝试和测试,已经预见并处理了许多边缘情况和问题。如果需要,您可以复制和调整此代码。


NB:一开始我很失望,因为CollectionUtils中没有任何一个方法提供重载版本,使您可以强制使用自己的Comparator(以便您可以重新定义equals以适应您的目的)。

但是从collections4 4.0开始,有一个新类Equator,它“确定类型T的对象之间的相等性”。在检查collections4 CollectionUtils.java源代码时,他们似乎正在将其与某些方法一起使用,但据我所知,这不适用于文件顶部使用CardinalityHelper类的方法...其中包括disjunctionintersection

我猜Apache团队还没有开始处理这个问题,因为这是一个非平凡的问题:你需要创建类似于“AbstractEquatingCollection”类,它不使用元素固有的equals和hashCode方法,而是必须使用Equator的方法来实现所有基本的方法,例如add、contains等。值得注意的是,事实上,在查看源代码时,AbstractCollection并没有实现add,其抽象子类(如AbstractSet)也没有;只有像HashSet和ArrayList这样的具体类在实现add。这真是让人头痛。
与此同时,请关注此空间。显然的临时解决方案是将所有元素包装在定制的包装器类中,该类使用equals和hashCode来实现您想要的相等性...然后操作这些包装对象的集合。

此外,有智者说过:“了解依赖的代价”。 - Stanislaw Baranski
@StanislawBaranski 这是一个有趣的评论。这是建议我们不应该过于依赖这些库吗?当你在计算机上使用操作系统时,这已经是一个巨大的信任飞跃了,不是吗?我很高兴使用Apache库,因为我认为它们的质量非常高,并且假设它们的方法符合其“契约”并经过了彻底测试。你会花多少时间开发自己更信任的代码?从开源Apache库中复制代码并进行仔细检查可能值得考虑... - mike rodent

11
如果物品的基数不重要(即:重复元素被视为一个),则有一种方法可以做到这一点而无需进行排序:
```boolean result = new HashSet<> (listA).equals(new HashSet<> (listB));```
这将创建每个List的Set,然后使用HashSet的equals方法进行比较,它会忽略顺序。
如果基数很重要,则必须限制自己使用List提供的设施; 在这种情况下,@jschoen的答案更合适。

如果listA = [a, b, c, c],而listB = [a, b, c],结果将为true,但两个列表并不相等。 - Nikolas

8

将列表转换为Guava的Multiset非常有效。它们不考虑顺序,也会考虑重复元素。

static <T> boolean equalsIgnoreOrder(List<T> a, List<T> b) {
    return ImmutableMultiset.copyOf(a).equals(ImmutableMultiset.copyOf(b));
}

assert equalsIgnoreOrder(ImmutableList.of(3, 1, 2), ImmutableList.of(2, 1, 3));
assert !equalsIgnoreOrder(ImmutableList.of(1), ImmutableList.of(1, 1));

7

这是基于@cHao的解决方案。我进行了几个修复和性能改进。这个解决方案大约比等值排序复制的解决方案快两倍。适用于任何集合类型。空集合和null被视为相等。利用它的优势 ;)

/**
 * Returns if both {@link Collection Collections} contains the same elements, in the same quantities, regardless of order and collection type.
 * <p>
 * Empty collections and {@code null} are regarded as equal.
 */
public static <T> boolean haveSameElements(Collection<T> col1, Collection<T> col2) {
    if (col1 == col2)
        return true;

    // If either list is null, return whether the other is empty
    if (col1 == null)
        return col2.isEmpty();
    if (col2 == null)
        return col1.isEmpty();

    // If lengths are not equal, they can't possibly match
    if (col1.size() != col2.size())
        return false;

    // Helper class, so we don't have to do a whole lot of autoboxing
    class Count
    {
        // Initialize as 1, as we would increment it anyway
        public int count = 1;
    }

    final Map<T, Count> counts = new HashMap<>();

    // Count the items in col1
    for (final T item : col1) {
        final Count count = counts.get(item);
        if (count != null)
            count.count++;
        else
            // If the map doesn't contain the item, put a new count
            counts.put(item, new Count());
    }

    // Subtract the count of items in col2
    for (final T item : col2) {
        final Count count = counts.get(item);
        // If the map doesn't contain the item, or the count is already reduced to 0, the lists are unequal 
        if (count == null || count.count == 0)
            return false;
        count.count--;
    }

    // At this point, both collections are equal.
    // Both have the same length, and for any counter to be unequal to zero, there would have to be an element in col2 which is not in col1, but this is checked in the second loop, as @holger pointed out.
    return true;
}

你可以使用一个总计数器跳过最后的for循环。总计数器将计算每个阶段的计数总和。在第一个for循环中增加总计数器,在第二个for循环中减少它。如果总计数器大于0,则列表不匹配,否则它们匹配。目前,在最终的for循环中,你会检查所有计数是否为零,换句话说,是否所有计数的总和为零。使用总计数器有点颠倒了这种检查,如果计数的总和为零,则返回true,否则返回false。 - SatA
在我看来,值得跳过那个for循环,因为当列表匹配时(最坏情况),for循环会增加另一个不必要的O(n)。 - SatA
@SatA 实际上,您可以在不进行任何替换的情况下删除第三个循环。当一个键不存在或其计数变为负数时,第二个循环已经返回 false。由于两个列表的总大小匹配(这已经在前面检查过),因此在第二个循环之后不可能存在非零值,因为一个键不能存在正值而没有另一个键的负值。 - Holger
@holger 看起来你是完全正确的。据我所知,第三个循环根本不必要。 - SatA
@SatA ...而且使用Java 8,这可以被简洁地实现,就像在这个答案中所示。 - Holger
@Holger 很好的发现。 - DiddiZ

6

如果没有计算机或编程语言的帮助,你会怎么做呢?我给你两个元素列表,你需要告诉我它们是否包含相同的元素。你会如何处理呢?

一种方法是按照上面提到的那样,对列表进行排序,然后逐个比较元素以查看它们是否相等(这就是List.equals所做的)。这意味着你可以修改列表或复制它们,而不知道任务的具体情况,我无法确定哪一个/哪些是允许的。

另一种方法是遍历每个列表,计算每个元素出现的次数。如果两个列表在末尾具有相同的计数,则它们具有相同的元素。代码应该将每个列表转换为一个元素 -> (在列表中出现的次数)映射,然后对两个映射调用equals方法。如果映射是HashMap,则每个转换都是O(N)操作,比较也是如此。这将给您一个相当高效的时间算法,但需要额外的内存开销。


5

我曾经遇到过同样的问题,并想出了一个不同的解决方案。当涉及到重复项时,这个解决方案也可以起作用:

public static boolean equalsWithoutOrder(List<?> fst, List<?> snd){
  if(fst != null && snd != null){
    if(fst.size() == snd.size()){
      // create copied lists so the original list is not modified
      List<?> cfst = new ArrayList<Object>(fst);
      List<?> csnd = new ArrayList<Object>(snd);

      Iterator<?> ifst = cfst.iterator();
      boolean foundEqualObject;
      while( ifst.hasNext() ){
        Iterator<?> isnd = csnd.iterator();
        foundEqualObject = false;
        while( isnd.hasNext() ){
          if( ifst.next().equals(isnd.next()) ){
            ifst.remove();
            isnd.remove();
            foundEqualObject = true;
            break;
          }
        }

        if( !foundEqualObject ){
          // fail early
          break;
        }
      }
      if(cfst.isEmpty()){ //both temporary lists have the same size
        return true;
      }
    }
  }else if( fst == null && snd == null ){
    return true;
  }
  return false;
}

与其他解决方案相比的优势:
  • 复杂度低于O(N²)(尽管我尚未测试与其他答案中的解决方案的实际性能对比);
  • 提前退出;
  • 检查是否为空;
  • 即使涉及重复项也能正常工作:如果您有一个数组[1,2,3,3]和另一个数组[1,2,2,3],大多数解决方案都会告诉您它们在不考虑顺序的情况下是相同的。此解决方案通过从临时列表中删除相等元素来避免这种情况;
  • 使用语义相等性(equals)而不是引用相等性(==);
  • 不对项目进行排序,因此它们不需要按实现Comparable进行排序才能使此解决方案正常工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接