Operazione di sottrazione dell'elenco Python


227

Voglio fare qualcosa di simile a questo:

>>> x = [1,2,3,4,5,6,7,8,9,0]  
>>> x  
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]  
>>> y = [1,3,5,7,9]  
>>> y  
[1, 3, 5, 7, 9]  
>>> y - x   # (should return [2,4,6,8,0])

Ma questo non è supportato dalle liste di Python Qual è il modo migliore per farlo?


@ezdazuzena questa non è sottrazione. Questa è la differenza tra due elenchi. La tua condivisione non è una pubblicazione di questa domanda.
Celik

1
Cosa dovrebbe restituire [2, 2] - [2]? []? [2]?
McKay,

@McKay [2,2] - [2] dovrebbe restituire [2]. [2,2] - [1,2,2,3] dovrebbe tornare []
Robino il

Questa domanda riguarda la sottrazione dell'elenco ma la risposta accettata è più vicina all'impostazione della sottrazione.
Robino,

2
Cosa dovrebbe tornare [2, 1, 2, 3, 2, 4, 2] - [2, 3, 2] e perché? Dovrebbe trovare il 232 nel mezzo e restituire il 2142? o dovrebbe trovare il primo ogni volta e restituire 1242? O qualcos'altro? Quello che sto dicendo è che queste non sono risposte ovvie e dipendono dalle necessità.
McKay,

Risposte:


330

Usa una comprensione dell'elenco:

[item for item in x if item not in y]

Se vuoi usare la -sintassi infix, puoi semplicemente fare:

class MyList(list):
    def __init__(self, *args):
        super(MyList, self).__init__(args)

    def __sub__(self, other):
        return self.__class__(*[item for item in self if item not in other])

puoi quindi usarlo come:

x = MyList(1, 2, 3, 4)
y = MyList(2, 5, 2)
z = x - y   

Ma se non hai assolutamente bisogno delle proprietà dell'elenco (ad esempio, l'ordinamento), usa solo i set come le altre risposte raccomandano.


10
@admica, non utilizzare listper i nomi delle variabili in quanto ombreggia il listcostruttore. Se usi 'list', ti preghiamo di precederlo con un trattino basso. Inoltre, rilasciando *, hai rotto il mio codice ...
aaronasterling

19
Se lo fai [1,1,2,2] - [1,2]otterrai un elenco vuoto. [1,1,2,2] - [2][1,1]Quindi non è in realtà una lista di sottrazione, è più simile a "Elenco da Elenco X senza elementi dall'insieme Y " .
Alfred Zien,

@AlfredZien quello che ha detto
RetroCode

Il metodo di comprensione dell'elenco è molto più lento (nel mio esempio) rispetto al metodo della differenza impostata.
Redfiloux,

1
@BarnabasSzabolcs: Questo non salverà nulla, perché si convertirà yin un setprima di ogni controllo (che è un costo simile al lavoro originale). Avresti bisogno di fare yset = set(y)al di fuori di listcomp, quindi testare if item not in yset, o come un trucco egregio, [item for yset in [set(y)] for item in x if item not in yset]che abusa di listcom annidati per memorizzare nella cache ysetcome una riga . Una soluzione unilinea leggermente meno brutta che si comporti adeguatamente sarebbe da usare list(itertools.filterfalse(set(y).__contains__, x))perché l'argomento a filterfalseè costruito solo una volta.
ShadowRanger,

259

Usa la differenza impostata

>>> z = list(set(x) - set(y))
>>> z
[0, 8, 2, 4, 6]

Oppure potresti avere solo xey in modo da non dover fare alcuna conversione.


50
questo perderà qualsiasi ordine. Ciò può o meno avere importanza a seconda del contesto.
aaronasterling

63
Ciò perderà anche eventuali duplicati che potrebbero essere necessari / desiderati da mantenere.
Opale

RicevoTypeError: unhashable type: 'dict'
Havnar il

Questo è molto più veloce nei casi in cui le liste confrontate sono grandi
JqueryToAddNumbers

2
Se l'ordinazione e i duplicati degli articoli nell'elenco non sono importanti per il contesto, questa è un'ottima risposta e molto leggibile.
Watt Iamsuri,

37

Questa è un'operazione "imposta sottrazione". Utilizzare la struttura di dati impostata per questo.

In Python 2.7:

x = {1,2,3,4,5,6,7,8,9,0}
y = {1,3,5,7,9}
print x - y

Produzione:

>>> print x - y
set([0, 8, 2, 4, 6])

1
list (set ([1,2,3,4,5]) - set ([1,2,3])) = [4, 5] in modo che sia ciascuno di essi da impostare per primo, quindi sottrarre (o diff ) e torna all'elenco.
gseattle,

2
Non va bene se ti piace mantenere l'ordine degli oggetti originali dell'insieme x.
Zahran,

34

se gli articoli duplicati e ordinati sono un problema:

[i for i in a if not i in b or b.remove(i)]

a = [1,2,3,3,3,3,4]
b = [1,3]
result: [2, 3, 3, 3, 4]

2
Funziona, sebbene sia O(m * n)runtime (e io rabbrividisco ogni volta che un listcomp include effetti collaterali); puoi migliorarlo usandocollections.Counter per ottenere il O(m + n)runtime.
ShadowRanger,

Sto facendo fatica a capirlo, qualcuno può spiegare?
Anushka,

20

Per molti casi d'uso, la risposta che desideri è:

ys = set(y)
[item for item in x if item not in ys]

Questo è un ibrido tra la risposta di aaronasterling e la risposta di quantumSoup .

La versione di aaronasterling len(y)confronta gli elementi per ogni elemento in x, quindi richiede un tempo quadratico. La versione di quantumSoup utilizza set, quindi esegue una singola ricerca di set a tempo costante per ogni elemento in x—ma, poiché converte entrambi x e yin set, perde l'ordine dei tuoi elementi.

Convertendo solo yin un set e ripetendo xin ordine, ottieni il meglio da entrambi i mondi: tempo lineare e conservazione dell'ordine. *


Tuttavia, questo ha ancora un problema dalla versione di quantumSoup: richiede che i tuoi elementi siano hash. È praticamente integrato nella natura dei set. ** Se stai provando, ad esempio, a sottrarre un elenco di dadi da un altro elenco di dadi, ma l'elenco da sottrarre è grande, cosa fai?

Se riesci a decorare i tuoi valori in qualche modo che sono hash, questo risolve il problema. Ad esempio, con un dizionario semplice i cui valori sono essi stessi cancellabili:

ys = {tuple(item.items()) for item in y}
[item for item in x if tuple(item.items()) not in ys]

Se i tuoi tipi sono un po 'più complicati (ad esempio, spesso hai a che fare con valori compatibili con JSON, che sono hash, o elenchi o dicts i cui valori sono ricorsivamente dello stesso tipo), puoi comunque usare questa soluzione. Ma alcuni tipi non possono essere convertiti in nulla di hash.


Se i tuoi articoli non sono, e non possono essere realizzati, hash, ma sono comparabili, puoi almeno ottenere un tempo log-linear ( O(N*log M)che è molto meglio del O(N*M)tempo della soluzione dell'elenco, ma non buono come il O(N+M)tempo della soluzione impostata) ordinando e utilizzando bisect:

ys = sorted(y)
def bisect_contains(seq, item):
    index = bisect.bisect(seq, item)
    return index < len(seq) and seq[index] == item
[item for item in x if bisect_contains(ys, item)]

Se i tuoi articoli non sono né lavabili né confrontabili, allora sei bloccato con la soluzione quadratica.


* Nota che puoi anche farlo usando una coppia di OrderedSetoggetti, per i quali puoi trovare ricette e moduli di terze parti. Ma penso che sia più semplice.

** Il motivo per cui le ricerche impostate sono a tempo costante è che tutto ciò che deve fare è hash il valore e vedere se c'è una voce per quell'hash. Se non è possibile eseguire l'hashing del valore, questo non funzionerà.


7

Cercare i valori negli insiemi è più veloce che cercarli negli elenchi:

[item for item in x if item not in set(y)]

Credo che questo ridimensionerà leggermente meglio di:

[item for item in x if item not in y]

Entrambi preservano l'ordine delle liste.


Memorizzerà nella cache set(y)e non si convertirà yin un nuovo set su ogni ciclo? In caso contrario, avresti risposto bisogno di abarnert: ys = set(y); [i for i in x if i not in ys].
Jacktose,

2
Alcuni test approssimativi suggeriscono che il if i not in set(y)25% di tempo in più rispetto a if i not in y(dove si ytrova un elenco). La pre-conversione del set richiede il 55% di tempo in meno. Testato con piuttosto breve xe y, ma le differenze dovrebbero essere più pronunciate con la lunghezza, se non altro.
Jacktose,

1
@Jacktose: Sì, questa soluzione funziona di più, perché deve iterare e hash ogni elemento yper ogni elemento di x; a meno che il confronto di uguaglianza non sia veramente costoso rispetto al calcolo dell'hash, questo perderà sempre in chiaro item not in y.
ShadowRanger,

@ShadowRanger che ha un senso. Se impostare la conversione fosse un modo affidabile più veloce per fare quel controllo, penseresti che il compilatore farebbe sempre il controllo in quel modo.
Jacktose,

5

Se gli elenchi consentono elementi duplicati, puoi utilizzare Contatore dalle raccolte:

from collections import Counter
result = list((Counter(x)-Counter(y)).elements())

Se è necessario preservare l'ordine degli elementi da x:

result = [ v for c in [Counter(y)] for v in x if not c[v] or c.subtract([v]) ]

Questo è buono, anche se perde l'ordinamento; la correzione è un po 'più complicata .
ShadowRanger,

@ShadowRanger, lo è davvero. ma solo un po '.
Alain T.

Non ti preoccupare, vado solo a rabbrividire alle liste con la cache e gli effetti collaterali (anche se suppongo che la combinazione dei due rimuova gli effetti collaterali visibili esternamente?). :-)
ShadowRanger

Inoltre, questo codice non funzionerà come scritto; Counter.subtractnon rimuove gli elementi a valore zero ( -e lo -=fanno, ma non subtract), quindi non smetteresti mai di rimuoverli. Si desidera sostituire not v in ccon not c[v](che restituisce zero per gli elementi inesistenti, in modo da poter testare in sicurezza il ritorno per "zeroiness" tramite not).
ShadowRanger,

@ShadowRanger, Buona cattura! Risolto il problema ora.
Alain T.,

3

Penso che il modo più semplice per raggiungere questo obiettivo sia usando set ().

>>> x = [1,2,3,4,5,6,7,8,9,0]  
>>> y = [1,3,5,7,9]  
>>> list(set(x)- set(y))
[0, 2, 4, 6, 8]

3

Le altre soluzioni presentano uno dei pochi problemi:

  1. Non preservano l'ordine, o
  2. Non rimuovono un conteggio preciso di elementi, ad esempio per x = [1, 2, 2, 2]e y = [2, 2]si convertono yin a set, o rimuovono tutti gli elementi corrispondenti ( [1]solo lasciando ) o rimuovono uno di ciascun elemento unico (lasciando [1, 2, 2]), quando il comportamento corretto sarebbe rimuovere 2due volte, in partenza[1, 2] , o
  3. Funzionano O(m * n), dove una soluzione ottimale può fare il O(m + n)lavoro

Alain era sulla buona stradaCounter per risolvere il n. 2 e il n. 3, ma quella soluzione perderà gli ordini. La soluzione che conserva l'ordine (rimuovendo le prime ncopie di ciascun valore per le nripetizioni listdei valori da rimuovere) è:

from collections import Counter

x = [1,2,3,4,3,2,1]  
y = [1,2,2]  
remaining = Counter(y)

out = []
for val in x:
    if remaining[val]:
        remaining[val] -= 1
    else:
        out.append(val)
# out is now [3, 4, 3, 1], having removed the first 1 and both 2s.

Provalo online!

Per farlo rimuovere le ultime copie di ogni elemento, basta cambiare il forciclo in for val in reversed(x):e aggiungere out.reverse()immediatamente dopo essere usciti dal forciclo.

Costruendo il valore Counterè O(n)in termini di ylunghezza, l'iterazione xè O(n)in termini di xlunghezza e i Countertest di appartenenza e la mutazione sono O(1), mentre list.appendsono ammortizzati O(1)(un dato appendpuò essere O(n), ma per molti appendsecondi, le medie generali di O maggiore O(1)da sempre meno di questi richiede una riallocazione), quindi il lavoro complessivo svolto è O(m + n).

Puoi anche provare per determinare se c'erano elementi da ycui non sono stati rimossi dal xtest:

remaining = +remaining  # Removes all keys with zero counts from Counter
if remaining:
    # remaining contained elements with non-zero counts

Nota: Questo non richiede i valori per essere hashable, ma qualsiasi soluzione che non richiede oggetti hashabili o non è di uso generale (es può contare ints nella matrice di lunghezza fissa) o ha a che fare più di O(m + n)lavoro (ad esempio la migliore grande prossimo -O sarebbe fare una sorta listdi coppie valore / conteggi univoci, cambiando le O(1) dictricerche in O(log n)ricerche binarie; avresti bisogno di valori univoci con i loro conteggi, non solo valori non unici ordinati, perché altrimenti pagheresti i O(n)costi per rimuovere il elementi dal ordinati list).
ShadowRanger,

2

Prova questo.

def subtract_lists(a, b):
    """ Subtracts two lists. Throws ValueError if b contains items not in a """
    # Terminate if b is empty, otherwise remove b[0] from a and recurse
    return a if len(b) == 0 else [a[:i] + subtract_lists(a[i+1:], b[1:]) 
                                  for i in [a.index(b[0])]][0]

>>> x = [1,2,3,4,5,6,7,8,9,0]
>>> y = [1,3,5,7,9]
>>> subtract_lists(x,y)
[2, 4, 6, 8, 0]
>>> x = [1,2,3,4,5,6,7,8,9,0,9]
>>> subtract_lists(x,y)
[2, 4, 6, 8, 0, 9]     #9 is only deleted once
>>>

1

La risposta fornita da @aaronasterling sembra buono, tuttavia, non è compatibile con l'interfaccia predefinita di lista: x = MyList(1, 2, 3, 4)vs x = MyList([1, 2, 3, 4]). Pertanto, il codice seguente può essere utilizzato come un più amichevole elenco Python:

class MyList(list):
    def __init__(self, *args):
        super(MyList, self).__init__(*args)

    def __sub__(self, other):
        return self.__class__([item for item in self if item not in other])

Esempio:

x = MyList([1, 2, 3, 4])
y = MyList([2, 5, 2])
z = x - y

0

Penso che questo sia più veloce:

In [1]: a = [1,2,3,4,5]

In [2]: b = [2,3,4,5]

In [3]: c = set(a) ^ set(b)

In [4]: c
Out[4]: {1}

Questa non è sottrazione. In effetti, questa è la differenza simmetrica tra due elenchi.
Parth Chauhan,

Inoltre, questo funziona solo per gli oggetti hash all'interno degli elenchi
zhukovgreen,

-1

Questo esempio sottrae due elenchi:

# List of pairs of points
list = []
list.append([(602, 336), (624, 365)])
list.append([(635, 336), (654, 365)])
list.append([(642, 342), (648, 358)])
list.append([(644, 344), (646, 356)])
list.append([(653, 337), (671, 365)])
list.append([(728, 13), (739, 32)])
list.append([(756, 59), (767, 79)])

itens_to_remove = []
itens_to_remove.append([(642, 342), (648, 358)])
itens_to_remove.append([(644, 344), (646, 356)])

print("Initial List Size: ", len(list))

for a in itens_to_remove:
    for b in list:
        if a == b :
            list.remove(b)

print("Final List Size: ", len(list))

8
Evita questo, è O (N ^ 2)
Alexander - Ripristina Monica
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.