2016-01-29 8 views
11

Dato un elenco di insiemi:sottrazione su un elenco di gruppi

allsets = [set([1, 2, 4]), set([4, 5, 6]), set([4, 5, 7])] 

cosa è un modo divinatorio per calcolare il corrispondente elenco di insiemi di elementi aventi sovrapposizione con altre serie?

only = [set([1, 2]), set([6]), set([7])] 

C'è un modo per farlo con una comprensione di lista?

+0

correlati: [Sostituire elenco di lista con la lista "condensata" di lista, mentre mantenere l'ordine] (http://stackoverflow.com/q/13714755/4279) – jfs

risposta

19

Per evitare runtime quadratica, che ci si vuole fare un passo iniziale per capire quali elementi appaiono in più di una serie:

import itertools 
import collections 
element_counts = collections.Counter(itertools.chain.from_iterable(allsets)) 

allora si può semplicemente fare un elenco di gruppi mantenendo tutti gli elementi che comparire solo una volta:

nondupes = [{elem for elem in original if element_counts[elem] == 1} 
      for original in allsets] 

in alternativa, invece di costruire nondupes da element_counts direttamente, possiamo fare un passaggio aggiuntivo costruire un insieme di tutti gli elementi che appaiono esattamente in un input. Ciò richiede una dichiarazione aggiuntiva, ma ci permette di sfruttare l'operatore & per il set di intersezione a fare l'elenco di comprensione più breve e più efficiente:

element_counts = collections.Counter(itertools.chain.from_iterable(allsets)) 
all_uniques = {elem for elem, count in element_counts.items() if count == 1} 
#             ^viewitems() in Python 2.7 
nondupes = [original & all_uniques for original in allsets] 

Timing sembra indicare che utilizzando un all_uniques insieme produce un aumento di velocità sostanziale per il processo generale di eliminazione dei duplicati. Spetta a circa un 3.5x speedup su Python 3 per insiemi di input fortemente duplicati, anche se solo su un 30% speedup per il processo generale di eliminazione dei duplicati su Python 2 a causa di un maggior numero di runtime dominato dalla costruzione del contatore. Questa accelerazione è abbastanza consistente, sebbene non altrettanto importante quanto evitare il runtime quadratico utilizzando element_counts in primo luogo. Se utilizzi Python 2 e questo codice è critico per la velocità, ti consigliamo di utilizzare un numero ordinario dict o uno anziché uno Counter.

Un altro modo sarebbe quello di costruire un insieme dupes da element_counts Utilizzando original - dupes anziché original & all_uniques nell'elenco comprensione, come suggested da Munk. Se questo funziona meglio o peggio dell'uso di un set all_uniques e & dipenderebbe dal grado di duplicazione nel tuo input e dalla versione di Python in uso, ma è doesn'tseem fare una grande differenza in entrambi i casi.

+2

Certamente un modo migliore. Alcuni link per l'OP 1. ['chain.from_iterable'] (https://docs.python.org/3/library/itertools.html#itertools.chain.from_iterable) 2. [' collections.Counter'] (https : //docs.python.org/3/library/collections.html#collections.Counter) –

+0

sì, questo vince. +1 – timgeb

+2

La sintassi letterale potrebbe essere un po 'più carina con [{elem per elem in originale ...}] – munk

7

Sì, può essere fatto, ma non è certo divinatorio

>>> [(i-set.union(*[j for j in allsets if j!= i])) for i in allsets] 
[set([1, 2]), set([6]), set([7])] 

Qualche riferimento sul set può essere trovato in the documentation. L'operatore * si chiama unpacking operator.

+2

eww concordato. Evita questo come la peste. Preferisci qualche verbose per i loop (ma ottimo lavoro Bhargav!) –

+0

Non hai bisogno della lista interna –

+0

@PadraicCunningham Preferiresti un genexp lì? –

2

Un'altra soluzione con itertools.chain:

>>> from itertools import chain 
>>> [x - set(chain(*(y for y in allsets if y!=x))) for x in allsets] 
[set([1, 2]), set([6]), set([7])] 

fattibile anche senza il disimballaggio e utilizzando chain.from_iterable invece.

6

Una soluzione leggermente diversa utilizzando Contatore e comprensione, per sfruttare l'operatore - per la differenza di impostazione.

from itertools import chain 
from collections import Counter 

allsets = [{1, 2, 4}, {4, 5, 6}, {4, 5, 7}] 
element_counts = Counter(chain.from_iterable(allsets)) 

dupes = {key for key in element_counts 
     if element_counts[key] > 1} 

only = [s - dupes for s in allsets] 
+1

In realtà ci ho pensato dopo aver postato la mia soluzione originale, anche se ho usato '&' e creato un insieme 'unique_elements' invece di un insieme' dupes'. [Timing] (http://ideone.com/8b70l4) ha mostrato che '&' è circa il 30% più veloce rispetto a eseguire ogni volta una comprensione dei set di livello Python. Se 'e' o '-' si comporta meglio probabilmente dipende dal grado di duplicazione degli elementi e dalla versione di Python in uso. – user2357112

+0

Selezionare questa soluzione come la migliore risposta perché 1) è molto leggibile, 2) 15% -30% più veloce della soluzione user2357112 sui miei dati del mondo reale – Steve

+0

Soluzione molto bella e leggibile. Originariamente l'ho selezionato come migliore risposta in base a leggibilità e velocità. In seguito è stata modificata la risposta dell'utente2357112, che in seguito a ulteriori test è significativamente più veloce. – Steve