web-dev-qa-db-fra.com

Tester si les listes partagent des éléments de python

Je veux vérifier si aucun des éléments d'une liste sont présents dans une autre liste. Je peux le faire simplement avec le code ci-dessous, mais je soupçonne qu’il pourrait y avoir une fonction de bibliothèque pour le faire. Sinon, existe-t-il une méthode plus Pythonic pour obtenir le même résultat?.

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
   ....: 
110
fmark

Réponse courte : utilisez not set(a).isdisjoint(b), c'est généralement le plus rapide.

Il existe quatre méthodes courantes pour vérifier si deux listes a et b partagent des éléments. La première option consiste à convertir les deux en ensembles et à vérifier leur intersection, comme suit:

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

Comme les ensembles sont stockés à l'aide d'une table de hachage en Python, leur recherche est O(1) (voir ici pour plus d'informations sur la complexité des opérateurs en Python). Théoriquement, il s'agit de O(n+m) en moyenne pour n et m objets dans les listes a et b. Mais 1) il doit d’abord créer des ensembles à partir des listes, ce qui peut prendre un temps non négligeable, et 2) cela suppose que les collisions de hachage soient rares parmi vos données.

La deuxième façon de faire consiste à utiliser une expression génératrice effectuant une itération sur les listes, telle que:

any(i in a for i in b)

Cela permet de rechercher sur place, de sorte qu'aucune nouvelle mémoire n'est allouée pour les variables intermédiaires. Il sauve également sur la première découverte. Mais l'opérateur in est toujours O(n) sur les listes (voir ici ).

Une autre option proposée est un hybride pour parcourir l'une des listes, convertir l'autre en un ensemble et tester l'appartenance à cet ensemble, comme suit:

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

Une quatrième approche consiste à tirer parti de la méthode isdisjoint() des ensembles (figés) (voir here ), par exemple:

not set(a).isdisjoint(b)

Si les éléments que vous recherchez se trouvent au début d'un tableau (par exemple, il est trié), l'expression du générateur est privilégiée, car la méthode d'intersection d'ensembles doit allouer une nouvelle mémoire pour les variables intermédiaires:

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

Voici un graphique du temps d'exécution de cet exemple en fonction de la taille de la liste:

Element sharing test execution time when shared at the beginning

Notez que les deux axes sont logarithmiques. Ceci représente le meilleur cas pour l'expression du générateur. Comme on peut le constater, la méthode isdisjoint() est préférable pour les très petites tailles de liste, alors que l'expression du générateur est préférable pour les plus grandes.

Par contre, comme la recherche commence au début de l'expression hybride et génératrice, si l'élément partagé se trouve systématiquement à la fin du tableau (ou que les deux listes ne partagent aucune valeur), les approches d'intersection disjoint et définie sont alors beaucoup plus rapide que l'expression du générateur et l'approche hybride.

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

Il est intéressant de noter que l'expression du générateur est beaucoup plus lente pour les listes de grande taille. Ce n'est que pour 1000 répétitions, au lieu de 100000 pour la figure précédente. Cette configuration convient également bien lorsqu'aucun élément n'est partagé et constitue le meilleur cas pour les approches d'intersection disjointes et définies.

Voici deux analyses utilisant des nombres aléatoires (au lieu de modifier la configuration pour privilégier une technique ou une autre):

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

Forte chance de partage: les éléments sont tirés au hasard dans [1, 2*len(a)]. Faible chance de partage: les éléments sont tirés au hasard dans [1, 1000*len(a)].

Jusqu'à présent, cette analyse supposait que les deux listes avaient la même taille. Dans le cas de deux listes de tailles différentes, par exemple, a est beaucoup plus petit, isdisjoint() est toujours plus rapide:

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

Assurez-vous que la liste a est la plus petite, sinon les performances diminuent. Dans cette expérience, la taille de la liste a a été définie constante sur 5.

En résumé:

  • Si les listes sont très petites (<10 éléments), not set(a).isdisjoint(b) est toujours le plus rapide.
  • Si les éléments dans les listes sont triés ou ont une structure régulière dont vous pouvez tirer parti, l'expression du générateur any(i in a for i in b) est la plus rapide des grandes tailles de liste;
  • Testez l'intersection définie avec not set(a).isdisjoint(b), qui est toujours plus rapide que bool(set(a) & set(b)).
  • L'hybride "parcourir la liste, tester sur le jeu" a = set(a); any(i in a for i in b) est généralement plus lent que les autres méthodes.
  • L'expression du générateur et l'hybride sont beaucoup plus lentes que les deux autres approches lorsqu'il s'agit de listes sans éléments partagés.

Dans la plupart des cas, l'utilisation de la méthode isdisjoint() est la meilleure approche, car l'expression du générateur prend beaucoup plus de temps à s'exécuter, car elle est très inefficace lorsqu'aucun élément n'est partagé.

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

Remarque: ce qui précède suppose que vous voulez un booléen comme réponse. Si tout ce dont vous avez besoin est une expression à utiliser dans une instruction if, utilisez simplement if set(a) & set(b):

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

Ceci est asymptotiquement optimal (pire des cas, O (n + m)), et pourrait être meilleur que l’approche par intersection en raison du court-circuit de any.

Par exemple.:

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

retournera True dès qu'il arrivera à 3 in sb

EDIT: Autre variante (merci à Dave Kirby):

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

Cela repose sur l'itérateur de imap, qui est implémenté en C plutôt que par un générateur de compréhension. Il utilise également sb.__contains__ comme fonction de mappage. Je ne sais pas quelle différence de performance cela fait. Il sera toujours en court-circuit.

10
Matthew Flaschen

Vous pouvez aussi utiliser any avec une compréhension de liste:

any([item in a for item in b])
4
Ioachim

Dans python 2.6 ou version ultérieure, vous pouvez effectuer les opérations suivantes:

return not frozenset(a).isdisjoint(frozenset(b))
3
Toughy

Vous pouvez utiliser la fonction quelconque any/w une expression génératrice:

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

Comme John et Lie l'ont souligné, cela donne des résultats incorrects lorsque, pour chaque i partagé par les deux listes, bool (i) == False. CA devrait etre:

return any(i in b for i in a)
2
Anthony Conyers

J'en jetterai un autre avec un style de programmation fonctionnel:

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

Explication:

map(lambda x: x in a, b)

retourne une liste de booléens où des éléments de b se trouvent dans a. Cette liste est ensuite transmise à any, qui renvoie simplement True si des éléments sont True.

1
cs01

Cette question est assez ancienne, mais j'ai remarqué que pendant que les gens discutaient entre ensembles et listes, personne ne songeait à les utiliser ensemble. Suivant l'exemple de Soravux,

Le pire des cas pour les listes:

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

Et le meilleur des cas pour les listes:

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

Donc, même plus rapidement que parcourir deux listes est de parcourir une liste pour voir si elle est dans un ensemble, ce qui est logique dans la mesure où vérifier si un nombre est dans un ensemble prend un temps constant la liste.

Donc, ma conclusion est que parcourir une liste et vérifier si elle est dans un ensemble.

1
binoche9

si vous ne vous souciez pas de ce que l'élément chevauchant pourrait être, vous pouvez simplement vérifier le len de la liste combinée par rapport aux listes combinées en tant qu'ensemble. S'il y a des éléments qui se chevauchent, l'ensemble sera plus court:

len(set(a+b+c))==len(a+b+c) renvoie True, s'il n'y a pas de chevauchement.

1
domoarigato