Rimozione di duplicati da un elenco di elenchi


116

Ho un elenco di elenchi in Python:

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

E voglio rimuovere gli elementi duplicati da esso. Se fosse un normale elenco non di elenchi che potrei usare set. Ma sfortunatamente quell'elenco non è modificabile e non può creare un insieme di elenchi. Solo di tuple. Quindi posso trasformare tutti gli elenchi in tuple, quindi utilizzare set e tornare agli elenchi. Ma non è veloce.

Come è possibile farlo nel modo più efficiente?

Il risultato dell'elenco precedente dovrebbe essere:

k = [[5, 6, 2], [1, 2], [3], [4]]

Non mi interessa preservare l'ordine.

Nota: questa domanda è simile ma non proprio quella di cui ho bisogno. Ricerca SO ma non è stata trovata la copia esatta.


Analisi comparativa:

import itertools, time


class Timer(object):
    def __init__(self, name=None):
        self.name = name

    def __enter__(self):
        self.tstart = time.time()

    def __exit__(self, type, value, traceback):
        if self.name:
            print '[%s]' % self.name,
        print 'Elapsed: %s' % (time.time() - self.tstart)


k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] * 5
N = 100000

print len(k)

with Timer('set'):
    for i in xrange(N):
        kt = [tuple(i) for i in k]
        skt = set(kt)
        kk = [list(i) for i in skt]


with Timer('sort'):
    for i in xrange(N):
        ks = sorted(k)
        dedup = [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]


with Timer('groupby'):
    for i in xrange(N):
        k = sorted(k)
        dedup = list(k for k, _ in itertools.groupby(k))

with Timer('loop in'):
    for i in xrange(N):
        new_k = []
        for elem in k:
            if elem not in new_k:
                new_k.append(elem)

"loop in" (metodo quadratico) più veloce di tutti per gli elenchi brevi. Per elenchi lunghi è più veloce di tutti tranne il metodo groupby. Questo ha senso?

Per una breve lista (quella nel codice), 100000 iterazioni:

[set] Elapsed: 1.3900001049
[sort] Elapsed: 0.891000032425
[groupby] Elapsed: 0.780999898911
[loop in] Elapsed: 0.578000068665

Per un elenco più lungo (quello nel codice duplicato 5 volte):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599

1
Con "questo non è veloce", vuoi dire che l'hai cronometrato e non è abbastanza veloce per la tua applicazione, o che pensi che non sia veloce?
Torsten Marek

@Torsten, sembra che copiare troppo per essere un metodo intelligente. scusa, istinto. copia gli elenchi in tuple, poi in set, quindi torna all'elenco di elenchi (copia di nuovo le tuple in elenchi)
zaharpopov

@zaharpopov: non è così che funziona Python, nulla verrà copiato , solo nuovi contenitori per gli elementi esistenti (anche se per gli int, è più o meno lo stesso)
Jochen Ritzel

3
1. i tempi per i metodi che utilizzano l'ordinamento vengono ridotti, perché "k" viene rimandato alla variante ordinata. 2. L'ultimo metodo è più veloce perché il modo in cui generi i dati del test ti lascia con al massimo 4 elementi distinti. Prova qc. come K = [[int (u) for u in str (random.randrange (1, 1000))] for _ in range (100)]
Torsten Marek

@Torsten: fisso grazie. ma il metodo del ciclo è ancora veloce anche quando c'è un solo duplicato nell'elenco di 10
zaharpopov

Risposte:


167
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> import itertools
>>> k.sort()
>>> list(k for k,_ in itertools.groupby(k))
[[1, 2], [3], [4], [5, 6, 2]]

itertoolsspesso offre le soluzioni più veloci e più potente per questo tipo di problemi, ed è ben la pena di ottenere intimamente familiare con -!)

Modifica : come ho detto in un commento, i normali sforzi di ottimizzazione si concentrano su input di grandi dimensioni (l'approccio big-O) perché è molto più semplice che offre buoni ritorni sugli sforzi. Ma a volte (essenzialmente per "colli di bottiglia tragicamente cruciali" in profondi loop interni di codice che spingono i confini dei limiti delle prestazioni) potrebbe essere necessario entrare molto più in dettaglio, fornendo distribuzioni di probabilità, decidendo quali misure di prestazioni ottimizzare (forse il limite superiore o il 90 ° centile è più importante di una media o mediana, a seconda delle proprie app), eseguendo eventualmente controlli euristici all'inizio per scegliere algoritmi diversi a seconda delle caratteristiche dei dati di input, e così via.

Misurazioni accurate delle prestazioni "puntuali" (codice A vs codice B per un input specifico) fanno parte di questo processo estremamente costoso, e il modulo della libreria standard timeitaiuta qui. Tuttavia, è più facile usarlo al prompt della shell. Ad esempio, ecco un breve modulo per mostrare l'approccio generale a questo problema, salvalo come nodup.py:

import itertools

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

def doset(k, map=map, list=list, set=set, tuple=tuple):
  return map(list, set(map(tuple, k)))

def dosort(k, sorted=sorted, xrange=xrange, len=len):
  ks = sorted(k)
  return [ks[i] for i in xrange(len(ks)) if i == 0 or ks[i] != ks[i-1]]

def dogroupby(k, sorted=sorted, groupby=itertools.groupby, list=list):
  ks = sorted(k)
  return [i for i, _ in itertools.groupby(ks)]

def donewk(k):
  newk = []
  for i in k:
    if i not in newk:
      newk.append(i)
  return newk

# sanity check that all functions compute the same result and don't alter k
if __name__ == '__main__':
  savek = list(k)
  for f in doset, dosort, dogroupby, donewk:
    resk = f(k)
    assert k == savek
    print '%10s %s' % (f.__name__, sorted(resk))

Nota il controllo di integrità (eseguito quando lo fai python nodup.py) e la tecnica di sollevamento di base (fai nomi globali costanti locali a ciascuna funzione per la velocità) per mettere le cose su un piano di parità.

Ora possiamo eseguire controlli sul piccolo elenco di esempi:

$ python -mtimeit -s'import nodup' 'nodup.doset(nodup.k)'
100000 loops, best of 3: 11.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort(nodup.k)'
100000 loops, best of 3: 9.68 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby(nodup.k)'
100000 loops, best of 3: 8.74 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.donewk(nodup.k)'
100000 loops, best of 3: 4.44 usec per loop

confermando che l'approccio quadratico ha costanti sufficientemente piccole da renderlo attraente per elenchi minuscoli con pochi valori duplicati. Con un breve elenco senza duplicati:

$ python -mtimeit -s'import nodup' 'nodup.donewk([[i] for i in range(12)])'
10000 loops, best of 3: 25.4 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dogroupby([[i] for i in range(12)])'
10000 loops, best of 3: 23.7 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.doset([[i] for i in range(12)])'
10000 loops, best of 3: 31.3 usec per loop
$ python -mtimeit -s'import nodup' 'nodup.dosort([[i] for i in range(12)])'
10000 loops, best of 3: 25 usec per loop

l'approccio quadratico non è male, ma quelli ordinati e raggruppati sono migliori. Ecc. Ecc.

Se (come suggerisce l'ossessione per le prestazioni) questa operazione è al centro del ciclo interno della tua applicazione che spinge i confini, vale la pena provare la stessa serie di test su altri campioni di input rappresentativi, eventualmente rilevando qualche semplice misura che potrebbe consentire euristicamente scegli uno o l'altro approccio (ma la misura deve essere veloce, ovviamente).

Vale anche la pena considerare di mantenere una rappresentazione diversa per k: perché deve essere un elenco di elenchi piuttosto che un insieme di tuple in primo luogo? Se l'attività di rimozione dei duplicati è frequente e la profilazione mostra che si tratta del collo di bottiglia delle prestazioni del programma, mantenere un insieme di tuple tutto il tempo e ottenere un elenco di elenchi da esso solo se e dove necessario, potrebbe essere complessivamente più veloce, ad esempio.


@alex grazie per l'alternativa. questo metodo circa la stessa velocità di Danben, un po 'più veloce
zaharpopov

@alex: stranamente questo è più lento di un metodo quadratico ingenuo per elenchi più brevi (vedi modifica domanda)
zaharpopov

@zaharpopov: è così solo nel tuo caso speciale, cfr. il mio commento alla domanda.
Torsten Marek

@zaharpopov, se fornisci una distribuzione di probabilità delle lunghezze di elenchi e sottoliste e possibilità di duplicati, è possibile (con enorme sforzo) calcolare / misurare la distribuzione di probabilità dei runtime per un dato codice e ottimizzare qualsiasi misura di cui hai bisogno (mediana, media, 90 ° centile, qualunque cosa). Non viene quasi mai fatto a causa del ROI molto basso: normalmente ci si concentra sul caso molto più semplice di input di grandi dimensioni (l'approccio big-O), dove algoritmi inferiori danneggerebbero terribilmente le prestazioni. E comunque non vedo che specifichi alcuna distribuzione di probabilità nella tua Q ;-).
Alex Martelli

@zaharpov, contento che ti sia piaciuta!
Alex Martelli

21

Farlo manualmente, creare un nuovo kelenco e aggiungere voci non trovate finora:

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
new_k = []
for elem in k:
    if elem not in new_k:
        new_k.append(elem)
k = new_k
print k
# prints [[1, 2], [4], [5, 6, 2], [3]]

Semplice da comprendere, e si preserva l'ordine della prima occorrenza di ogni elemento dovrebbe essere utile, ma immagino che sia quadratico in complessità poiché stai cercando l'intero new_kelemento per ogni elemento.


@paul: molto strano - questo metodo è più veloce di tutti gli altri
zaharpopov

Sospetto che questo metodo non sarà più veloce per elenchi molto lunghi. Dipenderà dalla tua applicazione: se hai solo elenchi di sei elementi con due duplicati, è probabile che qualsiasi soluzione sia abbastanza veloce e dovresti scegliere il codice più chiaro.
Paul Stephenson

@zaharpopov, non è quadratico nel tuo benchmark perché duplichi lo stesso elenco più e più volte. Stai effettuando il benchmarking con un case ad angolo lineare.
Mike Graham

k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5mostrerà bene il comportamento quadratico
John La Rooy

17
>>> k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]
>>> k = sorted(k)
>>> k
[[1, 2], [1, 2], [3], [4], [4], [5, 6, 2]]
>>> dedup = [k[i] for i in range(len(k)) if i == 0 or k[i] != k[i-1]]
>>> dedup
[[1, 2], [3], [4], [5, 6, 2]]

Non so se sia necessariamente più veloce, ma non devi usare tuple e set.


Grazie Danben. questo è più veloce che passare alle tuple, quindi "impostare" e poi tornare alle liste?
zaharpopov

Puoi facilmente verificarlo: scrivi entrambi i metodi di deduplicazione, genera alcuni elenchi casuali utilizzando randome calcola il tempo con time.
danben

4

Tutte le setsoluzioni correlate a questo problema finora richiedono la creazione di un intero setprima dell'iterazione.

È possibile rendere questo pigro e allo stesso tempo preservare l'ordine, iterando l'elenco degli elenchi e aggiungendo a un "visto" set. Quindi restituisci un elenco solo se non viene trovato in questo tracker set.

Questa unique_everseenricetta è disponibile nella itertools documentazione . È disponibile anche nella toolzlibreria di terze parti :

from toolz import unique

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

# lazy iterator
res = map(list, unique(map(tuple, k)))

print(list(res))

[[1, 2], [4], [5, 6, 2], [3]]

Tieni presente che la tupleconversione è necessaria perché gli elenchi non sono modificabili.


3

Anche la tua lista "lunga" è piuttosto breve. Inoltre, li hai scelti per abbinare i dati effettivi? Le prestazioni varieranno in base a come appaiono effettivamente questi dati. Ad esempio, hai un breve elenco ripetuto più e più volte per creare un elenco più lungo. Ciò significa che la soluzione quadratica è lineare nei benchmark, ma non nella realtà.

Per elenchi effettivamente grandi, il codice impostato è la soluzione migliore: è lineare (sebbene abbia fame di spazio). I metodi sort e groupby sono O (n log n) e il metodo loop in è ovviamente quadratico, quindi sai come scaleranno man mano che n diventa davvero grande. Se questa è la dimensione reale dei dati che stai analizzando, a chi importa? È minuscolo.

Per inciso, vedo un notevole aumento della velocità se non creo un elenco intermedio per creare il set, vale a dire se sostituisco

kt = [tuple(i) for i in k]
skt = set(kt)

con

skt = set(tuple(i) for i in k)

La vera soluzione può dipendere da maggiori informazioni: sei sicuro che un elenco di elenchi sia davvero la rappresentazione di cui hai bisogno?


3

Elenco di tupla e {} possono essere utilizzati per rimuovere i duplicati

>>> [list(tupl) for tupl in {tuple(item) for item in k }]
[[1, 2], [5, 6, 2], [3], [4]]
>>> 

1

Crea un dizionario con tupla come chiave e stampa le chiavi.

  • crea un dizionario con tupla come chiave e indice come valore
  • stampa l'elenco delle chiavi del dizionario

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

dict_tuple = {tuple(item): index for index, item in enumerate(k)}

print [list(itm) for itm in dict_tuple.keys()]

# prints [[1, 2], [5, 6, 2], [3], [4]]

1

Questo dovrebbe funzionare.

k = [[1, 2], [4], [5, 6, 2], [1, 2], [3], [4]]

k_cleaned = []
for ele in k:
    if set(ele) not in [set(x) for x in k_cleaned]:
        k_cleaned.append(ele)
print(k_cleaned)

# output: [[1, 2], [4], [5, 6, 2], [3]]

0

Stranamente, le risposte sopra rimuovono i "duplicati", ma cosa succede se voglio rimuovere anche il valore duplicato ?? Quanto segue dovrebbe essere utile e non crea un nuovo oggetto in memoria!

def dictRemoveDuplicates(self):
    a=[[1,'somevalue1'],[1,'somevalue2'],[2,'somevalue1'],[3,'somevalue4'],[5,'somevalue5'],[5,'somevalue1'],[5,'somevalue1'],[5,'somevalue8'],[6,'somevalue9'],[6,'somevalue0'],[6,'somevalue1'],[7,'somevalue7']]


print(a)
temp = 0
position = -1
for pageNo, item in a:
    position+=1
    if pageNo != temp:
        temp = pageNo
        continue
    else:
        a[position] = 0
        a[position - 1] = 0
a = [x for x in a if x != 0]         
print(a)

e l'o / p è:

[[1, 'somevalue1'], [1, 'somevalue2'], [2, 'somevalue1'], [3, 'somevalue4'], [5, 'somevalue5'], [5, 'somevalue1'], [5, 'somevalue1'], [5, 'somevalue8'], [6, 'somevalue9'], [6, 'somevalue0'], [6, 'somevalue1'], [7, 'somevalue7']]
[[2, 'somevalue1'], [3, 'somevalue4'], [7, 'somevalue7']]

-1

Un'altra soluzione probabilmente più generica e più semplice è creare un dizionario codificato dalla versione stringa degli oggetti e ottenere i valori () alla fine:

>>> dict([(unicode(a),a) for a in [["A", "A"], ["A", "A"], ["A", "B"]]]).values()
[['A', 'B'], ['A', 'A']]

Il problema è che questo funziona solo per oggetti la cui rappresentazione di stringa è una chiave univoca abbastanza buona (il che è vero per la maggior parte degli oggetti nativi).

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.