2010-07-03 10 views
67

Quiero comprobar si cualquier de los elementos en una lista están presentes en otra lista. Puedo hacerlo simplemente con el siguiente código, pero sospecho que podría haber una función de biblioteca para hacer esto. Si no, hay un método más pitónico para lograr el mismo resultado.Prueba si las listas comparten elementos en python

In [78]: a = [1, 2, 3, 4, 5] 

In [79]: b = [8, 7, 6] 

In [80]: c = [8, 7, 6, 5] 

In [81]: def lists_overlap(a, b): 
    ....:  for i in a: 
    ....:   if i in b: 
    ....:    return True 
    ....:  return False 
    ....: 

In [82]: lists_overlap(a, b) 
Out[82]: False 

In [83]: lists_overlap(a, c) 
Out[83]: True 

In [84]: def lists_overlap2(a, b): 
    ....:  return len(set(a).intersection(set(b))) > 0 
    ....: 
+0

Las únicas optimizaciones que se me ocurren es descartar 'len (...)> 0' porque' bool (set ([])) 'produce False. Y, por supuesto, si mantuvieras tus listas como conjuntos para empezar, ahorrarías la sobrecarga de creación de conjuntos. – msw

+0

Relevante: https://stackoverflow.com/a/44786707/1959808 –

+0

Tenga en cuenta que no puede distinguir 'True' de' 1' y 'False' de' 0'. 'not set ([1]). isdisjoint ([True])' gets 'True', lo mismo con otras soluciones. – Dimali

Respuesta

163

Respuesta corta: not set(a).isdisjoint(b) utilizar, por lo general es el más rápido.

Existen cuatro formas comunes de probar si dos listas a y b comparten algún elemento. La primera opción es convertir tanto a los conjuntos y comprobar su intersección, como tal:

bool(set(a) & set(b)) 

Debido conjuntos se almacenan utilizando una tabla hash en Python, buscando ellos es O(1) (ver here para obtener más información acerca de la complejidad de operadores en Python). Teóricamente, esto es O(n+m) en promedio para los objetos n y m en las listas a y b. Pero 1) primero debe crear conjuntos de las listas, lo que puede tomar una cantidad de tiempo no despreciable, y 2) supone que las colisiones hash son escasas entre sus datos.

La segunda manera de hacerlo es usando una expresión generadora realizar iteración en las listas, como por ejemplo:

any(i in a for i in b) 

Esto permite buscar en el lugar, así que no hay nueva se asigna memoria para las variables intermedias. También rescata en el primer hallazgo. Pero el operador es siempre inO(n) en las listas (ver here).

Otra opción propuesta es un iterate hybridto través de uno de la lista, convertir el otro en un conjunto y la prueba para formar parte de este conjunto, así:

a = set(a); any(i in a for i in b) 

Un cuarto enfoque es aprovechar el método isdisjoint() de los conjuntos (congelados) (véase here), por ejemplo:

not set(a).isdisjoint(b) 

Si los elementos que realizan búsquedas están cerca del comienzo de una matriz (por ejemplo, se ordena), la expresión del generador se ve favorecida, como se los conjuntos me intersecan DTO tienen que asignar nueva memoria para las variables intermedias:

from timeit import timeit 
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=list(range(1000))", number=100000) 
26.077727576019242 
>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=list(range(1000))", number=100000) 
0.16220548999262974 

Aquí es un gráfico del tiempo de ejecución para este ejemplo en función del tamaño de la lista:

Element sharing test execution time when shared at the beginning

Tenga en cuenta que los dos ejes son logarítmicas. Esto representa el mejor caso para la expresión del generador. Como se puede ver, el método isdisjoint() es mejor para tamaños de lista muy pequeños, mientras que la expresión del generador es mejor para tamaños de lista más grandes.

Por otro lado, como la búsqueda comienza con el comienzo para la expresión híbrida y generadora, si el elemento compartido está sistemáticamente al final de la matriz (o ambas listas no comparten ningún valor), la combinación y el conjunto los enfoques de intersección son mucho más rápidos que la expresión del generador y el enfoque híbrido.

>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000)) 
13.739536046981812 
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000)) 
0.08102107048034668 

Element sharing test execution time when shared at the end

Es interesante notar que la expresión generadora es mucho más lento para grandes tamaños de la lista. Esto es solo para 1000 repeticiones, en lugar de 100000 para la figura anterior. Esta configuración también se aproxima bien cuando no se comparten elementos, y es el mejor caso para los enfoques de intersección disjuntos y conjuntos.

Aquí hay dos análisis utilizando números aleatorios (en lugar de manipular la configuración para favorecer una técnica u otra):

Element sharing test execution time for randomly generated data with high chance of sharing Element sharing test execution time for randomly generated data with high chance of sharing

alta posibilidad de compartir: los elementos se toman al azar de [1, 2*len(a)]. Poca posibilidad de compartir: los elementos se toman aleatoriamente del [1, 1000*len(a)].

Hasta ahora, este análisis supone que ambas listas son del mismo tamaño.En el caso de dos listas de diferentes tamaños, por ejemplo a es mucho más pequeño, isdisjoint() es siempre más rápido:

Element sharing test execution time on two differently sized lists when shared at the beginning Element sharing test execution time on two differently sized lists when shared at the end

Asegúrese de que la lista a es la más pequeña, de lo contrario el rendimiento disminuye. En este experimento, el tamaño de la lista a se estableció constante en 5.

En resumen:

  • Si las listas son muy pequeñas (< 10 elementos), not set(a).isdisjoint(b) es siempre el más rápido.
  • Si los elementos en las listas están ordenados o tienen una estructura regular que puede aprovechar, la expresión del generador any(i in a for i in b) es la más rápida en los tamaños de lista grandes;
  • Pruebe la intersección del conjunto con not set(a).isdisjoint(b), que siempre es más rápido que bool(set(a) & set(b)).
  • El híbrido "iterar a través de la lista, prueba en el conjunto" a = set(a); any(i in a for i in b) es generalmente más lento que otros métodos.
  • La expresión del generador y el híbrido son mucho más lentos que los otros dos enfoques cuando se trata de listas sin elementos compartidos.

En la mayoría de los casos, utilizando el método isdisjoint() es el mejor enfoque como la expresión generador se necesita mucho más tiempo para ejecutar, ya que es muy ineficiente cuando se comparten ningún elemento.

+3

Esa es una información útil, muestra que el análisis de gran O no es el único y el final de todo el razonamiento sobre el tiempo de ejecución. –

+0

¿qué pasa con el peor de los casos? 'any' sale en el primer valor no falso. Usando una lista donde el único valor coincidente está al final, obtenemos esto: 'timeit ('any (i en a para i en b)', setup =" a = list (rango (1000)); = [x + 998 para x en rango (999, -0, -1)] ", número = 1000) 13.739536046981812' ' timeit ('bool (set (a) & set (b))', setup = "a = list (rango (1000)); b = [x + 998 para x en rango (999, -0, -1)]", número = 1000) 0.08102107048034668' ... y es con 1000 iteraciones solamente. – RobM

+2

Gracias @RobM por la información. He actualizado mi respuesta para reflejar esto y para tener en cuenta las otras técnicas propuestas en este hilo. – Soravux

23
def lists_overlap3(a, b): 
    return bool(set(a) & set(b)) 

Nota: lo anterior supone que desea un booleano como la respuesta. Si todo lo que necesita es una expresión para utilizar en un comunicado por if, sólo tiene que utilizar if set(a) & set(b):

+3

Este es el caso más desfavorable O (n + m). Sin embargo, el inconveniente es que crea un nuevo conjunto y no rescata cuando un elemento común se encuentra temprano. –

+0

Tengo curiosidad de por qué esto es 'O (n + m)'. Supongo que los conjuntos se implementan utilizando hash-tables, y por lo tanto el operador 'in' puede trabajar en el tiempo' O (1) '(excepto en casos degenerados). ¿Es esto correcto? Si es así, dado que las tablas hash tienen el peor rendimiento de búsqueda de casos de 'O (n)', ¿significa esto que, en el peor de los casos, tendrá un rendimiento 'O (n * m)'? – fmark

+0

@fmark: Teóricamente, tienes razón. Prácticamente, a nadie le importa; lea los comentarios en Objetos/dictobject.c en la fuente de CPython (los conjuntos son solo dictos con solo claves, sin valores) y vea si puede generar una lista de claves que causará el rendimiento de búsqueda O (n). –

4

Usted podría también utilizar any con la lista de comprensión:

any([item in a for item in b]) 
+6

Podrías, pero el tiempo es O (n * m) mientras que el tiempo para el enfoque de intersección establecido es O (n + m). También podrías hacerlo SIN comprensión de la lista (perder el '[]') y funcionaría más rápido y usaría menos memoria, pero el tiempo seguiría siendo O (n * m). –

+1

Si bien su gran análisis de O es verdadero, sospecho que para pequeños valores de n y m, el tiempo necesario para construir los hashtables subyacentes entrará en juego. Big O ignora el tiempo que lleva computar los hashes. –

+2

La construcción de una "tabla hash" se amortiza O (n). –

2

Se puede utilizar el cualquier incorporado en la expresión generador de funciones/wa:

def list_overlap(a,b): 
    return any(i for i in a if i in b) 

Como John y Lie han señalado esto da resultados incorrectos cuando para cada i compartida por los dos bool listas (i) == false. Que debe ser:

return any(i in b for i in a) 
+2

El tiempo es O (n * m). –

+3

dar el resultado incorrecto cuando a = {0, 1, 2} yb = {0, 3, 4} –

+1

Comentario de la mentira de amplificación de Ryan: dará resultado incorrecto para cualquier elemento x que esté en la intersección donde 'bool (x)' Es falso. En el ejemplo de Lie Ryan, x es 0. Solo la corrección es 'any (verdadero para i en a si i en b)' que está mejor escrito como el ya visto 'any (i en b para i en a)'. –

10
def lists_overlap(a, b): 
    sb = set(b) 
    return any(el in sb for el in a) 

Esto es asintóticamente óptimo (peor caso O (n + m)), y podría ser mejor que el enfoque de intersección debido a any 's cortocircuitos.

Ej:

lists_overlap([3,4,5], [1,2,3]) 

devolverá true tan pronto como se pone a 3 in sb

EDIT: Otra variación (con gracias a David Kirby):

def lists_overlap(a, b): 
    sb = set(b) 
    return any(itertools.imap(sb.__contains__, a)) 

Esto se basa en imap El iterador, que se implementa en C, en lugar de una comprensión del generador. También usa sb.__contains__ como la función de mapeo. No sé cuánta diferencia de rendimiento hace esto. Todavía se cortocircuitará.

+1

Los bucles en el enfoque de intersección están todos en código C; hay un ciclo en su enfoque que incluye el código de Python. El gran desconocido es si una intersección vacía es probable o poco probable. –

+2

También podría usar 'any (itertools.imap (sb.__contains__, a)) 'que debería ser aún más rápido ya que evita el uso de una función lambda. –

+0

Gracias, @Dave. :) Acepto eliminar la lambda es una victoria. –

3

en Python 2.6 o posterior que puede hacer:

return not frozenset(a).isdisjoint(frozenset(b)) 
+1

Parece que uno no tiene que suministrar un conjunto o conjunto de antenas como primer argumento. Intenté con una cuerda y funcionó (es decir, cualquier iterable servirá). – Aktau

1

Esta pregunta es bastante antigua, pero me di cuenta de que mientras la gente discutía los conjuntos contra las listas, nadie pensaba en usarlos juntos. Siguiendo el ejemplo de Soravux,

peor de los casos para las listas:

>>> timeit('bool(set(a) & set(b))', setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000) 
100.91506409645081 
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000) 
19.746716022491455 
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000) 
0.092626094818115234 

Y el mejor de los casos para las listas:

>>> timeit('bool(set(a) & set(b))', setup="a=list(range(10000)); b=list(range(10000))", number=100000) 
154.69790101051331 
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=list(range(10000))", number=100000) 
0.082653045654296875 
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=list(range(10000))", number=100000) 
0.08434605598449707 

lo tanto, incluso más rápido que la iteración a través de dos listas es iterar a través de una lista para ver si está en un conjunto, lo que tiene sentido, ya que comprobar si un número está en un conjunto lleva tiempo constante, mientras que verificar iterando a través de una lista lleva tiempo proporcional a la longitud de la lista.

Por lo tanto, mi conclusión es que iterar a través de una lista, y comprobar si se encuentra en un conjunto.

+1

Usar el método 'isdisjoint()' en un conjunto (congelado) como lo indica @Toughy es aún mejor: 'timeit ('any (i en a para i en b)', setup =" a = set (rango (10000))); b = [x + 9999 para x en rango (10000)] ", número = 100000)' => 0.00913715362548828 – Aktau

1

si no le importa el elemento superpuesto, simplemente puede verificar el len de la lista combinada frente a las listas combinadas como un conjunto. Si hay elementos superpuestos, el conjunto será más corto:

len(set(a+b+c))==len(a+b+c) devuelve True, si no hay solapamiento.

+0

Si el primer valor se superpone, aún convertirá toda la lista en un conjunto, sin importar cuán grande sea. –

1

Voy a tirar otra en un estilo de programación funcional:

any(map(lambda x: x in a, b)) 

Explicación:

map(lambda x: x in a, b) 

devuelve una lista de booleanos, donde elementos de b se encuentran en a. Esa lista se pasa luego al any, que simplemente devuelve True si algún elemento es True.

Cuestiones relacionadas