Java ArrayList-如何判断两个列表是否相等,顺序无关紧要?


133

我有两个ArrayListS型Answer(自制课程)。

我想比较两个列表,看看它们是否包含相同的内容,但顺序无关紧要。

例:

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

List.equals声明两个列表包含相同的大小,内容和元素顺序,则相等。我想要相同的东西,但顺序无关紧要。

有没有简单的方法可以做到这一点?还是我需要做一个嵌套的for循环,并手动检查两个列表的每个索引?

注意:我无法将它们从ArrayList其他类型的列表中更改,它们需要保持不变。



参见Java中的List.containsAll(list)
Sahil Jain

Answers:


130

您可以使用Collections.sort(),然后使用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);
}

20
只要记住不要破坏原始列表的顺序即可(Collections.sort即传递副本)。
arshajii 2012年

@ARS是的,这是肯定的副作用,但仅在特定情况下才重要。
Jacob Schoen 2012年

3
您可以添加one = new ArrayList<String>(one); two = new ArrayList<String>(two);以避免破坏参数。
arshajii 2012年

@jschoen尝试执行Collections.sort()时出现此错误:绑定不匹配:Collections类型的通用方法sort(List <T>)不适用于参数(ArrayList <Answer>)。推断的类型Answer不是有效替代边界参数<T extended Comparable <?超级T >>
iaacp 2012年

3
函数内部的第二个“ if”语句可以简化为,if(one == null || two == null || one.size() != two.size()){ return false; }因为您已经在检查一个和两个是否均为空
Hugo

150

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

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

5
有趣的地方在哪里。严格地说,这可能是更好的解决方案。
雅各布·斯科恩

67
取决于是否[a, b, c][c, b, a, b]拥有相同的内容。这个答案会说他们这样做了,但是对于OP来说可能没有(因为一个包含重复项,而另一个不包含重复项)。更不用说效率问题了。
yshavit 2012年

4
基于注释的增强-System.out.println((((l1.size()== l2.size())&& l2.containsAll(l1)&& l1.containsAll(l2)));
Nrj 2015年

8
@Nrj,[1,2,2]和[2,1,1]呢?
ROMANIA_engineer

3
这种方法的复杂度为O(n ^ 2)。考虑两个相反的列表,例如:[1,2,3]和[3,2,1]。对于第一个元素,它将必须扫描n个元素,对于第二个n-1个元素,依此类推。因此,复杂度约为n ^ 2。我认为更好的方法是先排序然后再使用equals。它将具有O(n * log(n))的复杂度
puneet

90

再次拯救 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如果给定Collections包含具有完全相同基数的完全相同元素,则 返回。

也就是说,当且仅当的基数Ë一个等于的基数ëb,对于每个元件é一个b

参数:

  • a -第一个集合,一定不能 null
  • b -第二个集合,不得为 null

返回: true当集合包含具有相同基数的相同元素时。


该实现似乎与DiddiZ的回答大致相似。
user227353

好的...但是,如果答案是肯定的,那么如何抓住罪魁祸首(两个列表中不常见的元素)false呢?看我的答案。
麦克啮齿动物

implementation 'org.apache.commons:commons-collections4:4.3'使我走错了Caused by: com.android.builder.dexing.DexArchiveBuilderException: Failed to process
阿里顿·奥利维拉

13
// 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 2013年

这也可能会在第二个回路中短路(如果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,因此值得重新思考。如果在数字变为负数时在第二个循环中短路,则第三个循环将过时。此外,避免装箱可能是一把双刃剑,对于new而言Map.merge,使用装箱的整数在大多数情况下可能更简单,更有效。另请参阅此答案 ……
Holger

10

我会说这些答案错过了一个窍门。

Bloch在他的必不可少的,精妙的,简洁的有效Java中说,在项目47中,标题为“了解和使用库”,“总而言之,不要重新发明轮子”。他给出了几个非常清楚的理由说明为什么不这样做。

这里有一些答案建议使用CollectionUtilsApache Commons Collections库中的方法,但没有一个发现最美丽,优雅的方法来回答这个问题

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

}

罪魁祸首:即两者都不共有的元素Lists。确定哪些罪犯属于list1,哪些list2是相对简单的使用CollectionUtils.intersection( list1, culprits )CollectionUtils.intersection( list2, culprits )
但是,在诸如{“ a”,“ a”,“ b”} disjunction和{“ a”,“ b”,“ b”}的情况下,它趋于崩溃……除非这不是软件故障,而是所需任务的微妙/模糊性的本质所固有的。

您始终可以检查源代码(l。287)以完成由Apache工程师生产的此类任务。使用他们的代码的好处之一是,将对它进行彻底的尝试和测试,并预期和处理许多边缘情况和陷阱。如果需要,您可以将此代码复制并调整到您的内心。


注意:最初,我很失望,没有一种CollectionUtils方法提供重载版本,使您可以强制使用自己的版本Comparator(因此您可以重新定义equals以适合您的目的)。

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

我猜想Apache人还没有解决这个问题,因为它是不平凡的:您将不得不创建类似“ AbstractEquatingCollection”类的类,该类不使用其元素的内在equalshashCode方法,而必须使用那些的Equator所有基本方法,如addcontains等NB其实当你看到的源代码,AbstractCollection不执行add,也没有它的抽象子类,如AbstractSet...你必须要等到具体的类,如HashSetArrayListadd被实施。很头疼。

同时,我想同时观察这个空间。显而易见的临时解决方案是将您的所有元素包装在一个定制的包装器类中,该类使用equalshashCode实现您想要的等式...然后操纵Collections这些包装器对象。


同样,有人明智地说“知道依赖的代价”
Stanislaw Baranski '18

@StanislawBaranski这是一个有趣的评论。是否建议人们不要太依赖此类库?当您在已经有巨大信心的计算机上使用操作系统时,不是吗?我之所以乐于使用Apache库,是因为我认为它们确实具有很高的质量,并假定它们的方法符合其“合同”并经过了全面测试。您将花费多少时间来开发自己的代码,而您更信任该代码?从开放源代码的Apache库复制代码并仔细检查它可能是要考虑的事情……
mike rodent

8

如果项目的基数无关紧要(意味着:重复的元素被视为一个),那么有一种方法可以不必进行排序:

boolean result = new HashSet<>(listA).equals(new HashSet<>(listB));

这将创建一个Setout of each List,然后使用HashSetequals方法(当然)而无需理会排序。

如果基数很重要,那么您必须将自己局限于List; 提供的功能。在这种情况下,@ jschoen的答案会更合适。


如果listA = [a,b,c,c],而listB = [a,b,c],该怎么办。结果将为true,但列表不等于。
Nikolas

6

将列表转换为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));

6

这基于@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。
星期六

IMO,值得跳过该for循环,因为当列表确实匹配(最坏的情况)时,for循环会添加另一个不必要的O(n)。
星期六

@SatA实际上,您可以删除第三个循环而无需任何替换。false当某个键不存在或其计数变为负数时,第二个循环确实已经返回。由于两个列表的总大小匹配(已预先检查),因此在第二个循环之后不可能有非零值,因为一个键不能有正值,而另一个键不能有负值。
Holger

@holger看来您绝对正确。据我所知,根本不需要第三循环。
星期六

@SatA…以及Java 8,可以像此答案中那样简洁地实现。
Holger

5

想一想在没有计算机或编程语言的情况下您将如何做自己。我给您两个元素列表,您必须告诉我它们是否包含相同的元素。你会怎么做?

如上所述,一种方法是对列表进行排序,然后逐个元素查看它们是否相等(这是正确的List.equals)。这意味着您要么被允许修改列表,要么被允许复制它们-并且在不知道分配的情况下,我不知道是否允许这两个列表中的一个。

另一种方法是遍历每个列表,计算每个元素出现的次数。如果两个列表的末尾计数相同,则它们具有相同的元素。这样做的代码是将每个列表转换为的映射,elem -> (# of times the elem appears in the list)然后equals在两个映射上调用。如果map是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)而不是引用相等(==);
  • 不会对itens进行排序,因此它们不需要可排序(按implement Comparable)即可运行此解决方案。

3

如果您不希望对集合进行排序,并且需要[[A]“ B”“ C”]不等于[“ B”“ B”“ A”“ C”]的结果,

l1.containsAll(l2)&&l2.containsAll(l1)

还不够,您可能还需要检查大小:

    List<String> l1 =Arrays.asList("A","A","B","C");
    List<String> l2 =Arrays.asList("A","B","C");
    List<String> l3 =Arrays.asList("A","B","C");

    System.out.println(l1.containsAll(l2)&&l2.containsAll(l1));//cautions, this will be true
    System.out.println(isListEqualsWithoutOrder(l1,l2));//false as expected

    System.out.println(l3.containsAll(l2)&&l2.containsAll(l3));//true as expected
    System.out.println(isListEqualsWithoutOrder(l2,l3));//true as expected


    public static boolean isListEqualsWithoutOrder(List<String> l1, List<String> l2) {
        return l1.size()==l2.size() && l1.containsAll(l2)&&l2.containsAll(l1);
}

2

利用CollectionUtils减法的解决方案:

import static org.apache.commons.collections15.CollectionUtils.subtract;

public class CollectionUtils {
  static public <T> boolean equals(Collection<? extends T> a, Collection<? extends T> b) {
    if (a == null && b == null)
      return true;
    if (a == null || b == null || a.size() != b.size())
      return false;
    return subtract(a, b).size() == 0 && subtract(a, b).size() == 0;
  }
}

1

如果您关心顺序,则只需使用equals方法:

list1.equals(list2)

如果您不在乎订单,请使用此

Collections.sort(list1);
Collections.sort(list2);      
list1.equals(list2)

4
他说,他并不关心顺序。
mike啮齿动物

0

两全其美[@DiddiZ,@Chalkos]:这个主要基于@Chalkos方法,但修复了一个错误(ifst.next()),并改进了初始检查(取自@DiddiZ)并消除了对复制第一个集合(只需从第二个集合的副本中删除项目)。

不需要哈希函数或排序,并且可以在不平等的情况下尽早存在,这是迄今为止最有效的实现。除非您具有数千或更多的收集长度,并且具有非常简单的哈希函数。

public static <T> boolean isCollectionMatch(Collection<T> one, Collection<T> two) {
    if (one == two)
        return true;

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

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

    // copy the second list, so it can be modified
    final List<T> ctwo = new ArrayList<>(two);

    for (T itm : one) {
        Iterator<T> it = ctwo.iterator();
        boolean gotEq = false;
        while (it.hasNext()) {
            if (itm.equals(it.next())) {
                it.remove();
                gotEq = true;
                break;
            }
        }
        if (!gotEq) return false;
    }
    // All elements in one were found in two, and they're the same size.
    return true;
}

如果我没有记错的话,那么在一个有价值的情况下(列表相等但以相反的方式排序),该算法的复杂度将为O(N * N!)。
星期六

实际上,它将是O(N *(N / 2)),因为每次迭代,数组大小都会减小。
jazzgil

0

这是检查是否可以包含空值的数组列表是否相等的另一种方法:

List listA = Arrays.asList(null, "b", "c");
List listB = Arrays.asList("b", "c", null);

System.out.println(checkEquality(listA, listB)); // will return TRUE


private List<String> getSortedArrayList(List<String> arrayList)
{
    String[] array = arrayList.toArray(new String[arrayList.size()]);

    Arrays.sort(array, new Comparator<String>()
    {
        @Override
        public int compare(String o1, String o2)
        {
            if (o1 == null && o2 == null)
            {
                return 0;
            }
            if (o1 == null)
            {
                return 1;
            }
            if (o2 == null)
            {
                return -1;
            }
            return o1.compareTo(o2);
        }
    });

    return new ArrayList(Arrays.asList(array));
}

private Boolean checkEquality(List<String> listA, List<String> listB)
{
    listA = getSortedArrayList(listA);
    listB = getSortedArrayList(listB);

    String[] arrayA = listA.toArray(new String[listA.size()]);
    String[] arrayB = listB.toArray(new String[listB.size()]);

    return Arrays.deepEquals(arrayA, arrayB);
}

在列表和数组之间进行所有复制的目的是什么?
Holger

0

我的解决方案。它不是很酷,但是效果很好。

public static boolean isEqualCollection(List<?> a, List<?> b) {

    if (a == null || b == null) {
        throw new NullPointerException("The list a and b must be not null.");
    }

    if (a.size() != b.size()) {
        return false;
    }

    List<?> bCopy = new ArrayList<Object>(b);

    for (int i = 0; i < a.size(); i++) {

        for (int j = 0; j < bCopy.size(); j++) {
            if (a.get(i).equals(bCopy.get(j))) {
                bCopy.remove(j);
                break;
            }
        }
    }

    return bCopy.isEmpty();
}

-1

在这种情况下,列表{“ a”,“ b”}和{“ b”,“ a”}相等。并且{“ a”,“ b”}和{“ b”,“ a”,“ c”}不相等。如果使用复杂对象列表,请记住重写equals方法,因为containsAll在内部使用了它。

if (oneList.size() == secondList.size() && oneList.containsAll(secondList)){
        areEqual = true;
}

-1:使用{“ a”,“ a”,“ b”}和{“ a”,“ b”,“ b”}给出错误答案:签出源代码AbstractCollection.containsAll()。您必须允许我们在谈论Lists而不是在重复元素Sets。请看我的回答。
麦克罗丹(Mike
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.