2016-01-29 11 views
11

otrzymali listę zestawów:odejmowanie nad listą zestawów

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

Co jest pythonic sposób obliczyć odpowiednią listę zestawów elementów nie mających pokrywać się z innymi zestawami?

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

Czy istnieje sposób, aby to zrobić ze zrozumieniem listy?

+0

pokrewne: [lista listy Wymień "skondensowanej" listy lista zachowując kolejność] (http://stackoverflow.com/q/13714755/4279) – jfs

Odpowiedz

19

Aby uniknąć kwadratowego czasu pracy, którą chcesz dokonać wstępnej przepustkę, aby dowiedzieć się, które elementy pojawiają się w więcej niż jednym zestawie

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

Następnie można po prostu zrobić listę zestawów zachowując wszystkie elementy, które pojawiają się tylko raz:

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

Alternatywnie, zamiast konstruowania nondupes z element_counts bezpośrednio, możemy wykonać dodatkową przepustkę do skonstruuj zestaw wszystkich elementów, które pojawiają się dokładnie na jednym wejściu. To wymaga dodatkowego oświadczenia, ale pozwala nam skorzystać z operatora & dla zadanej skrzyżowania, aby lista zrozumienie krótsze i bardziej efektywne:

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 wskazuje na to, że za pomocą all_uniques zestaw produkuje znaczne przyspieszenie dla ogólnego procesu eliminacji duplikatów. To około 3.5x speedup na Pythonie 3 dla mocno zduplikowanych zestawów wejściowych, ale tylko o 30% speedup dla ogólnego procesu eliminacji duplikatów w Pythonie 2 z powodu większej ilości czasu zdominowanego przez konstruowanie licznika. Przyspieszenie to jest dość znaczące, ale nie jest tak ważne, jak uniknięcie kwadratowego środowiska wykonawczego, w pierwszej kolejności za pomocą element_counts. Jeśli korzystasz z Pythona 2, a ten kod jest krytyczny pod względem szybkości, możesz użyć zwykłego dict lub collections.defaultdict zamiast Counter.

Innym sposobem byłoby skonstruować dupes zestaw z element_counts i używać original - dupes zamiast original & all_uniques na liście zrozumienia, jak suggested przez Munka. To, czy działa lepiej, czy gorzej niż przy użyciu zestawu all_uniques i &, zależy od stopnia duplikacji danych wejściowych i wersji Pythona, ale także od tego, czy jest to duża różnica.

+2

Z pewnością lepszy sposób. Niektóre linki do 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

Tak, to wygrywa. +1 – timgeb

+2

Składnia dosłowna może być trochę ładniejsza z [{elem for elem in original ...}] – munk

7

Tak można to zrobić, ale jest mało pythonic

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

Niektóre odniesienia na zbiorach można znaleźć in the documentation. Operator * nazywa się unpacking operator.

+2

eww uzgodnione. Unikaj tego jak ognia. Wolę jakieś szczegółowe dla pętli (ale świetna robota Bhargav!) –

+0

Nie potrzebujesz wewnętrznej listy –

+0

@PadraicCunningham Czy wolisz tam genexp? –

2

Innym rozwiązaniem z 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])] 

wykonalne także bez rozpakowywania i stosując zamiast chain.from_iterable.

6

Nieco inne rozwiązanie z użyciem licznika i ze zrozumieniem, aby skorzystać z operatora - dla ustawionej różnicy.

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

Właściwie o tym myślałem po opublikowaniu mojego oryginalnego rozwiązania, chociaż użyłem '&' i zrobiłem zestaw 'unique_elements' zamiast zestawu' dupes'. [Timing] (http://ideone.com/8b70l4) pokazał, że '&' jest o około 30% szybszy niż za każdym razem, gdy zaczynasz rozumieć zestaw na poziomie Pythona. To, czy '&' czy '-' działa lepiej, prawdopodobnie zależy od stopnia duplikacji elementów i od wersji Pythona. – user2357112

+0

Wybór tego rozwiązania jako najlepszej odpowiedzi, ponieważ 1) jest bardzo czytelny, 2) 15% -30% szybszy niż rozwiązanie user2357112 w moich prawdziwych danych światowych – Steve

+0

Bardzo ładne i czytelne rozwiązanie. Początkowo wybrałem tę najlepszą odpowiedź na podstawie czytelności i szybkości. Później zmieniono odpowiedź na user2357112, która po dalszych testach jest znacznie szybsza. – Steve