Java ArrayList - як я можу визначити, якщо два списки рівні, порядок не має значення?


133

У мене є два ArrayListтипу типу Answer(саморобний клас).

Я хотів би порівняти два списки, щоб побачити, чи містять вони однаковий вміст, але без поваги.

Приклад:

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

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

Чи є простий спосіб це зробити? Або мені потрібно буде зробити вкладений цикл і вручну перевірити кожен індекс обох списків?

Примітка. Я не можу змінити їх ArrayListна інший тип списку, вони повинні залишатися таким.


4
побачити відповідь на це питання: stackoverflow.com/a/1075699/1133011
David Kroukamp

Див. List.containsВсі (список) у java
Sahil Jain

Відповіді:


130

Ви можете сортувати обидва списки за допомогою, Collections.sort()а потім використовувати метод рівних. Трохи кращим рішенням є спочатку перевірити, чи вони однакової довжини перед замовленням, якщо їх немає, то вони не рівні, потім сортувати, а потім використовувати дорівнює. Наприклад, якщо у вас було два списки рядків, це було б щось на зразок:

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це робиться) - тобто передавати копію.
Аршаджі

@ARS так, це певний побічний ефект, але лише якщо це має значення в їх конкретному випадку.
Джейкоб Шоен

3
Ви можете просто додати, one = new ArrayList<String>(one); two = new ArrayList<String>(two);щоб не зруйнувати аргументи.
Аршаджій

@jschoen При спробі зробити Collections.sort () видає мені цю помилку: Невідповідне поєднання: Загальний метод сортування (Список <T>) типу Колекції не застосовується для аргументів (ArrayList <Answer>). Виснований тип Відповідь не є дійсною заміною обмеженого параметра <T розширюється Порівняно <? super T >>
iaacp

3
Другий вислів "if" всередині функції можна спростити як би if(one == null || two == null || one.size() != two.size()){ return false; }тому, що ви вже перевіряєте, чи є один і два недійсними
Hugo

151

Мабуть, найпростішим способом для будь-якого списку буде:

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

5
Де в цьому веселощі. З усією серйозністю, хоча це, мабуть, краще рішення.
Джейкоб Шоен

67
Залежить від того, чи є [a, b, c]і [c, b, a, b]чи вважається такий самий зміст. Ця відповідь скаже, що вони є, але це може бути, що для ОП вони цього не роблять (оскільки один містить дублікат, а другий - ні). Нічого не сказати про питання ефективності.
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

90

Колекції 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

Документи:

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якщо колекції містять однакові елементи з однаковими кардиналіями.


Реалізація видається більш-менш схожою з відповіддю DiddiZ.
користувач227353

Гаразд ... а як бути з винуватцями винних (елементи, які не є спільними для двох списків), якщо відповідь є 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

Це також може мати коротке замикання у другому циклі, якщо воно countстане негативним, спрощуючи корпус циклу до if(!counts.containsKey(item) || --counts.get(item).count < 0) return false;Також, третю петлю можна спростити до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, але жоден не знайшов найкрасивішого, елегантного способу відповісти на це питання :

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"} ... за винятком цього не є збоєм програмного забезпечення, але притаманні природі тонкощів / неоднозначності бажаного завдання.

Ви завжди можете вивчити вихідний код (л. 287) для подібного завдання, виробленого інженерами Apache. Однією з переваг використання їх коду є те, що він буде ретельно випробуваний і протестований, з багатьма крайніми справами та готчами передбачається і вирішується. Ви можете скопіювати і підкоригувати цей код до вмісту серця, якщо це буде потрібно.


NB: Спочатку я був розчарований, що жоден із CollectionUtilsметодів не забезпечує перевантажену версію, що дозволяє вам нав'язувати своє Comparator(щоб ви могли переосмислитись equalsвідповідно до своїх цілей).

Але з collection4 4.0 з'явився новий клас, Equatorякий "визначає рівність між об'єктами типу T". Перевіряючи вихідний код collection4 CollectionUtils.java, вони, здається, використовують це деякими методами, але, наскільки я можу це зробити, це не стосується методів у верхній частині файлу, використовуючи CardinalityHelperклас ... який включати disjunctionі intersection.

Я гадаю, що люди Apache ще не вирішили цього питання, оскільки це нетривіально: вам доведеться створити щось на кшталт класу "AbstractEquatingCollection", який замість того, щоб використовувати притаманні його елементи equalsта hashCodeметоди, замість цього повинен був би використовувати ці з 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));

6

Це засновано на рішенні @cHao. Я включив кілька виправлень та покращення продуктивності. Це працює приблизно вдвічі швидше рішення, рівне замовленому копіюванню. Працює для будь-якого типу колекції. Порожні колекції та нуль вважаються рівними. Використовуйте на вашу користь;)

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

Ви можете пропустити остаточний цикл за допомогою лічильника сум. Лічильник сум підраховує загальну кількість підрахунків на кожному етапі. Збільшити лічильник підсумків у першому циклі та зменшити його у другому для циклу. Якщо лічильник сум більший за 0, списки не відповідають, інакше вони є. На даний момент у фінальній для циклу ви перевіряєте, чи всі підрахунки дорівнюють нулю або іншими словами, чи сума всіх підрахунків дорівнює нулю. Використовуючи вид лічильника суми, цей чек повертає, повертаючи істину, якщо загальна кількість підрахунків дорівнює нулю, або помилково неправильно.
СБА

IMO, варто пропустити цей цикл for, оскільки коли списки збігаються (найгірший сценарій) for-loop додає ще одне непотрібне O (n).
СБА

@SatA насправді ви можете видалити 3-й цикл без будь-якої заміни. Другий цикл вже повертається, falseколи ключ не існує або його кількість стає негативним. Оскільки загальний розмір обох списків збігається (це було перевірено наперед), неможливо мати ненульові значення після другого циклу, оскільки не може бути позитивних значень для ключа без негативних значень для іншого ключа.
Холгер

@holger, здається, ви абсолютно праві. Наскільки я можу сказати, 3-я петля зовсім не потрібна.
СБА

@SatA… і з Java 8 це можна реалізувати стисло, як у цій відповіді .
Холгер

5

Подумайте, як би ви це зробили самостійно, відсутній комп’ютер або мова програмування. Я даю вам два списки елементів, і ви повинні сказати мені, чи містять вони однакові елементи. Як би ти це зробив?

Один із підходів, як згадувалося вище, полягає в сортуванні списків, а потім переходимо по елементам, щоб побачити, чи рівні вони (що і List.equalsробить). Це означає, що вам дозволяється змінювати списки, або вам дозволяється копіювати їх - і не знаючи призначення, я не можу знати, чи дозволено те чи інше.

Іншим підходом було б пройти кожен список, порахувавши, скільки разів кожен елемент з’являється. Якщо обидва списки мають однакові підрахунки в кінці, вони мають однакові елементи. Кодом для цього було б перекласти кожен список на карту, elem -> (# of times the elem appears in the list)а потім зателефонувати 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), а не референтну рівність ( ==);
  • не сортує 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

Якщо ви дбаєте про порядок, тоді просто використовуйте метод рівних:

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

Якщо я не помиляюся, складність цього алгоритму в сценарії вартісного випадку (коли списки рівні, але відсортовані протилежно) буде O (N * N!).
СБА

Власне, це було б O (N * (N / 2)), так як з кожною ітерацією розмір масиву зменшується.
джазгіл

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

У чому сенс всього цього копіювання між списками та масивами?
Холгер

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"} не рівні. Якщо ви використовуєте список складних об'єктів, не забудьте переозначити метод рівних , оскільки містить усьому, що використовує його всередині.

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

-1: дає неправильну відповідь за допомогою {"a", "a", "b"} і {"a", "b", "b"}: перевірити вихідний код для AbstractCollection.containsAll(). Ви повинні дозволити наявність дублюючих елементів, як ми говоримо Lists, а не про Sets. Будь ласка, дивіться мою відповідь.
мійський гризун
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.