Java ArrayList - como posso saber se duas listas são iguais, a ordem não importa?

133

Eu tenho dois ArrayLists do tipoAnswer (classe criada por você).

Gostaria de comparar as duas listas para ver se elas contêm o mesmo conteúdo, mas sem que a ordem seja importante.

Exemplo:

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

List.equalsafirma que duas listas são iguais se contiverem o mesmo tamanho, conteúdo e ordem dos elementos. Eu quero a mesma coisa, mas sem ordem.

Existe uma maneira simples de fazer isso? Ou precisarei fazer um loop for aninhado e verificar manualmente cada índice das duas listas?

Nota: Não posso alterá-los ArrayListpara outro tipo de lista, eles precisam permanecer assim.

iaacp
fonte
4
ver a resposta para esta pergunta: stackoverflow.com/a/1075699/1133011
David Kroukamp
Veja List.containsAll (lista) em java
Sahil Jain

Respostas:

130

Você pode classificar as duas listas usando Collections.sort()e depois usar o método equals. Uma solução um pouco melhor é verificar primeiro se eles têm o mesmo comprimento antes de fazer o pedido, se não são, então não são iguais, depois classificam e usam iguais. Por exemplo, se você tivesse duas listas de Strings, seria algo como:

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);
}
Jacob Schoen
fonte
20
Lembre-se de não destruir a ordem da lista original (como Collections.sortfaz) - ou seja, passe uma cópia.
precisa saber é o seguinte
@ARS sim, esse é um efeito colateral definido, mas apenas se for importante no caso particular.
21412 Jacob Schoen
3
Você pode adicionar one = new ArrayList<String>(one); two = new ArrayList<String>(two);para evitar arruinar os argumentos.
precisa saber é o seguinte
@jschoen A tentativa de executar Collections.sort () está me dando este erro: Incompatibilidade vinculada: a classificação genérica do método (Lista <T>) do tipo Collections não é aplicável aos argumentos (ArrayList <Responder>). O tipo inferido Resposta não é um substituto válido para o parâmetro delimitado <T estende Comparável <? Super T >>
iaacp
3
A segunda função "if" dentro da função pode ser simplificada, if(one == null || two == null || one.size() != two.size()){ return false; }porque você já está verificando se um e dois são nulos.
Hugo
151

Provavelmente, a maneira mais fácil para qualquer lista seria:

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

fonte
5
Onde está a diversão nisso. Com toda a seriedade, embora esta seja provavelmente a melhor solução.
21412 Jacob Schoen
67
Depende se [a, b, c]e [c, b, a, b]é considerado como tendo o mesmo conteúdo. Essa resposta diria que sim, mas pode ser que, para o OP, eles não (já que um contém uma duplicata e o outro não). Para não falar das questões de eficiência.
usar o seguinte comando
4
aprimoramento com base nos comentários - System.out.println (((l1.size () == l2.size ()) && l2.containsAll (l1) && l1.containsAll (l2)));
Nrj
8
@Nrj, que tal [1,2,2] e [2,1,1]?
ROMANIA_engineer
3
Essa abordagem tem complexidade de O (n ^ 2). Considere duas listas que estão na ordem inversa, por exemplo: [1,2,3] e [3,2,1]. Para o primeiro elemento, ele precisará varrer n elementos, para os segundos elementos n-1 e assim por diante. Portanto, a complexidade será da ordem n ^ 2. Eu acho que a melhor maneira será classificar e depois usar iguais. Ele terá complexidade de O (N * log (n))
puneet
90

Coleções Apache Commons para o resgate mais uma vez:

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

Documentos:

org.apache.commons.collections4.CollectionUtils

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

Retorna truese o dado Collections contém exatamente os mesmos elementos com exatamente as mesmas cardinalidades.

Ou seja, se a cardinalidade de e em a é igual à cardinalidade de e em b , para cada elemento e em a ou b .

Parâmetros:

  • a - a primeira coleção, não deve ser null
  • b - a segunda coleção, não deve ser null

Retorna: truese as coleções contêm os mesmos elementos com as mesmas cardinalidades.

acdcjunior
fonte
A implementação parece mais ou menos semelhante com a resposta do DiddiZ.
user227353
OK ... mas e quanto a se apossar dos culpados (elementos que não são comuns nas duas listas) se a resposta for false? Veja minha resposta.
mike roedor
implementation 'org.apache.commons:commons-collections4:4.3'me deixou com um Caused by: com.android.builder.dexing.DexArchiveBuilderException: Failed to process caminho de erro .
Aliton Oliveira 13/08/19
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
fonte
Uau, fiquei realmente surpreso ao encontrar esse desempenho mais rápido do que todas as outras soluções. E suporta antecipadamente.
DiddiZ
Isto poderia também curto-circuito no segundo ciclo, se counttorna negativo, a simplificação do corpo do laço para if(!counts.containsKey(item) || --counts.get(item).count < 0) return false;Também, o terceiro circuito poderia ser simplificada parafor(Count c: counts.values()) if(c.count != 0) return false;
Holger
@ Holger Eu tinha considerado algo parecido com o primeiro (remover a contagem quando atingir zero, o que teria o mesmo efeito, mas também transformar as verificações no final em "retornar se countsestá vazio"), mas não queria obscurecer o ponto principal: o uso de um mapa basicamente o transforma em um problema de O (N + M) e é o maior aumento que você provavelmente obterá.
cHao 12/06/19
@cHao sim, você merece os créditos por apontar para uma solução com uma complexidade de tempo melhor do que as anteriores. Por acaso pensei nisso, porque há uma pergunta semelhante recente sobre iteráveis. Como agora também temos o Java 8, valeu a pena repensá-lo. Se você fizer um curto-circuito no segundo loop quando o número se tornar negativo, o terceiro loop se tornará obsoleto. Além disso, evitar o boxe pode ser uma faca de dois gumes, com novos Map.mergenúmeros inteiros em caixa pode ser mais simples e mais eficiente para a maioria dos casos de uso. Veja também esta resposta ...
Holger
10

Eu diria que essas respostas perdem um truque.

Bloch, em seu Java efetivo essencial, maravilhoso e conciso , diz, no item 47, o título "Conheça e use as bibliotecas", "Para resumir, não reinvente a roda". E ele dá várias razões muito claras por que não.

Existem algumas respostas aqui que sugerem métodos da CollectionUtilsbiblioteca Apache Commons Collections, mas nenhuma encontrou a maneira mais bonita e elegante de responder a essa pergunta :

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

}

Culpados : ou seja, os elementos que não são comuns a ambos Lists. Determinar a quais culpados pertencem list1e a qual list2é relativamente simples usar CollectionUtils.intersection( list1, culprits )e CollectionUtils.intersection( list2, culprits ).
No entanto, ele tende a desmoronar em casos como {"a", "a", "b"} disjunctioncom {"a", "b", "b"} ... exceto que isso não é uma falha do software, mas inerente à natureza das sutilezas / ambiguidades da tarefa desejada.

Você sempre pode examinar o código fonte (l. 287) para uma tarefa como essa, produzida pelos engenheiros da Apache. Um benefício do uso de seu código é que ele foi testado e testado exaustivamente, com muitos casos extremos e dicas antecipadas e resolvidas. Você pode copiar e ajustar esse código para o conteúdo do seu coração, se necessário.


NB: No começo, fiquei desapontado por nenhum dos CollectionUtilsmétodos fornecer uma versão sobrecarregada, permitindo que você imponha a sua própria Comparator(para que você possa redefinir equalspara atender às suas finalidades).

Mas a partir das coleções4 4.0, há uma nova classe, Equatorque "determina a igualdade entre objetos do tipo T". Ao examinar o código-fonte de collections4 CollectionUtils.java, eles parecem estar usando isso com alguns métodos, mas, pelo que sei, isso não se aplica aos métodos na parte superior do arquivo, usando a CardinalityHelperclasse ... inclua disjunctione intersection.

Suponho que o pessoal do Apache ainda não tenha resolvido isso porque não é trivial: você teria que criar algo como uma classe "AbstractEquatingCollection", que, em vez de usar os métodos equalse hashCodemétodos inerentes aos seus elementos, teria que usá-los de Equatortodos os métodos básicos, como add, containsetc. Nota: quando você olha para o código-fonte, AbstractCollectionnão implementa addnem suas subclasses abstratas como AbstractSet... você precisa esperar até as classes concretas como HashSete ArrayListantes addé implementado. Bastante dor de cabeça.

Enquanto isso, assista a este espaço, suponho. A solução provisória óbvia seria agrupar todos os seus elementos em uma classe de wrapper personalizada que usa equalse hashCodeimplementar o tipo de igualdade que você deseja ... e então manipular Collectionsesses objetos de wrapper.

microfone roedor
fonte
Além disso, alguém sábio disse "Saber o custo da dependência"
Stanislaw Baranski
@StanislawBaranski Esse é um comentário interessante. É uma sugestão de que não se deva depender muito dessas bibliotecas? Quando você usa um sistema operacional em um computador, isso já é um grande salto de fé, não é? A razão de eu estar feliz em usar as bibliotecas Apache é porque eu as considero de alta qualidade e assumo que seus métodos cumprem seu "contrato" e foram testados exaustivamente. Quanto tempo você levaria para desenvolver seu próprio código no qual você confiava mais? Basta copiar o código das bibliotecas Apache open-source e examinando pode ser algo para se pensar ...
mike roedor
8

Se a cardinalidade dos itens não importa (o que significa: elementos repetidos são considerados um), existe uma maneira de fazer isso sem precisar classificar:

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

Isso criará um Setfora de cada um Liste, em seguida, usará HashSeto equalsmétodo que (é claro) desconsidera a ordem.

Se a cardinalidade é importante, você deve limitar-se às instalações fornecidas por List; A resposta de @schoen seria mais adequada nesse caso.

Isaac
fonte
what if listA = [a, b, c, c] e listB = [a, b, c]. O resultado será verdadeiro, mas as listas não são iguais.
precisa
6

Converter as listas no Multiset do Guava funciona muito bem. Eles são comparados independentemente da ordem e os elementos duplicados também são levados em consideração.

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
fonte
6

Isso é baseado na solução @cHao. Incluí várias correções e melhorias de desempenho. Isso funciona aproximadamente duas vezes mais rápido que a solução de cópia ordenada igual. Funciona para qualquer tipo de coleção. Coleções vazias e nulas são consideradas iguais. Use a sua vantagem;)

/**
 * 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
fonte
Você pode pular o loop for final usando um contador de soma. O contador da soma contará o total de contagens em cada estágio. Aumente o contador de soma no primeiro loop for e diminua no segundo loop for. Se o contador da soma for maior que 0, as listas não corresponderão, caso contrário, elas serão. Atualmente, no loop for final, você verifica se todas as contagens são zero ou, em outras palavras, se a soma de todas as contagens é zero. O uso do contador de soma inverte essa verificação, retornando verdadeiro se o total de contagens for zero ou falso, caso contrário.
SATA
Na IMO, vale a pena pular esse loop for, pois quando as listas coincidem (pior cenário), o loop for adiciona outro O (n) desnecessário.
SATA
@SatA, na verdade, você pode remover o terceiro loop sem nenhuma substituição. O segundo loop já retorna falsequando uma chave não existe ou sua contagem se torna negativa. Como o tamanho total de ambas as listas corresponde (isso foi verificado antecipadamente), é impossível ter valores diferentes de zero após o segundo loop, pois não pode haver valores positivos para uma chave sem valores negativos para outra chave.
Holger
Parece que você está absolutamente correto. Até onde eu sei, o terceiro loop não é necessário.
SATA
@SatA… e com o Java 8, isso pode ser implementado de forma concisa, como nesta resposta .
Holger
5

Pense em como você faria isso, sem um computador ou linguagem de programação. Dou a você duas listas de elementos e você precisa me dizer se eles contêm os mesmos elementos. Como você faria?

Uma abordagem, como mencionado acima, é classificar as listas e, em seguida, ir elemento por elemento para ver se são iguais (que é o que List.equalsfaz). Isso significa que você pode modificar as listas ou copiá-las - e sem conhecer a tarefa, não sei se é permitido um ou ambos.

Outra abordagem seria percorrer cada lista, contando quantas vezes cada elemento aparece. Se ambas as listas tiverem as mesmas contagens no final, elas terão os mesmos elementos. O código para isso seria converter cada lista em um mapa elem -> (# of times the elem appears in the list)e, em seguida, chamar equalsos dois mapas. Se os mapas forem HashMap, cada uma dessas traduções é uma operação O (N), assim como a comparação. Isso fornecerá um algoritmo bastante eficiente em termos de tempo, com o custo de memória extra.

yshavit
fonte
5

Eu tive esse mesmo problema e surgiu com uma solução diferente. Este também funciona quando duplicatas estão envolvidas:

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

Vantagens em comparação com outras soluções:

  • complexidade menor que O (N²) (embora eu não tenha testado, seu desempenho é real comparado às soluções em outras respostas aqui);
  • sai cedo;
  • verifica se há nulo;
  • funciona mesmo quando há duplicatas: se você possui uma matriz [1,2,3,3]e outra matriz, a [1,2,2,3]maioria das soluções aqui informa que elas são iguais quando não se considera o pedido. Esta solução evita isso removendo elementos iguais das listas temporárias;
  • usa igualdade semântica ( equals) e não faz referência a igualdade ( ==);
  • não classifica itens, portanto eles não precisam ser classificados (por implement Comparable) para que esta solução funcione.
Chalkos
fonte
3

Se você não deseja classificar as coleções e precisa do resultado de que ["A" "B" "C"] não seja igual a ["B" "B" "A" "C"],

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

não é suficiente, você provavelmente também deve verificar o tamanho:

    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
fonte
2

Solução que aproveita o método de subtração 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
fonte
1

Se você se preocupa com a ordem, basta usar o método equals:

list1.equals(list2)

Se você não se importa com o pedido, use este

Collections.sort(list1);
Collections.sort(list2);      
list1.equals(list2)
Ramesh Kumar
fonte
4
Ele diz que não se importa com a ordem.
Mike rodent
0

Melhor dos dois mundos [@DiddiZ, @Chalkos]: este baseia-se principalmente no método @Chalkos, mas corrige um erro (ifst.next ()) e melhora as verificações iniciais (retiradas do @DiddiZ), além de remover a necessidade de copie a primeira coleção (apenas remove os itens de uma cópia da segunda coleção).

Não exigindo uma função ou classificação de hash e possibilitando a existência precoce da desigualdade, essa é a implementação mais eficiente ainda. Isto é, a menos que você tenha um comprimento de coleção na casa dos milhares ou mais e uma função de hash muito simples.

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
fonte
Se não me engano, a complexidade desse algoritmo em um cenário de valor (em que as listas são iguais, mas classificadas de maneira oposta) seria O (N * N!).
SATA
Na verdade, seria O (N * (N / 2)), pois a cada iteração, o tamanho da matriz diminui.
jazzgil
0

É uma maneira alternativa de verificar a igualdade das listas de matrizes que podem conter valores nulos:

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);
}
Ayaz Alifov
fonte
Qual é o sentido de toda essa cópia entre listas e matrizes?
Holger
0

Minha solução para isso. Não é tão legal, mas funciona bem.

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();
}
Cícero Moura
fonte
-1

Nesse caso, as listas {"a", "b"} e {"b", "a"} são iguais. E {"a", "b"} e {"b", "a", "c"} não são iguais. Se você usar uma lista de objetos complexos, lembre-se de substituir o método equals , pois containsAll o usa dentro.

if (oneList.size() == secondList.size() && oneList.containsAll(secondList)){
        areEqual = true;
}
Andrew
fonte
-1: fornece a resposta incorreta com {"a", "a", "b"} e {"a", "b", "b"}: consulte o código-fonte AbstractCollection.containsAll(). Você tem que permitir ter elementos duplicados, como estamos falando Lists, não sobre Sets. Por favor, veja minha resposta.
mike roedor