Menghapus duplikat dari daftar daftar

116

Saya memiliki daftar daftar dengan Python:

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

Dan saya ingin menghapus elemen duplikat darinya. Apakah jika itu daftar normal bukan daftar yang bisa saya gunakan set. Namun sayangnya daftar tersebut tidak dapat di-hash dan tidak dapat dijadikan kumpulan daftar. Hanya tupel. Jadi saya bisa mengubah semua daftar menjadi tupel kemudian menggunakan set dan kembali ke daftar. Tapi ini tidak cepat.

Bagaimana ini bisa dilakukan dengan cara yang paling efisien?

Hasil dari daftar di atas seharusnya:

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

Saya tidak peduli tentang menjaga ketertiban.

Catatan: pertanyaan ini serupa tetapi tidak sesuai dengan yang saya butuhkan. Mencari SO tetapi tidak menemukan duplikat yang tepat.


Pembandingan:

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" (metode kuadrat) tercepat dari semua untuk daftar pendek. Untuk daftar panjang, lebih cepat daripada semua orang kecuali metode groupby. Apakah ini masuk akal?

Untuk daftar singkat (yang ada di kode), 100000 iterasi:

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

Untuk daftar yang lebih panjang (yang ada di kode digandakan 5 kali):

[set] Elapsed: 3.68700003624
[sort] Elapsed: 3.43799996376
[groupby] Elapsed: 1.03099989891
[loop in] Elapsed: 1.85900020599
zaharpopov.dll
sumber
1
Yang Anda maksud dengan "ini tidak cepat", apakah Anda telah mengatur waktunya dan tidak cukup cepat untuk aplikasi Anda, atau menurut Anda itu tidak cepat?
Torsten Marek
@Torsten, sepertinya terlalu banyak menyalin untuk menjadi metode cerdas. maaf, firasat. salin daftar ke tupel, lalu setel, lalu kembali ke daftar (salin lagi tupel ke daftar)
zaharpopov
@zaharpopov: bukan itu cara Python bekerja, tidak ada yang akan disalin , hanya wadah baru untuk elemen yang ada (meskipun untuk int, hampir sama)
Jochen Ritzel
3
1. pengaturan waktu untuk metode yang menggunakan pengurutan dikempiskan, karena "k" dipantulkan ke varian yang diurutkan. 2. Metode terakhir lebih cepat karena cara Anda membuat data pengujian menyisakan paling banyak 4 elemen berbeda. Coba sth. seperti K = [[int (u) for u in str (random.randrange (1, 1000))] untuk _ in range (100)]
Torsten Marek
@Torsten: tetap terima kasih. tetapi tetap saja metode loop cepat bahkan ketika hanya ada satu duplikat dalam daftar 10
zaharpopov

Jawaban:

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

itertoolssering menawarkan solusi tercepat dan paling ampuh untuk masalah seperti ini, dan juga layak mendapatkan akrab dengan -!)

Sunting : seperti yang saya sebutkan dalam komentar, upaya pengoptimalan normal difokuskan pada input besar (pendekatan O besar) karena jauh lebih mudah sehingga menawarkan pengembalian yang baik atas upaya. Tapi kadang-kadang (pada dasarnya untuk "kemacetan yang sangat penting" dalam loop dalam kode yang mendorong batas-batas batas kinerja) seseorang mungkin perlu menjelaskan lebih detail, menyediakan distribusi probabilitas, memutuskan ukuran kinerja mana yang akan dioptimalkan (mungkin batas atas atau persentil ke-90 lebih penting daripada rata-rata atau median, bergantung pada aplikasinya), melakukan pemeriksaan kemungkinan heuristik di awal untuk memilih algoritme yang berbeda bergantung pada karakteristik data masukan, dan seterusnya.

Pengukuran yang cermat dari kinerja "titik" (kode A vs kode B untuk input tertentu) adalah bagian dari proses yang sangat mahal ini, dan modul pustaka standar timeit membantu di sini. Namun, lebih mudah menggunakannya pada prompt shell. Misalnya, berikut adalah modul singkat untuk menunjukkan pendekatan umum untuk masalah ini, simpan sebagai 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))

Perhatikan pemeriksaan kewarasan (dilakukan saat Anda baru saja melakukannya python nodup.py) dan teknik pengangkatan dasar (buat nama global konstan menjadi lokal untuk setiap fungsi untuk kecepatan) untuk menempatkan segala sesuatunya pada pijakan yang sama.

Sekarang kita dapat menjalankan pemeriksaan pada daftar contoh kecil:

$ 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

mengkonfirmasikan bahwa pendekatan kuadrat memiliki konstanta yang cukup kecil untuk membuatnya menarik untuk daftar kecil dengan sedikit nilai duplikat. Dengan daftar singkat tanpa duplikat:

$ 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

pendekatan kuadrat tidak buruk, tetapi jenis dan kelompok yang lebih baik. Dll, dll.

Jika (seperti yang ditunjukkan oleh obsesi dengan kinerja) operasi ini berada pada inti lingkaran dalam dari aplikasi pendorong-batas Anda, ada baiknya mencoba rangkaian pengujian yang sama pada sampel masukan perwakilan lainnya, mungkin mendeteksi beberapa ukuran sederhana yang secara heuristik dapat memungkinkan Anda pilih satu atau pendekatan lain (tetapi ukurannya harus cepat, tentu saja).

Ini juga layak dipertimbangkan untuk mempertahankan representasi yang berbeda untuk k- mengapa itu harus berupa daftar daftar daripada satu set tupel di tempat pertama? Jika tugas penghapusan duplikat sering terjadi, dan pembuatan profil menunjukkannya sebagai penghambat kinerja program, menyimpan sekumpulan tupel sepanjang waktu dan mendapatkan daftar daftar darinya hanya jika dan jika diperlukan, mungkin lebih cepat secara keseluruhan, misalnya.

Alex Martelli
sumber
@ alex terima kasih untuk alternatif. metode ini memiliki kecepatan yang sama dengan danben, beberapa% lebih cepat
zaharpopov
@alex: anehnya ini lebih lambat dari metode kuadrat naif untuk daftar yang lebih pendek (lihat edit pertanyaan)
zaharpopov
@zaharpopov: seperti itu hanya dalam kasus khusus Anda, lih. komentar saya untuk pertanyaan itu.
Torsten Marek
@zaharpopov, jika Anda memberikan distribusi probabilitas panjang list dan sublist serta kemungkinan duplikat, maka mungkin (dengan usaha keras) untuk menghitung / mengukur distribusi probabilitas runtime untuk kode tertentu dan mengoptimalkan ukuran apa pun yang Anda butuhkan (median, mean, 90 sentil, terserah). Ini hampir tidak pernah dilakukan karena ROI yang sangat rendah: biasanya berfokus pada kasus input besar yang jauh lebih mudah (pendekatan big-O), di mana algoritme yang lebih rendah akan sangat merugikan kinerja. Dan saya tidak melihat Anda menentukan distribusi probabilitas dalam Q Anda ;-).
Alex Martelli
@zaharpov, senang Anda menyukainya!
Alex Martelli
21

Melakukannya secara manual, membuat kdaftar baru dan menambahkan entri yang sejauh ini tidak ditemukan:

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

Mudah dipahami, dan Anda mempertahankan urutan kemunculan pertama setiap elemen semestinya itu berguna, tapi saya rasa itu kuadrat dalam kompleksitas saat Anda mencari keseluruhan new_kuntuk setiap elemen.

Paul Stephenson
sumber
@paul: sangat aneh - metode ini lebih cepat dari yang lain
zaharpopov
Saya menduga metode ini tidak akan lebih cepat untuk daftar yang sangat panjang. Ini akan bergantung pada aplikasi Anda: jika Anda benar-benar hanya memiliki daftar enam elemen dengan dua duplikat, maka solusi apa pun kemungkinan besar akan cukup cepat dan Anda harus menggunakan kode yang paling jelas.
Paul Stephenson
@zaharpopov, Ini bukan kuadrat dalam tolok ukur Anda karena Anda menggandakan daftar yang sama berulang kali. Anda melakukan benchmarking dengan case sudut linier.
Mike Graham
k = ([[1, 2], [4], [5, 6, 2], [1, 2], [3], [5, 2], [6], [8], [9]] +[[x] for x in range(1000)]) *5akan menunjukkan perilaku kuadrat dengan baik
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]]

Saya tidak tahu apakah itu pasti lebih cepat, tetapi Anda tidak harus menggunakan tupel dan set.

danben
sumber
Terima kasih danben. ini lebih cepat daripada beralih ke tupel lalu 'set' lalu kembali ke daftar?
zaharpopov
Anda dapat dengan mudah mengujinya - tulis kedua metode deduping, buat beberapa daftar acak menggunakan random, dan atur waktunya time.
danben
4

Semua setsolusi terkait untuk masalah ini sejauh ini memerlukan pembuatan keseluruhan setsebelum iterasi.

Hal ini dimungkinkan untuk membuat ini malas, dan pada saat yang sama mempertahankan ketertiban, dengan mengulang daftar daftar dan menambahkan ke "seen" set. Kemudian hanya menghasilkan daftar jika tidak ditemukan di pelacak ini set.

Ini unique_everseenresep tersedia di itertools docs . Ini juga tersedia di toolzperpustakaan pihak ketiga :

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

Perhatikan bahwa tuplekonversi diperlukan karena daftar tidak dapat dicirikan.

jpp
sumber
3

Bahkan daftar "panjang" Anda cukup pendek. Juga, apakah Anda memilihnya untuk dicocokkan dengan data sebenarnya? Performa akan berbeda dengan tampilan data yang sebenarnya. Misalnya, Anda memiliki daftar pendek yang berulang-ulang untuk membuat daftar yang lebih panjang. Ini berarti bahwa solusi kuadrat dalam tolok ukur Anda adalah linier, tetapi tidak dalam kenyataannya.

Untuk daftar yang benar-benar besar, set kode adalah taruhan terbaik Anda — ini linier (meskipun haus ruang). Metode sort dan groupby adalah O (n log n) dan metode loop in jelas kuadrat, jadi Anda tahu bagaimana ini akan diskalakan saat n menjadi sangat besar. Jika ini adalah ukuran sebenarnya dari data yang Anda analisis, lalu siapa yang peduli? Itu kecil.

Kebetulan, saya melihat percepatan yang nyata jika saya tidak membentuk daftar perantara untuk membuat set, artinya jika saya mengganti

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

dengan

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

Solusi sebenarnya mungkin bergantung pada lebih banyak informasi: Apakah Anda yakin bahwa daftar daftar benar-benar merupakan representasi yang Anda butuhkan?

Mike Graham
sumber
3

Daftar tupel dan {} bisa digunakan untuk menghapus duplikat

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

Buat kamus dengan tupel sebagai kuncinya, dan cetak kuncinya.

  • buat kamus dengan tupel sebagai kunci dan indeks sebagai nilai
  • mencetak daftar kunci kamus

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]]
SuperNova
sumber
1

Ini seharusnya berhasil.

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]]
Zoe L.
sumber
0

Anehnya, jawaban di atas menghapus 'duplikat' tetapi bagaimana jika saya juga ingin menghapus nilai duplikat ?? Berikut ini akan berguna dan tidak membuat objek baru di memori!

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)

dan output daya adalah:

[[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']]
zorze
sumber
-1

Solusi lain yang mungkin lebih umum dan sederhana adalah membuat kamus yang dikunci oleh versi string dari objek dan mendapatkan nilai () di akhir:

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

Masalahnya adalah ini hanya berfungsi untuk objek yang representasi stringnya merupakan kunci unik yang cukup baik (yang berlaku untuk sebagian besar objek native).

jacmkno.dll
sumber