Java ArrayList - как я могу определить, равны ли два списка, порядок не имеет значения?

133

Имею два ArrayListтипа Answer(самодельный класс).

Я хотел бы сравнить два списка, чтобы увидеть, содержат ли они одинаковое содержимое, но без учета порядка.

Пример:

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

List.equalsзаявляет, что два списка равны, если они содержат одинаковый размер, содержимое и порядок элементов. Я хочу то же самое, но без всякого порядка.

Есть простой способ сделать это? Или мне нужно будет выполнить вложенный цикл for и вручную проверить каждый индекс обоих списков?

Примечание: я не могу изменить их ArrayListна список другого типа, они должны оставаться такими.

iaacp
источник
4
см. ответ на этот вопрос: stackoverflow.com/a/1075699/1133011
Дэвид Кроукамп,
См. List.containsAll (список) в java
Сахил Джайн

Ответы:

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
@ARS да, это явный побочный эффект, но только если он имеет значение в их конкретном случае.
Jacob Schoen
3
Вы можете просто добавить, one = new ArrayList<String>(one); two = new ArrayList<String>(two);чтобы не испортить аргументы.
arshajii
@jschoen Попытка выполнить Collections.sort () дает мне эту ошибку: Несоответствие привязки: общий метод sort (List <T>) типа Collections не применим для аргументов (ArrayList <Answer>). Выведенный тип Answer не является допустимой заменой ограниченного параметра <T extends Comparable <? super T >>
iaacp
3
Второй оператор «if» внутри функции можно упростить, так if(one == null || two == null || one.size() != two.size()){ return false; }как вы уже проверяете, равны ли один и два значения null
Хьюго
151

Вероятно, самый простой способ составить любой список:

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

источник
5
Где в этом веселье. Если серьезно, то это, вероятно, лучшее решение.
Jacob Schoen
67
Зависит от того , считается ли [a, b, c]и [c, b, a, b]имеют одинаковое содержимое. В этом ответе говорится, что они есть, но может быть, что для OP они этого не делают (поскольку один содержит дубликат, а другой нет). Не говоря уже о вопросах эффективности.
yshavit
4
улучшение на основе комментариев - System.out.println (((l1.size () == l2.size ()) && l2.containsAll (l1) && l1.containsAll (l2)));
Nrj
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. Я думаю, что лучше будет отсортировать, а затем использовать равные. Он будет иметь сложность O (n * log (n))
puneet
91

Коллекции Apache Commons снова приходят на помощь:

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

Docs:

org.apache.commons.collections4.CollectionUtils

public static boolean isEqualCollection(java.util.Collection a,
                                        java.util.Collection b)

Возвращает, trueесли заданные Collections содержат точно такие же элементы с точно такими же мощностями.

То есть, если и только если мощность e в a равна мощности e в b , для каждого элемента e в a или b .

Параметры:

  • a - первая коллекция, не должно быть null
  • b - второй сборник не должен быть null

Возвращает: trueесли коллекции содержат одинаковые элементы с одинаковой мощностью.

acdcjunior
источник
Реализация кажется более или менее похожей на ответ DiddiZ.
user227353
Хорошо ... но как насчет того, чтобы найти виновных (элементы, которые не являются общими для двух списков), если ответ будет false? Смотрите мой ответ.
Майк грызун
implementation 'org.apache.commons:commons-collections4:4.3'оставил мне Caused by: com.android.builder.dexing.DexArchiveBuilderException: Failed to process путь к ошибке .
Aliton Oliveira
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;
}
Chao
источник
Вау, был очень удивлен, обнаружив, что это работает быстрее, чем все другие решения. И он поддерживает ранний выход.
DiddiZ 01
Это также может привести к короткому замыканию во втором цикле, если countстанет отрицательным, что упростит тело цикла до. if(!counts.containsKey(item) || --counts.get(item).count < 0) return false;Кроме того, 3-й цикл можно упростить доfor(Count c: counts.values()) if(c.count != 0) return false;
Холгер
@Holger Я подумал о чем-то похожем на первый (удаление счетчика, когда он достигнет нуля, что будет иметь тот же эффект, но также превратит проверки в конце в «вернуть, countsявляется ли пустым»), но не хотел скрывать главный момент: использование карты в основном превращает это в проблему O (N + M) и является самым большим преимуществом, которое вы, вероятно, получите.
cHao
@cHao да, вы заслуживаете похвалы за то, что указали на решение с лучшей временной сложностью, чем предыдущие. Я просто подумал об этом, потому что недавно возник аналогичный вопрос относительно итераций. Поскольку теперь у нас также есть Java 8, стоило переосмыслить ее. Если вы закоротите второй цикл, когда число станет отрицательным, третий цикл станет устаревшим. Кроме того, отказ от бокса может быть палкой о двух концах, Map.mergeпоскольку в большинстве случаев использование целых чисел в коробке может быть проще и эффективнее. См. Также этот ответ
Хольгер
10

Я бы сказал, что эти ответы упускают один трюк.

Блох в своей замечательной и краткой « Эффективной Java» в пункте 47, озаглавленном «Знайте и используйте библиотеки», говорит: «Подводя итог, не изобретайте велосипед». И он приводит несколько очень понятных причин, почему нет.

Здесь есть несколько ответов, которые предлагают методы из CollectionUtilsбиблиотеки Apache 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. Одно из преимуществ использования их кода состоит в том, что он будет тщательно опробован и протестирован, с ожидаемыми и устраненными множеством крайних случаев и ошибок. Вы можете скопировать и настроить этот код по своему усмотрению, если потребуется.


NB. Сначала я был разочарован тем, что ни один из CollectionUtilsметодов не предоставляет перегруженную версию, позволяющую вам навязывать свою собственную Comparator(чтобы вы могли переопределить equalsв соответствии со своими целями).

Но из collections4 4.0 появился новый класс, Equatorкоторый «определяет равенство между объектами типа T». При изучении исходного кода collections4 CollectionUtils.java они, похоже, используют это с некоторыми методами, но, насколько я могу понять, это не применимо к методам в верхней части файла, использующим CardinalityHelperкласс ... который включить disjunctionи intersection.

Я предполагаю , что люди , Apache не удосужились это еще и потому , что это нетривиально: вы должны создать что - то вроде класса «AbstractEquatingCollection», который вместо того , чтобы использовать присущие его элементы equalsи hashCodeметоды вместо этого должен использовать те of Equatorдля всех основных методов, таких как add, containsи т. д. NB на самом деле, когда вы смотрите на исходный код, AbstractCollectionне реализует и не реализует addсвои абстрактные подклассы, такие как AbstractSet..., вам нужно дождаться, пока конкретные классы, такие как HashSetи ArrayListдо addреализовано. Довольно головная боль.

А пока, полагаю, понаблюдайте за этим пространством. Очевидным промежуточным решением было бы обернуть все ваши элементы в специальный класс-оболочку, который использует equalsи hashCodeреализует тот вид равенства, который вы хотите ... затем манипулируйте Collectionsэтими объектами-оболочками.

Майк грызун
источник
Кроме того, кто-то мудрый сказал: «Знайте цену зависимости»
Станислав Барански
@StanislawBaranski Это интересный комментарий. Это предложение не слишком зависеть от таких библиотек? Когда вы используете ОС на компьютере, это уже большой прыжок веры, не так ли? Причина, по которой я счастлив использовать библиотеки Apache, заключается в том, что я считаю их действительно очень качественными и предполагаю, что их методы соответствуют их «контракту» и были тщательно протестированы. Сколько времени вы бы потратили на разработку собственного кода, которому вы больше доверяли? Возможно, стоит подумать о копировании кода из библиотек Apache с открытым исходным кодом и его тщательном
Майк грызун
8

Если количество элементов не имеет значения (что означает: повторяющиеся элементы рассматриваются как один), то есть способ сделать это без сортировки:

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

Это позволит создать Setиз каждого List, а затем использовать HashSet«s equalsметод , который (конечно) пренебрегает заказа.

Если количество элементов имеет значение, вы должны ограничиться возможностями, предоставляемыми List; Ответ @jschoen в этом случае был бы более подходящим.

Исаак
источник
что, если listA = [a, b, c, c] и listB = [a, b, c]. результат будет истинным, но списки не равны.
Николас
6

Преобразование списков для гуавы в 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));
Natix
источник
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;
}
DiddiZ
источник
Вы можете пропустить последний цикл for, используя счетчик сумм. Счетчик сумм будет считать общее количество отсчетов на каждом этапе. Увеличьте счетчик суммы в первом цикле for и уменьшите его во втором цикле for. Если счетчик сумм больше 0, списки не совпадают, в противном случае - совпадают. В настоящее время в последнем цикле for вы проверяете, все ли счетчики равны нулю или, другими словами, если сумма всех счетчиков равна нулю. Использование счетчика сумм отменяет эту проверку, возвращая истину, если сумма подсчетов равна нулю, или ложь в противном случае.
Сб, 01
IMO, стоит пропустить этот цикл for, поскольку, когда списки совпадают (наихудший сценарий), цикл for добавляет еще один ненужный O (n).
Сб, 01
@SatA на самом деле, вы можете удалить 3-й цикл без какой-либо замены. Второй цикл уже возвращается, falseкогда ключ не существует или его счетчик становится отрицательным. Поскольку общий размер обоих списков совпадает (это было проверено заранее), невозможно иметь ненулевые значения после второго цикла, так как не может быть положительных значений для ключа без отрицательных значений для другого ключа.
Holger
@holger, кажется, ты абсолютно прав. Насколько я могу судить, 3-й цикл вообще не нужен.
Сб.
@SatA… а с Java 8 это можно реализовать кратко, как в этом ответе .
Holger
5

Подумайте, как бы вы сделали это сами, не имея компьютера или языка программирования. Я даю вам два списка элементов, и вы должны сказать мне, содержат ли они одинаковые элементы. Как бы ты это сделал?

Один из подходов, как упоминалось выше, состоит в том, чтобы отсортировать списки, а затем переходить от одного элемента к другому, чтобы проверить, равны ли они (что и List.equalsпроисходит). Это означает, что либо вам разрешено изменять списки, либо вам разрешено их копировать - и, не зная назначения, я не могу знать, разрешено ли одно или оба из них.

Другой подход - просмотреть каждый список, подсчитывая, сколько раз появляется каждый элемент. Если оба списка имеют одинаковое количество в конце, они имеют одинаковые элементы. Код для этого должен был бы преобразовать каждый список в карту, elem -> (# of times the elem appears in the list)а затем вызвать equalsдве карты. Если карты есть HashMap, каждый из этих переводов является операцией O (N), как и сравнение. Это даст вам довольно эффективный с точки зрения времени алгоритм за счет дополнительной памяти.

yshavit
источник
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), чтобы это решение работало.
Chalkos
источник
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);
}
Jaskey
источник
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;
  }
}
maxdanilkin
источник
1

Если вам важен порядок, просто используйте метод equals:

list1.equals(list2)

Если вас не волнует порядок, используйте это

Collections.sort(list1);
Collections.sort(list2);      
list1.equals(list2)
Рамеш Кумар
источник
4
Он говорит, что его не волнует порядок.
Майк грызун
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;
}
jazzgil
источник
Если я не ошибаюсь, сложность этого алгоритма в сценарии стоящего случая (где списки равны, но отсортированы противоположным образом) будет O (N * N!).
Сб. 05
На самом деле это было бы 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. Пожалуйста, посмотрите мой ответ.
Майк грызун