¿Cómo comparar eficientemente dos listas desordenadas (no conjuntos) en Python?

141
a = [1, 2, 3, 1, 2, 3]
b = [3, 2, 1, 3, 2, 1]

a & b deben considerarse iguales, porque tienen exactamente los mismos elementos, solo que en un orden diferente.

La cuestión es que mis listas reales consistirán en objetos (mis instancias de clase), no enteros.

johndir
fuente
77
¿Cómo se comparan los objetos?
Marcelo Cantos
2
¿Cuál es el tamaño esperado de las listas reales? ¿Las listas que se comparan serán de tamaños comparables o muy diferentes? ¿Espera que la mayoría de las listas coincidan o no?
Dmitry B.
Uno puede verificar len()s primero.
barba gris

Respuestas:

245

O (n) : el método Counter () es el mejor (si sus objetos son hashaable):

def compare(s, t):
    return Counter(s) == Counter(t)

O (n log n) : el método sorted () es el siguiente mejor (si sus objetos son ordenables):

def compare(s, t):
    return sorted(s) == sorted(t)

O (n * n) : si los objetos no son hashables ni pueden pedirse, puede usar la igualdad:

def compare(s, t):
    t = list(t)   # make a mutable copy
    try:
        for elem in s:
            t.remove(elem)
    except ValueError:
        return False
    return not t
Raymond Hettinger
fuente
1
Gracias. Convertí cada objeto en una cadena y luego usé el método Counter ().
johndir
Hola @Raymond, recientemente encontré esta pregunta en una entrevista y la utilicé sorted(), sin saberlo Counter. El entrevistador insistió en que había un método más eficiente y claramente hice un espacio en blanco. Después de extensas pruebas en Python 3 con el timeitmódulo, ordenado consistentemente sale más rápido en las listas de enteros. En listas de, 1k elementos, aproximadamente 1.5% más lento y en listas cortas, 10 elementos, 7.5% más lento. Pensamientos?
arctelix
44
Para las listas cortas, el análisis big-O suele ser irrelevante porque los tiempos están dominados por factores constantes. Para las listas más largas, sospecho que algo está mal con su evaluación comparativa. Por 100 entradas con 5 repeticiones cada una, obtengo: 127 usec para ordenado y 42 para Counter (aproximadamente 3 veces más rápido). Con 1,000 entradas con 5 repeticiones, el contador es 4 veces más rápido. python3.6 -m timeit -s 'from collections import Counter' -s 'from random import shuffle' -s 't=list(range(100)) * 5' -s 'shuffle(t)' -s 'u=t[:]' -s 'shuffle(u)' 'Counter(t)==Counter(u)'
Raymond Hettinger
@ Raymond De hecho, estamos obteniendo resultados diferentes. Publiqué mi configuración en una sala de chat sorted vs counter. Tengo mucha curiosidad sobre lo que está pasando aquí.
arctelix
44
No, gracias. No tengo mucho interés en depurar scripts de sincronización espurios. Aquí están sucediendo muchas cosas (código puro de Python vs C, se aplica este tipo de datos a datos aleatorios versus datos semi ordenados, diferentes detalles de implementación entre versiones, cuántos duplicados hay en los datos, etc.)
Raymond Hettinger
16

Puedes ordenar ambos:

sorted(a) == sorted(b)

Una ordenación de conteo también podría ser más eficiente (pero requiere que el objeto sea hashable).

>>> from collections import Counter
>>> a = [1, 2, 3, 1, 2, 3]
>>> b = [3, 2, 1, 3, 2, 1]
>>> print (Counter(a) == Counter(b))
True
Mark Byers
fuente
El contador usa hashing, pero los objetos no son inquebrantables per se. Solo tiene que implementar una solución sensata __hash__, pero eso podría ser imposible para las colecciones.
Jochen Ritzel
2
ordenado tampoco funcionará para todo, por ejemplo, números complejossorted([0, 1j])
John La Rooy
1
sorted () tampoco funciona con conjuntos donde los operadores de comparación han sido anulados para pruebas de subconjunto / superconjunto.
Raymond Hettinger
12

Si sabe que los elementos son siempre utilizables, puede usar uno Counter()que sea O (n)
Si sabe que los elementos siempre se pueden ordenar, puede usar sorted()cuál es O (n log n)

En el caso general, no puede confiar en poder ordenar o tener los elementos, por lo que necesita un respaldo como este, que desafortunadamente es O (n ^ 2)

len(a)==len(b) and all(a.count(i)==b.count(i) for i in a)
John La Rooy
fuente
5

La mejor manera de hacerlo es ordenar las listas y compararlas. (El uso Counterno funcionará con objetos que no sean hashaable). Esto es sencillo para los enteros:

sorted(a) == sorted(b)

Se vuelve un poco más complicado con objetos arbitrarios. Si le importa la identidad del objeto, es decir, si los mismos objetos están en ambas listas, puede usar la id()función como la clave de clasificación.

sorted(a, key=id) == sorted(b, key==id)

(En Python 2.x en realidad no necesita el key=parámetro, porque puede comparar cualquier objeto con cualquier objeto. El orden es arbitrario pero estable, por lo que funciona bien para este propósito; no importa el orden de los objetos) en, solo que el orden es el mismo para ambas listas. En Python 3, sin embargo, la comparación de objetos de diferentes tipos no está permitida en muchas circunstancias, por ejemplo, no puede comparar cadenas con enteros, por lo que si tendrá objetos de varios tipos, lo mejor es usar explícitamente la ID del objeto).

Si desea comparar los objetos en la lista por valor, por otro lado, primero debe definir qué significa "valor" para los objetos. Entonces necesitará alguna forma de proporcionar eso como una clave (y para Python 3, como un tipo consistente). Una forma potencial que funcionaría para muchos objetos arbitrarios es ordenarlos por ellos repr(). Por supuesto, esto podría desperdiciar mucho tiempo extra y crear repr()cadenas de memoria para listas grandes, etc.

sorted(a, key=repr) == sorted(b, key==repr)

Si los objetos son todos de su propio tipo, puede definirlos __lt__()para que el objeto sepa cómo compararse con los demás. Luego puede ordenarlos y no preocuparse por el key=parámetro. Por supuesto, también podría definir __hash__()y usar Counter, lo que será más rápido.

un poco
fuente
4

https://docs.python.org/3.5/library/unittest.html#unittest.TestCase.assertCountEqual

ClaimCountEqual (primero, segundo, msg = Ninguno)

Pruebe que la secuencia primero contiene los mismos elementos que la segunda, independientemente de su orden. Cuando no lo hacen, se generará un mensaje de error que enumera las diferencias entre las secuencias.

Los elementos duplicados no se ignoran al comparar primero y segundo. Verifica si cada elemento tiene el mismo recuento en ambas secuencias. Equivalente a: afirmarEqual (Counter (list (first)), Counter (list (second))) pero también funciona con secuencias de objetos no compartibles.

Nuevo en la versión 3.2.

o en 2.7: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertItemsEqual

más listo
fuente
2
(¿Qué agrega esto a la respuesta de jarekwg ?)
greybeard
3

Si la lista contiene elementos que no se pueden compartir (como una lista de objetos), es posible que pueda usar la clase Counter y la función id () como:

from collections import Counter
...
if Counter(map(id,a)) == Counter(map(id,b)):
    print("Lists a and b contain the same objects")
Marte
fuente
2

Espero que el siguiente código funcione en su caso:

if ((len(a) == len(b)) and
   (all(i in a for i in b))):
    print 'True'
else:
    print 'False'

Esto asegurará que todos los elementos en las listas ay bsean iguales, independientemente de si están en el mismo orden o no.

Para una mejor comprensión, consulte mi respuesta en esta pregunta

Pabitra Pati
fuente
1

Deje a, b listas

def ass_equal(a,b):
try:
    map(lambda x: a.pop(a.index(x)), b) # try to remove all the elements of b from a, on fail, throw exception
    if len(a) == 0: # if a is empty, means that b has removed them all
        return True 
except:
    return False # b failed to remove some items from a

No es necesario hacerlos hashaable o ordenarlos.

Umur Kontacı
fuente
1
Sí, pero esto es O (n ** 2) como lo indicaron varios otros pósters, por lo que solo debe usarse si los otros métodos no funcionan. También asume aapoyos pop(es mutable) y index(es una secuencia). Raymond no asume nada mientras que gnibbler asume solo una secuencia.
AGF
0

El uso del unittestmódulo le brinda un enfoque limpio y estándar.

import unittest

test_object = unittest.TestCase()
test_object.assertCountEqual(a, b)
Meysam Sadeghi
fuente