Verifica se gli elenchi condividono elementi in Python


131

Voglio verificare se uno degli elementi in un elenco è presente in un altro elenco. Posso farlo semplicemente con il codice qui sotto, ma sospetto che potrebbe esserci una funzione di libreria per farlo. In caso contrario, esiste un metodo più pitonico per ottenere lo stesso risultato.

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

Le uniche ottimizzazioni a cui riesco a pensare è la caduta len(...) > 0perché bool(set([]))produce Falso. E, naturalmente, se hai tenuto le tue liste come set per cominciare, risparmierai il sovraccarico di creazione del set.
msw,


1
Nota che non puoi distinguere Trueda 1e Falseda 0. not set([1]).isdisjoint([True])ottiene True, lo stesso con altre soluzioni.
Dimali,

Risposte:


313

Risposta breve : usare not set(a).isdisjoint(b), è generalmente il più veloce.

Esistono quattro modi comuni per verificare se due elenchi ae bcondividere elementi. La prima opzione è convertire entrambi in set e controllare la loro intersezione, come tale:

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

Poiché i set vengono archiviati utilizzando una tabella hash in Python, la ricerca èO(1) (vedere qui per ulteriori informazioni sulla complessità degli operatori in Python). Teoricamente, questo è O(n+m)in media per ne moggetti negli elenchi ae b. Ma 1) deve prima creare serie fuori dagli elenchi, il che può richiedere una quantità non trascurabile di tempo, e 2) supporre che le collisioni di hashing siano sparse tra i tuoi dati.

Il secondo modo per farlo è usare un'espressione del generatore che esegue l'iterazione sugli elenchi, come ad esempio:

any(i in a for i in b)

Ciò consente la ricerca sul posto, quindi non viene allocata nuova memoria per le variabili intermedie. Si salva anche alla prima scoperta. Ma l' inoperatore è sempre O(n)in lista (vedi qui ).

Un'altra opzione proposta è un ibrido per scorrere l'elenco, convertire l'altro in un set e testare l'appartenenza a questo set, in questo modo:

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

Un quarto approccio consiste nell'utilizzare il isdisjoint()metodo degli insiemi (congelati) (vedere qui ), ad esempio:

not set(a).isdisjoint(b)

Se gli elementi ricercati sono vicini all'inizio di un array (ad es. È ordinato), viene favorita l'espressione del generatore, poiché il metodo di intersezione insiemi deve allocare nuova memoria per le variabili intermedie:

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

Ecco un grafico del tempo di esecuzione per questo esempio in funzione della dimensione dell'elenco:

Tempo di esecuzione del test di condivisione dell'elemento se condiviso all'inizio

Si noti che entrambi gli assi sono logaritmici. Questo rappresenta il caso migliore per l'espressione del generatore. Come si può vedere, ilisdisjoint() metodo è migliore per dimensioni di elenco molto piccole, mentre l'espressione del generatore è migliore per dimensioni di elenco più grandi.

D'altra parte, poiché la ricerca inizia con l'inizio dell'espressione ibrida e del generatore, se l'elemento condiviso è sistematicamente alla fine dell'array (o entrambi gli elenchi non condividono alcun valore), gli approcci di intersezione disgiunti e impostati sono quindi molto più veloce dell'espressione del generatore e dell'approccio ibrido.

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

Tempo di esecuzione del test di condivisione dell'elemento se condiviso alla fine

È interessante notare che l'espressione del generatore è molto più lenta per elenchi di dimensioni maggiori. Questo è solo per 1000 ripetizioni, anziché 100000 per la figura precedente. Questa impostazione si avvicina bene anche quando non sono condivisi elementi ed è il caso migliore per gli approcci di intersezione disgiunti e impostati.

Ecco due analisi usando numeri casuali (invece di attrezzare l'installazione per favorire una tecnica o un'altra):

Tempo di esecuzione del test di condivisione degli elementi per dati generati casualmente con elevate possibilità di condivisione Tempo di esecuzione del test di condivisione degli elementi per dati generati casualmente con elevate possibilità di condivisione

Elevata possibilità di condivisione: gli elementi vengono presi casualmente [1, 2*len(a)] . Bassa possibilità di condivisione: gli elementi vengono presi casualmente[1, 1000*len(a)] .

Fino ad ora, questa analisi supponeva che entrambe le liste avessero le stesse dimensioni. Nel caso di due elenchi di dimensioni diverse, ad esempio aè molto più piccolo, isdisjoint()è sempre più veloce:

Tempo di esecuzione del test di condivisione dell'elemento su due elenchi di dimensioni diverse se condiviso all'inizio Tempo di esecuzione del test di condivisione dell'elemento su due elenchi di dimensioni diverse se condiviso alla fine

Assicurarsi che l' aelenco sia più piccolo, altrimenti le prestazioni diminuiscono. In questo esperimento, la adimensione dell'elenco è stata impostata su costante5 .

In sintesi:

  • Se gli elenchi sono molto piccoli (<10 elementi), not set(a).isdisjoint(b) è sempre il più veloce.
  • Se gli elementi negli elenchi sono ordinati o hanno una struttura regolare di cui puoi trarre vantaggio, l'espressione del generatore any(i in a for i in b) è la più veloce su elenchi di grandi dimensioni;
  • Prova l'intersezione impostata con not set(a).isdisjoint(b), che è sempre più veloce dibool(set(a) & set(b)) .
  • L'ibrido "scorre attraverso l'elenco, prova sul set" a = set(a); any(i in a for i in b) è generalmente più lento di altri metodi.
  • L'espressione del generatore e l'ibrido sono molto più lenti rispetto agli altri due approcci quando si tratta di elenchi senza condividere elementi.

Nella maggior parte dei casi, l'utilizzo del isdisjoint()metodo è l'approccio migliore in quanto l'esecuzione del generatore richiederà molto più tempo per l'esecuzione, poiché è molto inefficiente quando non sono condivisi elementi.


8
Ecco alcuni dati utili lì, mostrano che l'analisi big-O non è il tutto e termina tutti i ragionamenti sul tempo di esecuzione.
Steve Allison,

che dire dello scenario peggiore? anyesce al primo valore non falso. Usando un elenco in cui l'unico valore corrispondente è alla fine, otteniamo questo: 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 ... ed è solo con 1000 iterazioni.
RobM,

2
Grazie @RobM per l'informazione. Ho aggiornato la mia risposta per riflettere questo e per tenere conto delle altre tecniche proposte in questo thread.
Soravux,

Dovrebbe essere not set(a).isdisjoint(b)per verificare se due elenchi condividono un membro. set(a).isdisjoint(b)ritorna Truese le due liste non condividono un membro. La risposta dovrebbe essere modificata?
Guillochon,

1
Grazie per l'heads up, @Guillochon, è stato risolto.
Soravux,

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

Nota: quanto sopra presuppone che si desideri un valore booleano come risposta. Se tutto ciò che serve è un'espressione da usare in ifun'istruzione, basta usareif set(a) & set(b):


5
Questo è il caso peggiore O (n + m). Tuttavia, il lato negativo è che crea un nuovo set e non viene salvato quando viene trovato presto un elemento comune.
Matthew Flaschen,

1
Sono curioso di sapere perché O(n + m). La mia ipotesi sarebbe che gli insiemi siano implementati usando le tabelle hash, e quindi l' inoperatore possa lavorare in O(1)tempo (eccetto in casi degeneri). È corretto? In tal caso, dato che le tabelle hash hanno prestazioni di ricerca nel caso peggiore O(n), ciò significa che nel caso peggiore avrà O(n * m)prestazioni?
fmark

1
@fmark: teoricamente, hai ragione. Praticamente, a nessuno importa; leggi i commenti in Oggetti / dictobject.c nel sorgente CPython (i set sono solo dicts con solo chiavi, nessun valore) e vedi se riesci a generare un elenco di chiavi che causeranno prestazioni di ricerca O (n).
John Machin,

Ok, grazie per il chiarimento, mi chiedevo se ci fosse un po 'di magia in corso :). Anche se concordo sul fatto che praticamente non ho bisogno di preoccuparmene, è banale generare un elenco di chiavi che causeranno O(n)prestazioni di ricerca;), vedi pastebin.com/Kn3kAW7u Solo per i lafs.
fmark

2
Si lo so. Inoltre ho appena letto la fonte che mi hai indicato, che documenta ancora più magia nel caso di funzioni hash non casuali (come quella incorporata). Supponevo che fosse necessaria casualità, come quella Java, che si traduce in mostruosità come questa stackoverflow.com/questions/2634690/… . Devo continuare a ricordare a me stesso che Python non è Java (grazie alla divinità!).
fmark

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

Questo è asintoticamente ottimale (caso peggiore O (n + m)) e potrebbe essere migliore dell'approccio intersezione a causa di any del cortocircuito.

Per esempio:

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

restituirà True non appena arriverà 3 in sb

EDIT: Un'altra variante (grazie a Dave Kirby):

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

Questo si basa imapsull'iteratore, che è implementato in C, piuttosto che sulla comprensione di un generatore. Utilizza anche sb.__contains__come funzione di mappatura. Non so quanta differenza prestazionale ciò comporti. Continuerà a cortocircuitare.


1
I loop in approccio intersezione sono tutti in codice C; c'è un loop nel tuo approccio che include il codice Python. Il grande sconosciuto è se un incrocio vuoto è probabile o improbabile.
John Machin,

2
Puoi anche usare any(itertools.imap(sb.__contains__, a))quale dovrebbe essere ancora più veloce poiché evita di usare una funzione lambda.
Dave Kirby,

Grazie @Dave. :) Sono d'accordo che rimuovere la lambda è una vittoria.
Matthew Flaschen,

4

È inoltre possibile utilizzare anycon la comprensione dell'elenco:

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

6
Potresti, ma il tempo è O (n * m) mentre il tempo per l'approccio dell'intersezione impostato è O (n + m). Potresti anche farlo SENZA comprensione dell'elenco (perdere il []) e funzionerebbe più velocemente e userebbe meno memoria, ma il tempo sarebbe comunque O (n * m).
John Machin,

1
Mentre la tua grande analisi O è vera, sospetto che per piccoli valori di n e m entrerà in gioco il tempo necessario per costruire gli hashtable sottostanti. Big O ignora il tempo necessario per calcolare gli hash.
Anthony Conyers,

2
La costruzione di una "tabella hash" è ammortizzata O (n).
John Machin,

1
Lo capisco, ma la costante che stai gettando è piuttosto grande. Non importa per grandi valori di n, ma per quelli piccoli.
Anthony Conyers,

3

In Python 2.6 o versioni successive puoi fare:

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

1
Sembra che non si debba fornire un set o frozenset come primo argomento. Ho provato con una stringa e ha funzionato (cioè: qualsiasi iterabile lo farà).
Aktau,

2

È possibile utilizzare qualsiasi espressione del generatore di funzioni / wa incorporata:

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

Come hanno sottolineato John e Lie, questo dà risultati errati quando per ogni i condiviso dalle due liste bool (i) == False. Dovrebbe essere:

return any(i in b for i in a)

1
Amplificazione del commento di Lie Ryan: darà un risultato sbagliato per qualsiasi elemento x che si trova nell'intersezione dove bool(x)è False. Nell'esempio di Lie Ryan, x è 0. L'unica soluzione è any(True for i in a if i in b)quale è meglio scritto come già visto any(i in b for i in a).
John Machin,

1
Correzione: darà risultato sbagliato quando tutti gli elementi xdi intersezione sono tali che bool(x)è False.
John Machin,

1

Questa domanda è piuttosto vecchia, ma ho notato che mentre le persone discutevano set contro liste, nessuno pensava di usarle insieme. Seguendo l'esempio di Soravux,

Peggior caso per le liste:

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

E il caso migliore per gli elenchi:

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

Quindi, anche più veloce dell'iterazione attraverso due elenchi, è l'iterazione di un elenco per vedere se si trova in un set, il che ha senso dal momento che il controllo se un numero è in un set richiede tempo costante mentre il controllo iterando attraverso un elenco richiede tempo proporzionale alla lunghezza di la lista.

Pertanto, la mia conclusione è che scorre attraverso un elenco e controlla se si trova in un set .


1
L'uso del isdisjoint()metodo su un set (congelato) come indicato da @Toughy è ancora meglio: 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

se non ti interessa quale potrebbe essere l'elemento sovrapposto, puoi semplicemente controllare l' lenelenco combinato rispetto agli elenchi combinati come set. Se ci sono elementi sovrapposti, l'insieme sarà più corto:

len(set(a+b+c))==len(a+b+c) ritorna True, se non c'è sovrapposizione.


Se il primo valore si sovrappone, convertirà comunque l'intero elenco in un set, non importa quanto sia grande.
Peter Wood,

1

Ne lancerò un altro con uno stile di programmazione funzionale:

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

Spiegazione:

map(lambda x: x in a, b)

restituisce un elenco di valori booleani in cui bsi trovano gli elementi di a. Tale elenco viene quindi passato a any, che restituisce semplicemente Truese ci sono elementi True.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.