dari daftar bilangan bulat, dapatkan nomor terdekat dengan nilai yang diberikan

158

Diberikan daftar bilangan bulat, saya ingin mencari nomor mana yang paling dekat dengan nomor yang saya berikan input:

>>> myList = [4, 1, 88, 44, 3]
>>> myNumber = 5
>>> takeClosest(myList, myNumber)
...
4

Apakah ada cara cepat untuk melakukan ini?

Ricky Robinson
sumber
2
bagaimana juga mengembalikan indeks bahwa ini terjadi dalam daftar.
Charlie Parker
1
@ sancho.s Terlihat dengan baik. Padahal jawaban untuk pertanyaan ini jauh lebih baik daripada jawaban untuk pertanyaan lain itu. Jadi saya akan memilih untuk menutup yang lain sebagai duplikat dari yang ini.
Jean-François Corbett

Jawaban:

327

Jika kita tidak yakin bahwa daftar diurutkan, kita bisa menggunakan built-in min()fungsi , untuk menemukan elemen yang memiliki jarak minimum dari jumlah yang ditentukan.

>>> min(myList, key=lambda x:abs(x-myNumber))
4

Perhatikan bahwa ini juga berfungsi dengan dikt dengan kunci int, seperti {1: "a", 2: "b"}. Metode ini membutuhkan waktu O (n).


Jika daftar sudah diurutkan, atau Anda bisa membayar harga untuk mengurutkan array sekali saja, gunakan metode pembagian dua bagian yang diilustrasikan dalam jawaban @ Lauritz yang hanya membutuhkan waktu O (log n) (perhatikan namun memeriksa apakah daftar yang sudah diurutkan adalah O (n) dan pengurutan adalah O (n log n).)

kennytm
sumber
13
Berbicara dalam kompleksitas, ini adalah O(n), di mana sedikit peretasan bisectakan memberi Anda peningkatan besar-besaran O(log n)(jika array input Anda diurutkan).
mic_e
5
@mic_e: Itu hanya jawaban Lauritz .
kennytm
3
bagaimana dengan juga mengembalikan indeks bahwa ini terjadi dalam daftar?
Charlie Parker
@CharlieParker Buat implementasi Anda sendiri min, jalankan di atas kamus ( items()) alih-alih daftar, dan kembalikan kunci alih-alih nilai pada akhirnya.
Dustin Oprea
2
Atau gunakan numpy.argminalih-alih minuntuk mendapatkan indeks alih-alih nilai.
148

Saya akan mengganti nama fungsi take_closestagar sesuai dengan konvensi penamaan PEP8.

Jika yang Anda maksud adalah eksekusi cepat dan bukan penulisan cepat, minseharusnya tidak menjadi senjata pilihan Anda, kecuali dalam satu kasus penggunaan yang sangat sempit. The minsolusi perlu memeriksa setiap nomor dalam daftar dan melakukan perhitungan untuk setiap nomor. bisect.bisect_leftSebaliknya, menggunakan hampir selalu lebih cepat.

"Hampir" berasal dari fakta yang bisect_leftmengharuskan daftar harus diurutkan untuk bekerja. Mudah-mudahan, use case Anda sedemikian rupa sehingga Anda dapat mengurutkan daftar sekali dan kemudian membiarkannya sendiri. Meskipun tidak, selama Anda tidak perlu menyortir sebelum setiap kali Anda menelepon take_closest, bisectmodul tersebut kemungkinan akan keluar di atas. Jika Anda ragu, coba keduanya dan lihat perbedaan dunia nyata.

from bisect import bisect_left

def take_closest(myList, myNumber):
    """
    Assumes myList is sorted. Returns closest value to myNumber.

    If two numbers are equally close, return the smallest number.
    """
    pos = bisect_left(myList, myNumber)
    if pos == 0:
        return myList[0]
    if pos == len(myList):
        return myList[-1]
    before = myList[pos - 1]
    after = myList[pos]
    if after - myNumber < myNumber - before:
       return after
    else:
       return before

Bisect bekerja dengan berulang kali membagi dua daftar dan mencari tahu bagian mana yang myNumberharus diambil dengan melihat nilai tengahnya. Ini berarti ia memiliki waktu berjalan O (log n) sebagai kebalikan dari O (n) waktu berjalan jawaban tertinggi yang dipilih . Jika kami membandingkan kedua metode dan menyediakan keduanya dengan diurutkan myList, ini adalah hasilnya:

$ python -m timeit -s "
dari impor take_closest terdekat
dari randint impor acak
a = range (-1000, 1000, 10) "" take_closest (a, randint (-1100, 1100)) "

100000 loop, terbaik 3: 2,22 usec per loop

$ python -m timeit -s "
dari impor terdekat with_min
dari randint impor acak
a = range (-1000, 1000, 10) "" with_min (a, randint (-1100, 1100)) "

10.000 loop, terbaik dari 3: 43.9 USD per loop

Jadi dalam tes khusus ini, bisecthampir 20 kali lebih cepat. Untuk daftar yang lebih panjang, perbedaannya akan lebih besar.

Bagaimana jika kita meratakan lapangan bermain dengan menghapus prasyarat yang myListharus diurutkan? Katakanlah kita mengurutkan salinan daftar setiap kali take_closest dipanggil, sementara membiarkan minsolusinya tidak berubah. Menggunakan daftar 200-item dalam tes di atas, bisectsolusinya masih yang tercepat, meskipun hanya sekitar 30%.

Ini adalah hasil yang aneh, mengingat langkah penyortirannya adalah O (n log (n)) ! Satu-satunya alasan minyang masih kalah adalah bahwa penyortiran dilakukan dalam kode c yang sangat dioptimalkan, sementara minharus bersusah payah memanggil fungsi lambda untuk setiap item. Seiring myListbertambahnya ukuran, minsolusinya pada akhirnya akan lebih cepat. Perhatikan bahwa kami harus menumpuk segalanya agar minsolusi dapat menang.

Lauritz V. Thaulow
sumber
2
Penyortiran sendiri membutuhkan O (N log N), sehingga akan lebih lambat saat N menjadi besar. Misalnya, jika Anda menggunakan a=range(-1000,1000,2);random.shuffle(a)Anda akan menemukan itu takeClosest(sorted(a), b)akan menjadi lebih lambat.
kennytm
3
@ KennyTM Saya akan memberikan Anda itu, dan saya akan menunjukkannya dalam jawaban saya. Tetapi selama getClosestmungkin dipanggil lebih dari sekali untuk setiap jenis, ini akan lebih cepat, dan untuk kasus penggunaan semacam-sekali, itu adalah no-brainer.
Lauritz V. Thaulow
bagaimana dengan juga mengembalikan indeks bahwa ini terjadi dalam daftar?
Charlie Parker
Jika myListsudah np.arraymaka gunakan np.searchsorteddi tempat bisectlebih cepat.
Michael Hall
8
>>> takeClosest = lambda num,collection:min(collection,key=lambda x:abs(x-num))
>>> takeClosest(5,[4,1,88,44,3])
4

Sebuah lambda adalah cara khusus menulis "anonim" fungsi (fungsi yang tidak memiliki nama). Anda dapat menetapkan nama apa saja yang Anda inginkan karena lambda adalah ekspresi.

Cara "panjang" untuk menulis hal di atas adalah:

def takeClosest(num,collection):
   return min(collection,key=lambda x:abs(x-num))
Burhan Khalid
sumber
2
Namun perlu dicatat bahwa menugaskan lambda untuk nama tidak disarankan sesuai dengan PEP 8 .
Evert Heylen
6
def closest(list, Number):
    aux = []
    for valor in list:
        aux.append(abs(Number-valor))

    return aux.index(min(aux))

Kode ini akan memberi Anda indeks nomor terdekat dalam daftar.

Solusi yang diberikan oleh KennyTM adalah keseluruhan terbaik, tetapi dalam kasus Anda tidak dapat menggunakannya (seperti brython), fungsi ini akan melakukan pekerjaan

Gustavo Lima
sumber
5

Iterate atas daftar dan bandingkan nomor terdekat saat ini dengan abs(currentNumber - myNumber):

def takeClosest(myList, myNumber):
    closest = myList[0]
    for i in range(1, len(myList)):
        if abs(i - myNumber) < closest:
            closest = i
    return closest
João Silva
sumber
1
Anda juga bisa mengembalikan indeks.
Charlie Parker
1
! Salah! Seharusnya if abs(myList[i] - myNumber) < abs(closest - myNumber): closest = myList[i];. Lebih baik menyimpan nilai itu sebelumnya.
lk_vc
Tentunya fungsi seperti berdiri sudah mengembalikan indeks terdekat. Agar memenuhi persyaratan OP tidak boleh baris terakhir kedua baca terdekat = myList [i]
Paula Livingstone
2

Penting untuk dicatat bahwa ide saran Lauritz menggunakan bisect sebenarnya tidak menemukan nilai terdekat di MyList ke MyNumber. Sebagai gantinya, membagi dua menemukan nilai berikutnya dalam urutan setelah MyNumber di MyList. Jadi dalam kasus OP Anda benar-benar mendapatkan posisi 44 kembali, bukan posisi 4.

>>> myList = [1, 3, 4, 44, 88] 
>>> myNumber = 5
>>> pos = (bisect_left(myList, myNumber))
>>> myList[pos]
...
44

Untuk mendapatkan nilai yang paling dekat dengan 5 Anda bisa mencoba mengubah daftar menjadi array dan menggunakan argmin dari numpy seperti itu.

>>> import numpy as np
>>> myNumber = 5   
>>> myList = [1, 3, 4, 44, 88] 
>>> myArray = np.array(myList)
>>> pos = (np.abs(myArray-myNumber)).argmin()
>>> myArray[pos]
...
4

Saya tidak tahu seberapa cepat ini akan terjadi, tebakan saya akan "tidak terlalu".

jmdeamer
sumber
2
Fungsi Lauritz bekerja dengan benar. Anda hanya menggunakan bisect_left saja tetapi Lauritz menyarankan fungsi takeClosest (...) yang membuat pemeriksaan tambahan.
Kanat
Jika Anda akan menggunakan NumPy, Anda bisa menggunakannya np.searchsortedsebagai ganti bisect_left. Dan @ Kanat benar - solusi Lauritz tidak termasuk kode yang memilih yang mana dari dua kandidat lebih dekat.
John Y
1

Memperluas jawaban Gustavo Lima. Hal yang sama dapat dilakukan tanpa membuat daftar yang sama sekali baru. Nilai-nilai dalam daftar dapat diganti dengan diferensial saat FORloop berlanjut.

def f_ClosestVal(v_List, v_Number):
"""Takes an unsorted LIST of INTs and RETURNS INDEX of value closest to an INT"""
for _index, i in enumerate(v_List):
    v_List[_index] = abs(v_Number - i)
return v_List.index(min(v_List))

myList = [1, 88, 44, 4, 4, -2, 3]
v_Num = 5
print(f_ClosestVal(myList, v_Num)) ## Gives "3," the index of the first "4" in the list.
JayJay123
sumber
1

Jika saya dapat menambahkan jawaban @ Lauritz

Agar tidak ada galat run, jangan lupa menambahkan kondisi sebelum bisect_leftbaris:

if (myNumber > myList[-1] or myNumber < myList[0]):
    return False

jadi kode lengkapnya akan terlihat seperti:

from bisect import bisect_left

def takeClosest(myList, myNumber):
    """
    Assumes myList is sorted. Returns closest value to myNumber.
    If two numbers are equally close, return the smallest number.
    If number is outside of min or max return False
    """
    if (myNumber > myList[-1] or myNumber < myList[0]):
        return False
    pos = bisect_left(myList, myNumber)
    if pos == 0:
            return myList[0]
    if pos == len(myList):
            return myList[-1]
    before = myList[pos - 1]
    after = myList[pos]
    if after - myNumber < myNumber - before:
       return after
    else:
       return before
umn
sumber