Java ArrayList: ¿cómo puedo saber si dos listas son iguales, sin importar el orden?


133

Tengo dos ArrayLists de tipo Answer(clase propia).

Me gustaría comparar las dos listas para ver si contienen el mismo contenido, pero sin importar el orden.

Ejemplo:

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

List.equalsestablece que dos listas son iguales si contienen el mismo tamaño, contenido y orden de elementos. Quiero lo mismo, pero sin importar el orden.

¿Hay una manera simple de hacer esto? ¿O tendré que hacer un bucle anidado para y verificar manualmente cada índice de ambas listas?

Nota: No puedo cambiarlos ArrayLista otro tipo de lista, deben permanecer así.


44
vea la respuesta a esta pregunta: stackoverflow.com/a/1075699/1133011
David Kroukamp

Ver List.containsAll (list) en java
Sahil Jain

Respuestas:


130

Puedes ordenar ambas listas usando Collections.sort() y luego usar el método igual. Una solución ligeramente mejor es verificar primero si tienen la misma longitud antes de ordenar, si no lo son, entonces no son iguales, luego ordenar y luego usar iguales. Por ejemplo, si tuviera dos listas de cadenas, sería algo así 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);
}

20
Solo recuerde no destruir el orden de la lista original (como lo Collections.sorthace), es decir, pasar una copia.
arshajii

@ARS sí, ese es un efecto secundario definitivo, pero solo si es importante en su caso particular.
Jacob Schoen el

3
Simplemente puede agregar one = new ArrayList<String>(one); two = new ArrayList<String>(two);para evitar arruinar los argumentos.
arshajii

@jschoen Intentar hacer Collections.sort () me está dando este error: No coinciden los límites: El método genérico de clasificación (Lista <T>) de tipo Colecciones no es aplicable para los argumentos (ArrayList <Answer>). El tipo inferido Respuesta no es un sustituto válido para el parámetro acotado <T extiende Comparable <? super T >>
iaacp

3
La segunda instrucción "if" dentro de la función se puede simplificar if(one == null || two == null || one.size() != two.size()){ return false; }porque ya está comprobando si tanto uno como dos son nulos
Hugo

151

Probablemente la forma más fácil para cualquier lista sería:

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

55
Dónde está la diversión en eso. Con toda seriedad, esta es probablemente la mejor solución.
Jacob Schoen el

67
Depende de si [a, b, c]y [c, b, a, b]se considera que tienen el mismo contenido. Esta respuesta diría que sí, pero podría ser que para el OP no lo hagan (ya que uno contiene un duplicado y el otro no). Por no hablar de los problemas de eficiencia.
yshavit

44
mejora basada en comentarios: System.out.println (((l1.size () == l2.size ()) && l2.containsAll (l1) && l1.containsAll (l2)));
Nrj

8
@Nrj, ¿qué pasa con [1,2,2] y [2,1,1]?
RUMANIA_engineer

3
Este enfoque tiene una complejidad de O (n ^ 2). Considere dos listas que están en orden inverso, por ejemplo: [1,2,3] y [3,2,1]. Para el primer elemento tendrá que escanear n elementos, para el segundo elemento n-1 y así sucesivamente. Entonces la complejidad será de orden n ^ 2. Creo que la mejor manera será ordenar y luego usar iguales. Tendrá complejidad de O (n * log (n))
juegue el

90

Colecciones de Apache Commons al rescate una vez más:

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)

Devuelve trueiff el dadoCollection s contienen exactamente los mismos elementos con exactamente las mismas cardinalidades.

Es decir, si la cardinalidad de e en a es igual a la cardinalidad de e en b , para cada elemento e en a o b .

Parámetros:

  • a - la primera colección, no debe ser null
  • b - la segunda colección, no debe ser null

Devuelve: truesi las colecciones contienen los mismos elementos con las mismas cardinalidades.


La implementación parece más o menos similar con la respuesta de DiddiZ.
user227353

De acuerdo ... pero ¿qué pasa con la búsqueda de los culpables (elementos que no son comunes en las dos listas) si la respuesta es false? Mira mi respuesta.
Mike roedor

implementation 'org.apache.commons:commons-collections4:4.3'me ha dejado con una Caused by: com.android.builder.dexing.DexArchiveBuilderException: Failed to process ruta de error .
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;
}

Wow, me sorprendió mucho ver que esto funciona más rápido que todas las demás soluciones. Y es compatible desde el principio.
DiddiZ

Esto también podría cortocircuito en el segundo bucle, si countse convierte en negativo, lo que simplifica el cuerpo del bucle a if(!counts.containsKey(item) || --counts.get(item).count < 0) return false;Además, la tercera bucle podría simplificarse enfor(Count c: counts.values()) if(c.count != 0) return false;
Holger

@Holger había considerado algo similar al primero (eliminar el conteo cuando llega a cero, lo que tendría el mismo efecto pero también convertiría los cheques al final en "devolver si countsestá vacío"), pero no quise oscurecer El punto principal: que el uso de un mapa básicamente convierte esto en un problema de O (N + M), y es el mayor impulso que probablemente obtendrá.
cHao

@ cHao sí, te mereces los créditos por apuntar a una solución con una mejor complejidad temporal que las anteriores. Simplemente pensé en ello, porque hay una pregunta similar reciente con respecto a los iterables. Como ahora también tenemos Java 8, valió la pena repensarlo. Si hace un cortocircuito en el segundo ciclo cuando el número se vuelve negativo, el tercer ciclo se vuelve obsoleto. Además, evitar el boxeo podría ser una espada de doble filo, con nuevos Map.mergenúmeros enteros en caja podría ser más simple y más eficiente para la mayoría de los casos de uso. Ver también esta respuesta ...
Holger

10

Yo diría que estas respuestas pierden un truco.

Bloch, en su esencial, maravilloso, conciso Java efectivo , dice, en el ítem 47, título "Conozca y use las bibliotecas", "Para resumir, no reinvente la rueda". Y da varias razones muy claras por las que no.

Aquí hay algunas respuestas que sugieren métodos de CollectionUtilsla biblioteca Apache Commons Collections, pero ninguna ha descubierto la forma más hermosa y elegante de responder esta pregunta :

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

}

Culpables : es decir, los elementos que no son comunes a ambos Lists. Determinar a qué culpables pertenece list1y a qué list2es relativamente sencillo usar CollectionUtils.intersection( list1, culprits )y CollectionUtils.intersection( list2, culprits ).
Sin embargo, tiende a desmoronarse en casos como {"a", "a", "b"}disjunction con {"a", "b", "b"} ... excepto que esto no es una falla del software, pero inherente a la naturaleza de las sutilezas / ambigüedades de la tarea deseada.

Siempre puede examinar el código fuente (l. 287) para una tarea como esta, producida por los ingenieros de Apache. Una ventaja de usar su código es que habrá sido probado y probado exhaustivamente, con muchos casos extremos y problemas previstos y tratados. Puede copiar y ajustar este código al contenido de su corazón si es necesario.


Nota: Al principio me decepcionó que ninguno de los CollectionUtilsmétodos proporciona una versión sobrecargada que le permite imponer la suya Comparator(para que pueda redefinirla equalssegún sus propósitos).

Pero de collections4 4.0 hay una nueva clase, Equatorque "determina la igualdad entre los objetos de tipo T". Al examinar el código fuente de collections4 CollectionUtils.java, parecen estar usando esto con algunos métodos, pero por lo que puedo entender, esto no es aplicable a los métodos en la parte superior del archivo, usando la CardinalityHelperclase ... que incluir disjunctiony intersection.

Supongo que la gente de Apache todavía no se ha dado cuenta de esto porque no es trivial: tendrías que crear algo así como una clase "AbstractEquatingCollection", que en lugar de usar los elementos equalsy hashCodemétodos inherentes a sus elementos, tendría que usar esos de Equatortodos los métodos básicos, como add, containsetc. NB, de hecho, cuando mira el código fuente, AbstractCollectionno implementa add, ni sus subclases abstractas como AbstractSet... tiene que esperar hasta las clases concretas como HashSety ArrayListantes addestá implementado. Todo un dolor de cabeza.

Mientras tanto mira este espacio, supongo. La solución provisional obvia sería envolver todos sus elementos en una clase de envoltura a medida que utilice equalse hashCodeimplementar el tipo de igualdad que desee ... luego manipular Collectionsestos objetos de envoltura.


Además, alguien sabio dijo "Conozca el costo de la dependencia"
Stanislaw Baranski

@StanislawBaranski Ese es un comentario interesante. ¿Es una sugerencia de que uno no debería depender demasiado de tales bibliotecas? Cuando usas un sistema operativo en una computadora que ya es un gran salto de fe, ¿no? La razón por la que estoy feliz de usar las bibliotecas de Apache es porque considero que son de muy alta calidad y supongo que sus métodos cumplen con su "contrato" y han sido probados a fondo. ¿Cuánto tiempo tardarías en desarrollar tu propio código en el que confiabas más? Copiar el código de las bibliotecas de Apache de código abierto y examinarlo podría ser algo en lo que pensar ...
#

8

Si la cardinalidad de los elementos no importa (es decir, los elementos repetidos se consideran uno), entonces hay una manera de hacerlo sin tener que ordenar:

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

Esto creará un Setfuera de cada uno List, y luego usará HashSetel equalsmétodo que (por supuesto) ignora el pedido.

Si la cardinalidad es importante, debe limitarse a las instalaciones proporcionadas por List; La respuesta de @ jschoen sería más adecuada en ese caso.


¿Qué pasa si la lista A = [a, b, c, c] y la lista B = [a, b, c]. El resultado será verdadero, pero las listas no son iguales.
Nikolas

6

La conversión de las listas a Multiset de Guava funciona muy bien. Se comparan independientemente de su orden y los elementos duplicados también se tienen en cuenta.

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

Esto se basa en la solución @cHao. Incluí varias correcciones y mejoras de rendimiento. Esto funciona aproximadamente el doble de rápido que la solución de copia ordenada igual. Funciona para cualquier tipo de colección. Las colecciones vacías y nulas se consideran iguales. Úselo para su ventaja;)

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

Puede omitir el ciclo for for final utilizando un contador de suma. El contador de la suma contará el total de cuentas en cada etapa. Aumente el contador de la suma en el primer ciclo for y disminuya en el segundo ciclo for. Si el contador de la suma es mayor que 0, las listas no coinciden; de lo contrario, sí. Actualmente, en el ciclo for for final, verifica si todos los recuentos son cero o, en otras palabras, si la suma de todos los recuentos es cero. El uso del tipo de contador de suma invierte esta verificación, devolviendo verdadero si el total de conteos es cero, o falso de lo contrario.
Sáb

En mi opinión, vale la pena omitir ese bucle for ya que cuando las listas coinciden (en el peor de los casos), el bucle for agrega otro O (n) innecesario.
Sáb

@SatA en realidad, puede eliminar el tercer ciclo sin ningún reemplazo. El segundo ciclo ya regresa falsecuando una clave no existe o su recuento se vuelve negativo. Dado que el tamaño total de ambas listas coincide (esto se ha verificado por adelantado), es imposible tener valores distintos de cero después del segundo ciclo, ya que no puede haber valores positivos para una clave sin valores negativos para otra clave.
Holger

@holger parece que tienes toda la razón. Por lo que puedo decir, el tercer ciclo no es necesario en absoluto.
Sáb

@SatA ... y con Java 8, esto se puede implementar de manera concisa, como en esta respuesta .
Holger

5

Piense cómo lo haría usted mismo, sin una computadora o lenguaje de programación. Te doy dos listas de elementos, y tienes que decirme si contienen los mismos elementos. ¿Como lo harias?

Un enfoque, como se mencionó anteriormente, es ordenar las listas y luego ir elemento por elemento para ver si son iguales (que es lo que List.equalshace). Esto significa que puede modificar las listas o copiarlas, y sin conocer la asignación, no puedo saber si están permitidas o ambas.

Otro enfoque sería revisar cada lista, contando cuántas veces aparece cada elemento. Si ambas listas tienen los mismos recuentos al final, tienen los mismos elementos. El código para eso sería traducir cada lista a un mapa elem -> (# of times the elem appears in the list)y luego llamar equalsa los dos mapas. Si los mapas son HashMap, cada una de esas traducciones es una operación O (N), como lo es la comparación. Eso le dará un algoritmo bastante eficiente en términos de tiempo, a costa de un poco de memoria adicional.


5

Tuve este mismo problema y se me ocurrió una solución diferente. Este también funciona cuando hay duplicados involucrados:

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

Ventajas en comparación con algunas otras soluciones:

  • complejidad menor que O (N²) (aunque no he probado su rendimiento real en comparación con las soluciones en otras respuestas aquí);
  • sale temprano;
  • comprueba si es nulo;
  • funciona incluso cuando hay duplicados involucrados: si tiene una matriz [1,2,3,3]y otra matriz, la [1,2,2,3]mayoría de las soluciones aquí le dicen que son las mismas cuando no considera el orden. Esta solución evita esto eliminando elementos iguales de las listas temporales;
  • utiliza la igualdad semántica ( equals) y no la igualdad de referencia ( ==);
  • no clasifica itens, por lo que no es necesario ordenarlos (por implement Comparable) para que esta solución funcione.

3

Si no desea ordenar las colecciones y necesita el resultado de que ["A" "B" "C"] no es igual a ["B" "B" "A" "C"],

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

no es suficiente, también debe verificar el tamaño:

    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

Solución que aprovecha el método de resta de 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

Si le importa el orden, simplemente use el método igual:

list1.equals(list2)

Si no te importa el orden, usa esto

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

44
Dice que no le importa el orden.
Mike roedor

0

Lo mejor de ambos mundos [@DiddiZ, @Chalkos]: este se basa principalmente en el método @Chalkos, pero corrige un error (ifst.next ()), mejora las comprobaciones iniciales (tomadas de @DiddiZ) y elimina la necesidad de copie la primera colección (solo elimina elementos de una copia de la segunda colección).

No requiere una función de hash o clasificación, y permite una pronta existencia de desigualdad, esta es la implementación más eficiente hasta ahora. Eso es a menos que tenga una longitud de colección de miles o más, y una función de hash muy simple.

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

Si no me equivoco, la complejidad de este algoritmo en un caso de valor (donde las listas son iguales pero ordenadas de manera opuesta) sería O (N * N!).
Sáb

En realidad, sería O (N * (N / 2)), ya que con cada iteración, el tamaño de la matriz disminuye.
jazzgil

0

Es una forma alternativa de verificar la igualdad de las listas de matrices que pueden contener 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);
}

¿Cuál es el punto de toda esta copia entre listas y matrices?
Holger

0

Mi solución para esto. No es tan genial, pero funciona bien.

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

En ese caso, las listas {"a", "b"} y {"b", "a"} son iguales. Y {"a", "b"} y {"b", "a", "c"} no son iguales. Si usa una lista de objetos complejos, recuerde anular el método igual , ya que contiene Todo lo usa en su interior.

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

-1: da la respuesta incorrecta con {"a", "a", "b"} y {"a", "b", "b"}: comprueba el código fuente de AbstractCollection.containsAll(). Debe permitir tener elementos duplicados de los que estamos hablando Lists, no de los que estamos hablando Sets. Por favor mira mi respuesta.
Mike roedor
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.