Uji apakah daftar membagikan item apa pun dengan python


131

Saya ingin memeriksa apakah ada item dalam satu daftar ada di daftar lain. Saya dapat melakukannya hanya dengan kode di bawah ini, tetapi saya menduga mungkin ada fungsi perpustakaan untuk melakukan ini. Jika tidak, apakah ada metode yang lebih pythonic untuk mencapai hasil yang sama.

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

In [79]: b = [8, 7, 6]

In [80]: c = [8, 7, 6, 5]

In [81]: def lists_overlap(a, b):
   ....:     for i in a:
   ....:         if i in b:
   ....:             return True
   ....:     return False
   ....: 

In [82]: lists_overlap(a, b)
Out[82]: False

In [83]: lists_overlap(a, c)
Out[83]: True

In [84]: def lists_overlap2(a, b):
   ....:     return len(set(a).intersection(set(b))) > 0
   ....: 

Satu-satunya optimisasi yang dapat saya pikirkan adalah menjatuhkan len(...) > 0karena bool(set([]))menghasilkan False. Dan tentu saja jika Anda menyimpan daftar sebagai set untuk memulai, Anda akan menghemat set overhead penciptaan.
msw


1
Perhatikan bahwa Anda tidak dapat membedakan Truedari 1dan Falsedari 0. not set([1]).isdisjoint([True])dapatkan True, sama dengan solusi lain.
Dimali

Jawaban:


313

Jawaban singkat : gunakan not set(a).isdisjoint(b), umumnya yang tercepat.

Ada empat cara umum untuk menguji apakah dua daftar adan bberbagi item apa pun. Opsi pertama adalah mengonversi keduanya menjadi set dan memeriksa persimpangannya, seperti:

bool(set(a) & set(b))

Karena set disimpan menggunakan tabel hash di Python, pencarian mereka adalahO(1) (lihat di sini untuk informasi lebih lanjut tentang kompleksitas operator di Python). Secara teoritis, ini O(n+m)rata-rata untuk ndan mobjek dalam daftar adan b. Tapi 1) pertama-tama ia harus membuat set dari daftar, yang dapat mengambil jumlah waktu yang tidak dapat diabaikan, dan 2) itu mengandaikan bahwa tabrakan hashing jarang di antara data Anda.

Cara kedua untuk melakukannya adalah menggunakan ekspresi generator yang melakukan iterasi pada daftar, seperti:

any(i in a for i in b)

Ini memungkinkan untuk mencari di tempat, sehingga tidak ada memori baru yang dialokasikan untuk variabel perantara. Itu juga menyelamatkan pada penemuan pertama. Tetapi inoperator selalu ada O(n)dalam daftar (lihat di sini ).

Opsi lain yang diusulkan adalah hibrida untuk beralih melalui salah satu daftar, mengonversi yang lain dalam satu set dan menguji keanggotaan pada set ini, seperti:

a = set(a); any(i in a for i in b)

Pendekatan keempat adalah mengambil keuntungan dari isdisjoint()metode set (beku) (lihat di sini ), misalnya:

not set(a).isdisjoint(b)

Jika elemen yang Anda cari berada di dekat awal array (misalnya diurutkan), ekspresi generator lebih disukai, karena metode persimpangan set harus mengalokasikan memori baru untuk variabel perantara:

from timeit import timeit
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=list(range(1000))", number=100000)
26.077727576019242
>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=list(range(1000))", number=100000)
0.16220548999262974

Berikut adalah grafik waktu eksekusi untuk contoh ini dalam fungsi ukuran daftar:

Waktu pelaksanaan pengujian berbagi elemen saat dibagikan di awal

Perhatikan bahwa kedua sumbu bersifat logaritmik. Ini mewakili kasus terbaik untuk ekspresi generator. Seperti dapat dilihat, isdisjoint()metode ini lebih baik untuk ukuran daftar yang sangat kecil, sedangkan ekspresi generator lebih baik untuk ukuran daftar yang lebih besar.

Di sisi lain, saat pencarian dimulai dengan awal untuk ekspresi hybrid dan generator, jika elemen bersama secara sistematis di akhir array (atau kedua daftar tidak berbagi nilai apa pun), maka pendekatan persimpangan disjoint dan set kemudian jauh lebih cepat daripada ekspresi generator dan pendekatan hybrid.

>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000))
13.739536046981812
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000))
0.08102107048034668

Waktu pelaksanaan pengujian berbagi elemen saat dibagikan di akhir

Sangat menarik untuk dicatat bahwa ekspresi generator jauh lebih lambat untuk ukuran daftar yang lebih besar. Ini hanya untuk 1000 repetisi, bukan 100000 untuk gambar sebelumnya. Pengaturan ini juga mendekati dengan baik ketika ketika tidak ada elemen yang dibagikan, dan merupakan kasus terbaik untuk pemisahan dan mengatur pendekatan persimpangan.

Berikut adalah dua analisis menggunakan angka acak (alih-alih mencurangi pengaturan untuk mendukung satu teknik atau lainnya):

Waktu pelaksanaan pengujian berbagi elemen untuk data yang dihasilkan secara acak dengan peluang berbagi yang tinggi Waktu pelaksanaan pengujian berbagi elemen untuk data yang dihasilkan secara acak dengan peluang berbagi yang tinggi

Peluang berbagi yang tinggi: elemen diambil secara acak [1, 2*len(a)] . Peluang berbagi yang rendah: elemen diambil secara acak [1, 1000*len(a)].

Hingga kini, analisis ini seharusnya kedua daftar memiliki ukuran yang sama. Dalam hal dua daftar ukuran yang berbeda, misalnya ajauh lebih kecil, isdisjoint()selalu lebih cepat:

Waktu pelaksanaan pengujian berbagi elemen pada dua daftar berukuran berbeda ketika dibagikan di awal Waktu pelaksanaan pengujian berbagi elemen pada dua daftar berukuran berbeda ketika dibagikan di akhir

Pastikan adaftar tersebut lebih kecil, jika tidak kinerjanya menurun. Dalam percobaan ini, aukuran daftar ditetapkan menjadi5 .

Singkatnya:

  • Jika daftar sangat kecil (<10 elemen), not set(a).isdisjoint(b)selalu yang tercepat.
  • Jika elemen dalam daftar diurutkan atau memiliki struktur reguler yang dapat Anda manfaatkan, ekspresi generator any(i in a for i in b)adalah yang tercepat pada ukuran daftar besar;
  • Tes persimpangan dengan set not set(a).isdisjoint(b), yang selalu lebih cepat dari bool(set(a) & set(b)).
  • Hibrid "iterate through list, test on set" a = set(a); any(i in a for i in b)umumnya lebih lambat daripada metode lain.
  • Ekspresi generator dan hibrida jauh lebih lambat daripada dua pendekatan lain ketika datang ke daftar tanpa berbagi elemen.

Dalam kebanyakan kasus, menggunakan isdisjoint()metode adalah pendekatan terbaik karena ekspresi generator akan membutuhkan waktu lebih lama untuk dieksekusi, karena sangat tidak efisien ketika tidak ada elemen yang dibagikan.


8
Itu beberapa data yang berguna di sana, menunjukkan bahwa analisis big-O bukanlah segalanya dan mengakhiri semua alasan tentang waktu yang berjalan.
Steve Allison

bagaimana dengan skenario terburuk? anykeluar pada nilai non-False pertama. Dengan menggunakan daftar di mana satu-satunya nilai yang cocok adalah di akhir, kami mendapatkan ini: timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,-0,-1)]", number=1000) 13.739536046981812 timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,-0,-1)]", number=1000) 0.08102107048034668 ... dan hanya dengan 1000 iterasi.
RobM

2
Terima kasih @RobM untuk informasinya. Saya telah memperbarui jawaban saya untuk mencerminkan hal ini dan mempertimbangkan teknik-teknik lain yang diusulkan di utas ini.
Soravux

Seharusnya not set(a).isdisjoint(b)untuk menguji apakah dua daftar berbagi anggota. set(a).isdisjoint(b)kembali Truejika kedua daftar tidak membagikan anggota. Jawabannya harus diedit?
Guillochon

1
Terima kasih untuk kepala, @Guillochon, sudah diperbaiki.
Soravux

25
def lists_overlap3(a, b):
    return bool(set(a) & set(b))

Catatan: di atas mengasumsikan bahwa Anda menginginkan boolean sebagai jawabannya. Jika yang Anda butuhkan adalah ekspresi untuk digunakan dalam ifpernyataan, cukup gunakanif set(a) & set(b):


5
Ini adalah kasus terburuk O (n + m). Namun, sisi buruknya adalah ia membuat set baru, dan tidak menyelamatkan ketika elemen umum ditemukan lebih awal.
Matthew Flaschen

1
Saya ingin tahu mengapa ini terjadi O(n + m). Dugaan saya adalah bahwa set diimplementasikan menggunakan tabel hash, dan dengan demikian inoperator dapat bekerja dalam O(1)waktu (kecuali dalam kasus degenerasi). Apakah ini benar? Jika demikian, mengingat bahwa tabel hash memiliki kinerja pencarian kasus terburuk O(n), apakah ini berarti bahwa dalam kasus tidak seperti terburuk itu akan memiliki O(n * m)kinerja?
fmark

1
@ tanda: Secara teoritis, Anda benar. Praktis, tidak ada yang peduli; baca komentar di Objects / dictobject.c di sumber CPython (set hanya dicts dengan kunci saja, tanpa nilai) dan lihat apakah Anda dapat membuat daftar kunci yang akan menyebabkan kinerja pencarian O (n).
John Machin

Oke, terima kasih sudah menjelaskan, saya bertanya-tanya apakah ada keajaiban yang terjadi :). Meskipun saya setuju bahwa secara praktis saya tidak perlu peduli, itu sepele untuk menghasilkan daftar kunci yang akan menyebabkan O(n)kinerja pencarian;), lihat pastebin.com/Kn3kAW7u Hanya untuk lafs.
fmark

2
Ya aku tahu. Ditambah lagi, saya baru saja membaca sumber yang Anda tunjukkan kepada saya, yang mendokumentasikan lebih banyak keajaiban dalam hal fungsi hash non-acak (seperti yang ada di dalamnya). Saya berasumsi bahwa ini memerlukan keacakan, seperti Java, yang menghasilkan monster seperti ini stackoverflow.com/questions/2634690/… . Saya harus terus mengingatkan diri sendiri bahwa Python Is Not Java (terima kasih tuhan!).
fmark

10
def lists_overlap(a, b):
  sb = set(b)
  return any(el in sb for el in a)

Ini asimptotik optimal (kasus terburuk O (n + m)), dan mungkin lebih baik daripada pendekatan persimpangan karena anyhubungan arus pendek.

Misalnya:

lists_overlap([3,4,5], [1,2,3])

akan mengembalikan True segera setelah tiba 3 in sb

EDIT: Variasi lain (dengan terima kasih kepada Dave Kirby):

def lists_overlap(a, b):
  sb = set(b)
  return any(itertools.imap(sb.__contains__, a))

Ini bergantung pada imapiterator, yang diimplementasikan dalam C, daripada pemahaman generator. Ini juga digunakan sb.__contains__sebagai fungsi pemetaan. Saya tidak tahu seberapa besar perbedaan kinerja ini. Itu masih akan mengalami arus pendek.


1
Loop dalam pendekatan persimpangan semuanya dalam kode C; ada satu loop dalam pendekatan Anda yang mencakup kode Python. Yang tidak diketahui besar adalah apakah persimpangan kosong kemungkinan atau tidak.
John Machin

2
Anda juga bisa menggunakan any(itertools.imap(sb.__contains__, a))yang seharusnya lebih cepat karena tidak menggunakan fungsi lambda.
Dave Kirby

Terima kasih, @Dave. :) Saya setuju menghapus lambda adalah sebuah kemenangan.
Matthew Flaschen

4

Anda juga bisa menggunakan anydengan pemahaman daftar:

any([item in a for item in b])

6
Anda bisa, tetapi waktunya adalah O (n * m) sedangkan waktu untuk pendekatan persimpangan ditetapkan adalah O (n + m). Anda juga bisa melakukannya TANPA daftar pemahaman (kehilangan []) dan itu akan berjalan lebih cepat dan menggunakan lebih sedikit memori, tetapi waktunya masih akan O (n * m).
John Machin

1
Sementara analisis O besar Anda benar, saya menduga bahwa untuk nilai kecil n dan m waktu yang diperlukan untuk membangun hashtable yang mendasarinya akan ikut bermain. Big O mengabaikan waktu yang diperlukan untuk menghitung hash.
Anthony Conyers

2
Membangun "hashtable" diamortisasi O (n).
John Machin

1
Saya mengerti tetapi konstanta yang Anda buang cukup besar. Tidak masalah untuk nilai n yang besar, tetapi nilai n kecil.
Anthony Conyers

3

Dengan python 2.6 atau yang lebih baru, Anda dapat melakukan:

return not frozenset(a).isdisjoint(frozenset(b))

1
Sepertinya orang tidak harus menyediakan set atau frozenset sebagai argumen pertama. Saya mencoba dengan sebuah string dan berhasil (yaitu: iterable akan melakukan apa pun).
Aktau

2

Anda dapat menggunakan ekspresi generator fungsi / fungsi bawaan apa saja:

def list_overlap(a,b): 
     return any(i for i in a if i in b)

Seperti yang ditunjukkan oleh John dan Lie, ini memberikan hasil yang salah ketika untuk setiap saya dibagikan oleh dua daftar, bool (i) == Salah. Harus:

return any(i in b for i in a)

1
Menguatkan komentar Lie Ryan: akan memberikan hasil yang salah untuk setiap item x yang ada di persimpangan di mana bool(x)False. Dalam contoh Lie Ryan, x adalah 0. Hanya memperbaiki any(True for i in a if i in b)yang lebih baik ditulis seperti yang sudah terlihat any(i in b for i in a).
John Machin

1
Koreksi: akan memberikan hasil yang salah ketika semua item xdi persimpangan adalah seperti yang bool(x)adalah False.
John Machin

1

Pertanyaan ini cukup lama, tetapi saya perhatikan bahwa sementara orang-orang berdebat set vs daftar, bahwa tidak ada yang berpikir untuk menggunakannya bersama-sama. Mengikuti contoh Soravux,

Kasus terburuk untuk daftar:

>>> timeit('bool(set(a) & set(b))',  setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
100.91506409645081
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
19.746716022491455
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
0.092626094818115234

Dan kasus terbaik untuk daftar:

>>> timeit('bool(set(a) & set(b))',  setup="a=list(range(10000)); b=list(range(10000))", number=100000)
154.69790101051331
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=list(range(10000))", number=100000)
0.082653045654296875
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=list(range(10000))", number=100000)
0.08434605598449707

Jadi, bahkan lebih cepat daripada iterasi melalui dua daftar adalah iterasi daftar untuk melihat apakah itu dalam set, yang masuk akal karena memeriksa apakah nomor dalam set membutuhkan waktu yang konstan sementara memeriksa dengan iterasi melalui daftar membutuhkan waktu sebanding dengan panjang Daftar.

Jadi, kesimpulan saya adalah bahwa iterate melalui daftar, dan periksa apakah sudah di set .


1
Menggunakan isdisjoint()metode pada set (beku) seperti yang ditunjukkan oleh @Toughy bahkan lebih baik: timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)=> 0,00913715362548828
Aktau

1

jika Anda tidak peduli apa elemen yang tumpang tindih mungkin, Anda cukup memeriksa lendaftar gabungan vs daftar digabungkan sebagai satu set. Jika ada elemen yang tumpang tindih, set akan lebih pendek:

len(set(a+b+c))==len(a+b+c) mengembalikan True, jika tidak ada tumpang tindih.


Jika nilai pertama tumpang tindih masih akan mengubah seluruh daftar menjadi satu set, tidak peduli seberapa besar.
Peter Wood

1

Saya akan melempar yang lain dengan gaya pemrograman fungsional:

any(map(lambda x: x in a, b))

Penjelasan:

map(lambda x: x in a, b)

mengembalikan daftar boolean di mana elemen bditemukan di a. Daftar itu kemudian diteruskan ke any, yang hanya mengembalikan Truejika ada elemen True.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.