Testen Sie, ob Listen Elemente in Python gemeinsam nutzen

131

Ich möchte überprüfen , ob jede der Elemente in einer Liste in eine andere Liste vorhanden sind. Ich kann es einfach mit dem folgenden Code tun, aber ich vermute, dass es eine Bibliotheksfunktion gibt, um dies zu tun. Wenn nicht, gibt es eine pythonischere Methode, um das gleiche Ergebnis zu erzielen.

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
   ....: 
fmark
quelle
Die einzige Optimierung, an die ich denken kann, ist das Fallenlassen, len(...) > 0weil sie bool(set([]))Falsch ergibt. Und wenn Sie Ihre Listen zunächst als Sets behalten, sparen Sie natürlich den Aufwand für die Set-Erstellung.
Msw
Relevant: stackoverflow.com/a/44786707/1959808
Ioannis Filippidis
1
Beachten Sie, dass Sie nicht Truevon 1und Falsevon unterscheiden können 0. not set([1]).isdisjoint([True])bekommt True, das gleiche mit anderen Lösungen.
Dimali

Antworten:

313

Kurze Antwort : Verwenden Sie not set(a).isdisjoint(b), es ist in der Regel die schnellste.

Es gibt vier gängige Methoden, um zu testen, ob zwei Listen vorhanden sind, aund bum Elemente gemeinsam zu nutzen. Die erste Möglichkeit besteht darin, beide in Mengen umzuwandeln und deren Schnittpunkt als solche zu überprüfen:

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

Da Sätze gespeichert sind in Python eine Hash - Tabelle, die Such sie istO(1) (siehe hier für weitere Informationen über die Komplexität von Operatoren in Python). Theoretisch ist dies O(n+m)im Durchschnitt für nund mObjekte in Listen aund b. Aber 1) es müssen zuerst Sätze aus den Listen erstellt werden, was eine nicht zu vernachlässigende Zeit in Anspruch nehmen kann, und 2) es wird angenommen, dass Hashing-Kollisionen unter Ihren Daten spärlich sind.

Die zweite Möglichkeit besteht darin, einen Generatorausdruck zu verwenden, der eine Iteration der Listen ausführt, z.

any(i in a for i in b)

Dies ermöglicht die direkte Suche, sodass kein neuer Speicher für Zwischenvariablen zugewiesen wird. Es kommt auch beim ersten Fund heraus. Der inOperator ist aber immer O(n)auf Listen (siehe hier ).

Eine andere vorgeschlagene Option ist ein Hybrid, um eine der Listen zu durchlaufen, die andere in einen Satz umzuwandeln und die Mitgliedschaft in diesem Satz zu testen, wie folgt:

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

Ein vierter Ansatz besteht darin, die isdisjoint()Methode der (eingefrorenen) Mengen (siehe hier ) zu nutzen, zum Beispiel:

not set(a).isdisjoint(b)

Befinden sich die von Ihnen gesuchten Elemente am Anfang eines Arrays (z. B. sortiert), wird der Generatorausdruck bevorzugt, da die Mengenschnittmethode neuen Speicher für die Zwischenvariablen zuweisen muss:

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

Hier ist ein Diagramm der Ausführungszeit für dieses Beispiel in Abhängigkeit von der Listengröße:

Testausführungszeit für die gemeinsame Nutzung von Elementen, wenn sie zu Beginn freigegeben wurde

Beachten Sie, dass beide Achsen logarithmisch sind. Dies ist der beste Fall für den Generatorausdruck. Wie zu sehen ist, ist die isdisjoint()Methode für sehr kleine Listengrößen besser, während der Generatorausdruck für größere Listengrößen besser ist.

Wenn andererseits die Suche mit dem Beginn des Hybrid- und Generatorausdrucks beginnt und sich das gemeinsam genutzte Element systematisch am Ende des Arrays befindet (oder beide Listen keine Werte gemeinsam haben), werden die disjunkten und festgelegten Schnittpunkte verwendet viel schneller als der Generatorausdruck und der Hybridansatz.

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

Testausführungszeit für Elementfreigabe, wenn am Ende freigegeben

Es ist interessant festzustellen, dass der Generatorausdruck bei größeren Listen viel langsamer ist. Dies gilt nur für 1000 Wiederholungen anstelle der 100000 für die vorherige Abbildung. Dieses Setup kommt auch dann gut an, wenn keine Elemente gemeinsam genutzt werden, und ist der beste Fall für disjunkte und festgelegte Schnittpunkte.

Hier sind zwei Analysen unter Verwendung von Zufallszahlen (anstatt das Setup so zu manipulieren, dass die eine oder andere Technik bevorzugt wird):

Testausführungszeit für die Elementfreigabe für zufällig generierte Daten mit hoher Wahrscheinlichkeit der Freigabe Testausführungszeit für die Elementfreigabe für zufällig generierte Daten mit hoher Wahrscheinlichkeit der Freigabe

Hohe Chance zu teilen: Elemente werden zufällig ausgewählt [1, 2*len(a)]. Geringe Chance zum Teilen: Elemente werden zufällig ausgewählt [1, 1000*len(a)].

Bisher wurde bei dieser Analyse angenommen, dass beide Listen gleich groß sind. Bei zwei Listen unterschiedlicher Größe, zum Beispiel aviel kleiner, isdisjoint()geht es immer schneller:

Testausführungszeit für die gemeinsame Nutzung von Elementen in zwei Listen unterschiedlicher Größe, wenn sie zu Beginn gemeinsam genutzt werden Testausführungszeit für die gemeinsame Nutzung von Elementen auf zwei Listen unterschiedlicher Größe, wenn sie am Ende gemeinsam genutzt werden

Stellen Sie sicher, dass die aListe kleiner ist, da sonst die Leistung abnimmt. In diesem Experiment wurde die aListengröße konstant auf gesetzt 5.

Zusammenfassend:

  • Wenn die Listen sehr klein sind (<10 Elemente), not set(a).isdisjoint(b)ist immer die schnellste.
  • Wenn die Elemente in den Listen sortiert sind oder eine reguläre Struktur haben, die Sie nutzen können, ist der Generatorausdruck any(i in a for i in b)bei großen Listengrößen am schnellsten.
  • Testen Sie den eingestellten Schnittpunkt mit not set(a).isdisjoint(b), der immer schneller ist als bool(set(a) & set(b)).
  • Der Hybrid "Liste durchlaufen, Test am Set" a = set(a); any(i in a for i in b)ist im Allgemeinen langsamer als andere Methoden.
  • Der Generatorausdruck und der Hybrid sind viel langsamer als die beiden anderen Ansätze, wenn es um Listen ohne gemeinsame Nutzung von Elementen geht.

In den meisten Fällen ist die Verwendung der isdisjoint()Methode der beste Ansatz, da die Ausführung des Generatorausdrucks viel länger dauert, da er sehr ineffizient ist, wenn keine Elemente gemeinsam genutzt werden.

Soravux
quelle
8
Das sind einige nützliche Daten, die zeigen, dass die Big-O-Analyse nicht das A und O der Überlegungen zur Laufzeit ist.
Steve Allison
Was ist mit dem schlimmsten Fall? anywird beim ersten nicht falschen Wert beendet. Wenn wir eine Liste verwenden, in der der einzige übereinstimmende Wert am Ende steht, erhalten wir Folgendes: 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 ... und nur mit 1000 Iterationen.
RobM
2
Danke @RobM für die Information. Ich habe meine Antwort aktualisiert, um dies widerzuspiegeln und die anderen in diesem Thread vorgeschlagenen Techniken zu berücksichtigen.
Soravux
Es sollte not set(a).isdisjoint(b)getestet werden, ob zwei Listen ein Mitglied gemeinsam nutzen. set(a).isdisjoint(b)Gibt zurück, Truewenn die beiden Listen kein Mitglied gemeinsam nutzen. Die Antwort sollte bearbeitet werden?
Guillochon
1
Vielen Dank für das Heads-up, @ Guillochon, es ist behoben.
Soravux
25
def lists_overlap3(a, b):
    return bool(set(a) & set(b))

Hinweis: In der obigen Beschreibung wird davon ausgegangen, dass Sie einen Booleschen Wert als Antwort wünschen. Wenn Sie nur einen Ausdruck benötigen, der in einer ifAnweisung verwendet werden soll, verwenden Sie einfachif set(a) & set(b):

John Machin
quelle
5
Dies ist der Worst-Case-O (n + m). Der Nachteil ist jedoch, dass ein neuer Satz erstellt wird und nicht ausfällt, wenn ein gemeinsames Element frühzeitig gefunden wird.
Matthew Flaschen
1
Ich bin gespannt, warum das so ist O(n + m). Ich würde vermuten, dass Sets mithilfe von Hash-Tabellen implementiert werden und der inOperator somit O(1)rechtzeitig arbeiten kann (außer in entarteten Fällen). Ist das richtig? Wenn ja, bedeutet dies, dass Hash-Tabellen angesichts der Worst-Case-Lookup-Leistung eine Leistung aufweisen O(n), die im Gegensatz zu einem schlechteren Fall eine O(n * m)Leistung aufweist?
Mark
1
@fmark: Theoretisch hast du recht. Praktisch kümmert es niemanden; Lesen Sie die Kommentare in Objects / dictobject.c in der CPython-Quelle (Sets sind nur Diktate mit nur Schlüsseln, keine Werte) und prüfen Sie, ob Sie eine Liste von Schlüsseln erstellen können, die eine O (n) -Suchleistung verursachen.
John Machin
Okay, danke für die Klarstellung, ich habe mich gefragt, ob etwas Magie los ist :). Obwohl ich damit einverstanden bin, dass ich mich praktisch nicht darum kümmern muss, ist es trivial, eine Liste von Schlüsseln zu erstellen, die eine Suchleistung verursachen O(n);), siehe pastebin.com/Kn3kAW7u Nur für lafs.
Mark
2
Ja ich weiß. Außerdem habe ich gerade die Quelle gelesen, auf die Sie mich hingewiesen haben, die bei nicht zufälligen Hash-Funktionen (wie der eingebauten) noch mehr Magie dokumentiert. Ich nahm an, dass es Zufälligkeit erfordert, wie die von Java, was zu Ungeheuerlichkeiten wie dieser führt . Stackoverflow.com/questions/2634690/… . Ich muss mich immer wieder daran erinnern, dass Python nicht Java ist (Gott sei Dank!).
Mark
10
def lists_overlap(a, b):
  sb = set(b)
  return any(el in sb for el in a)

Dies ist asymptotisch optimal (Worst-Case O (n + m)) und kann aufgrund des anyKurzschlusses besser sein als der Schnittpunktansatz .

Z.B:

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

wird True zurückgeben, sobald es soweit ist 3 in sb

EDIT: Eine weitere Variante (danke an Dave Kirby):

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

Dies beruht auf imapdem in C implementierten Iterator und nicht auf einem Generatorverständnis. Es wird auch sb.__contains__als Zuordnungsfunktion verwendet. Ich weiß nicht, wie viel Leistungsunterschied dies macht. Es wird immer noch kurzgeschlossen.

Matthew Flaschen
quelle
1
Schleifen im Kreuzungsansatz sind alle in C-Code; Es gibt eine Schleife in Ihrem Ansatz, die Python-Code enthält. Das große Unbekannte ist, ob eine leere Kreuzung wahrscheinlich oder unwahrscheinlich ist.
John Machin
2
Sie können auch verwenden, any(itertools.imap(sb.__contains__, a))was noch schneller sein sollte, da die Verwendung einer Lambda-Funktion vermieden wird.
Dave Kirby
Danke, @ Dave. :) Ich bin damit einverstanden, dass das Entfernen des Lambda ein Gewinn ist.
Matthew Flaschen
4

Sie können auch anymit Listenverständnis verwenden:

any([item in a for item in b])
Ioachim
quelle
6
Sie könnten, aber die Zeit ist O (n * m), während die Zeit für den festgelegten Schnittpunktansatz O (n + m) ist. Sie könnten es auch OHNE Listenverständnis tun (verlieren Sie das []) und es würde schneller laufen und weniger Speicher verbrauchen, aber die Zeit wäre immer noch O (n * m).
John Machin
1
Während Ihre große O-Analyse wahr ist, vermute ich, dass für kleine Werte von n und m die Zeit ins Spiel kommt, die zum Erstellen der zugrunde liegenden Hashtabellen benötigt wird. Big O ignoriert die Zeit, die zum Berechnen der Hashes benötigt wird.
Anthony Conyers
2
Das Erstellen einer "Hashtabelle" wird mit O (n) abgeschrieben.
John Machin
1
Ich verstehe das, aber die Konstante, die du wegwirfst, ist ziemlich groß. Es spielt keine Rolle für große Werte von n, aber für kleine.
Anthony Conyers
3

In Python 2.6 oder höher können Sie Folgendes tun:

return not frozenset(a).isdisjoint(frozenset(b))
Toughy
quelle
1
Es scheint, als müsste man nicht als erstes Argument ein Set oder Frozenset angeben. Ich habe es mit einem String versucht und es hat funktioniert (dh: jedes iterable reicht aus).
Aktau
2

Sie können den beliebigen Ausdruck für die integrierte Funktion / den Generator verwenden:

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

Wie John und Lie darauf hingewiesen haben, führt dies zu falschen Ergebnissen, wenn für jedes i, das von den beiden Listen geteilt wird, bool (i) == False. Es sollte sein:

return any(i in b for i in a)
Anthony Conyers
quelle
1
Verstärkung von Lie Ryans Kommentar: Gibt ein falsches Ergebnis für jedes Element x, das sich an der Kreuzung bool(x)befindet, an der False ist. In Lie Ryans Beispiel ist x 0. Nur Fix ist any(True for i in a if i in b)das, was besser geschrieben ist als das bereits Gesehene any(i in b for i in a).
John Machin
1
Korrektur: wird falsches Ergebnis geben , wenn alle Elemente xin der Kreuzung so sind , dass bool(x)ist False.
John Machin
1

Diese Frage ist ziemlich alt, aber ich bemerkte, dass während die Leute über Sets gegen Listen stritten, niemand daran dachte, sie zusammen zu verwenden. Nach Soravux 'Beispiel

Schlimmster Fall für Listen:

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

Und der beste Fall für Listen:

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

Noch schneller als das Durchlaufen von zwei Listen ist das Durchlaufen einer Liste, um festzustellen, ob sie sich in einem Satz befindet. Dies ist sinnvoll, da das Überprüfen, ob sich eine Zahl in einem Satz befindet, eine konstante Zeit benötigt, während das Überprüfen durch Durchlaufen einer Liste proportional zur Länge von dauert Die Liste.

Daher ist meine Schlussfolgerung, dass ich eine Liste durchlaufen und prüfen muss, ob sie in einem Satz enthalten ist .

binoche9
quelle
1
Die Verwendung der isdisjoint()Methode für ein (eingefrorenes) Set, wie von @Toughy angegeben, ist sogar noch besser: 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,00913715362548828
Aktau
1

Wenn es Ihnen egal ist, was das überlappende Element sein könnte, können Sie einfach lendie kombinierte Liste mit den als Satz kombinierten Listen vergleichen. Wenn es überlappende Elemente gibt, wird die Menge kürzer:

len(set(a+b+c))==len(a+b+c) Gibt True zurück, wenn keine Überlappung vorliegt.

domoarigato
quelle
Wenn sich der erste Wert überschneidet, wird die gesamte Liste in eine noch so große Menge konvertiert.
Peter Wood
1

Ich werde einen anderen mit einem funktionalen Programmierstil einwerfen:

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

Erläuterung:

map(lambda x: x in a, b)

Gibt eine Liste von Booleschen Werten zurück, in denen sich Elemente von bbefinden a. Diese Liste wird dann an übergeben any, die einfach zurückgibt, Truewenn Elemente vorhanden sind True.

cs01
quelle