จากรายการจำนวนเต็มรับจำนวนที่ใกล้เคียงที่สุดกับค่าที่กำหนด


158

รับรายการจำนวนเต็มฉันต้องการค้นหาว่าตัวเลขใดที่ใกล้เคียงกับตัวเลขที่ฉันป้อนมากที่สุด:

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

มีวิธีที่รวดเร็วในการทำเช่นนี้?


2
สิ่งที่เกี่ยวกับการคืนค่าดัชนีว่าสิ่งนี้เกิดขึ้นในรายการ
Charlie Parker


1
@ sancho.s เห็นได้ชัด แม้ว่าคำตอบของคำถามนี้จะดีกว่าคำตอบของคำถามอื่น ดังนั้นฉันจะลงคะแนนเพื่อปิดอีกอันหนึ่งเป็นคู่ที่เหมือนกัน
Jean-François Corbett

คำตอบ:


326

หากเราไม่แน่ใจว่ามีการเรียงลำดับรายการเราสามารถใช้ฟังก์ชันในตัวmin()เพื่อค้นหาองค์ประกอบที่มีระยะทางขั้นต่ำจากจำนวนที่ระบุ

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

หมายเหตุว่ามันยังทำงานร่วมกับ dicts ด้วยปุ่ม int {1: "a", 2: "b"}เช่น วิธีนี้ใช้เวลา O (n)


หากรายการถูกเรียงลำดับแล้วหรือคุณสามารถชำระราคาของการเรียงลำดับอาร์เรย์ได้เพียงครั้งเดียวให้ใช้วิธีการแบ่งครึ่งที่แสดงในคำตอบของ @ Lauritzซึ่งใช้เวลา O (log n) เท่านั้น (หมายเหตุอย่างไรก็ตามการตรวจสอบว่ารายการเรียงลำดับแล้วหรือไม่ (n) และการเรียงลำดับคือ O (n log n))


13
การพูดด้วยความซับซ้อนนี่คือO(n)ที่การแฮ็กเพียงเล็กน้อยbisectจะช่วยให้คุณได้รับการปรับปรุงอย่างมากO(log n)(หากมีการจัดเรียงอาร์เรย์อินพุตของคุณ)
mic_e

5
@mic_e: นั่นเป็นเพียงคำตอบ Lauritz ของ
kennytm

3
สิ่งที่เกี่ยวกับการส่งคืนดัชนีว่าสิ่งนี้เกิดขึ้นในรายการ?
Charlie Parker

@CharlieParker สร้างการใช้งานของคุณเองminเรียกใช้ผ่านพจนานุกรม ( items()) แทนที่จะเป็นรายการและส่งคืนคีย์แทนค่าในที่สุด
Dustin Oprea

2
หรือใช้numpy.argminแทนminการรับดัชนีแทนค่า

148

ฉันจะเปลี่ยนชื่อฟังก์ชั่นtake_closestให้เป็นไปตามอนุสัญญาการตั้งชื่อ PEP8

หากคุณหมายถึงการใช้งานแบบด่วนซึ่งตรงกันข้ามกับการเขียนอย่างรวดเร็วไม่minควรเป็นอาวุธที่คุณเลือกยกเว้นในกรณีการใช้งานที่แคบมาก การแก้ปัญหาจำเป็นต้องตรวจสอบทุกหมายเลขในรายการและทำการคำนวณสำหรับแต่ละหมายเลข การใช้งานแทนจะเร็วขึ้นเกือบตลอดเวลาminbisect.bisect_left

"เกือบ" มาจากความจริงที่ว่าbisect_leftต้องมีการเรียงลำดับรายการเพื่อทำงาน หวังว่ากรณีการใช้งานของคุณจะช่วยให้คุณสามารถเรียงลำดับรายการได้เพียงครั้งเดียวแล้วทิ้งไว้คนเดียว แม้ว่าจะไม่ได้ตราบใดที่คุณไม่จำเป็นต้องเรียงลำดับก่อนครั้งที่คุณโทรทุกtake_closestที่bisectโมดูลจะมีโอกาสออกมาด้านบน หากคุณมีข้อสงสัยลองทั้งคู่และดูความแตกต่างในโลกแห่งความเป็นจริง

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 ทำงานโดยแบ่งครึ่งรายการและค้นหาครึ่งที่myNumberจะต้องโดยดูที่ค่ากลาง วิธีการนี้จะมีเวลาทำงานของO (log n)เมื่อเทียบกับO (n)เวลาการทำงานของคำตอบที่ได้รับการโหวตสูงสุด หากเราเปรียบเทียบทั้งสองวิธีและจัดหาทั้งคู่ด้วยการเรียงลำดับผลลัพธ์myListเหล่านี้คือ:

$ python -m timeit -s "
จากการนำเข้าที่ใกล้เคียงที่สุด take_closest
จากการนำเข้าสุ่ม randint
a = range (-1000, 1000, 10) "" take_closest (a, randint (-1100, 1100)) "

100,000 ลูป, ดีที่สุดคือ 3: 2.22 usec ต่อลูป

$ python -m timeit -s "
จากการนำเข้าที่ใกล้เคียงที่สุด with_min
จากการนำเข้าสุ่ม randint
a = range (-1000, 1000, 10) "" with_min (a, randint (-1100, 1100)) "

10000 ลูปดีที่สุด 3: 43.9 usec ต่อลูป

ดังนั้นในการทดสอบbisectนี้เร็วกว่าเกือบ 20 เท่า สำหรับรายการที่ยาวกว่าความแตกต่างจะมากขึ้น

เกิดอะไรขึ้นถ้าเราปรับระดับการเล่นโดยการลบเงื่อนไขที่myListต้องเรียงลำดับ? สมมติว่าเราจัดเรียงสำเนาของรายการทุกครั้งที่ take_closestถูกเรียกใช้ในขณะที่ปล่อยให้minโซลูชันไม่เปลี่ยนแปลง เมื่อใช้รายการ 200 ข้อในการทดสอบด้านบนbisectโซลูชันจะยังคงเร็วที่สุด แต่ประมาณ 30% เท่านั้น

นี่เป็นผลลัพธ์ที่แปลกเมื่อพิจารณาว่าขั้นตอนการเรียงลำดับคือO (n log (n)) ! เหตุผลเดียวที่minยังคงสูญเสียคือการเรียงลำดับทำในรหัส c ที่ปรับให้เหมาะสมสูงสุดในขณะที่minต้องทำการเรียกฟังก์ชั่นแลมบ์ดาสำหรับทุกรายการ เมื่อmyListขนาดเพิ่มขึ้นminโซลูชันจะเร็วขึ้นในที่สุด โปรดทราบว่าเราต้องซ้อนทุกอย่างเพื่อให้ได้minทางออก


2
การเรียงลำดับตัวเองต้องการ O (N log N) ดังนั้นมันจะช้าลงเมื่อ N กลายเป็นใหญ่ ตัวอย่างเช่นหากคุณใช้a=range(-1000,1000,2);random.shuffle(a)คุณจะพบว่าtakeClosest(sorted(a), b)จะช้าลง
kennytm

3
@ KennyTM ฉันจะให้คุณและฉันจะชี้ให้เห็นในคำตอบของฉัน แต่ตราบใดที่getClosestอาจมีการเรียกมากกว่าหนึ่งครั้งสำหรับการเรียงลำดับทุกครั้งสิ่งนี้จะเร็วขึ้นและสำหรับกรณีการเรียงลำดับครั้งเดียวมันไม่ใช่เรื่องง่าย
Lauritz V. Thaulow

สิ่งที่เกี่ยวกับการส่งคืนดัชนีว่าสิ่งนี้เกิดขึ้นในรายการ?
Charlie Parker

หากmyListมีอยู่แล้วการnp.arrayใช้np.searchsortedแทนที่bisectเร็วกว่า
Michael Hall

8
>>> takeClosest = lambda num,collection:min(collection,key=lambda x:abs(x-num))
>>> takeClosest(5,[4,1,88,44,3])
4

แลมบ์ดาเป็นลักษณะพิเศษของการเขียน "ไม่ระบุชื่อ" ฟังก์ชั่น (ฟังก์ชั่นที่ไม่ได้มีชื่อ) คุณสามารถกำหนดชื่อใด ๆ ที่คุณต้องการเพราะแลมบ์ดาเป็นนิพจน์

วิธีการ "ยาว" ในการเขียนข้างต้นจะเป็น:

def takeClosest(num,collection):
   return min(collection,key=lambda x:abs(x-num))

2
หมายเหตุ แต่ที่กำหนดแลมบ์ดาเป็นชื่อเป็นกำลังใจตามPEP 8
หลีกเลี่ยง Heylen

6
def closest(list, Number):
    aux = []
    for valor in list:
        aux.append(abs(Number-valor))

    return aux.index(min(aux))

รหัสนี้จะให้ดัชนีของจำนวนที่ใกล้เคียงที่สุดในรายการ

วิธีแก้ปัญหาที่ KennyTM ให้ไว้นั้นดีที่สุดโดยรวม แต่ในกรณีที่คุณไม่สามารถใช้งานได้ (เช่น brython) ฟังก์ชั่นนี้จะใช้งานได้


5

ทำซ้ำรายการและเปรียบเทียบจำนวนที่ใกล้เคียงที่สุดในปัจจุบันด้วย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

1
คุณสามารถส่งคืนดัชนี
Charlie Parker

1
! ไม่ถูกต้อง! if abs(myList[i] - myNumber) < abs(closest - myNumber): closest = myList[i];ควรจะเป็น เก็บค่านั้นไว้ล่วงหน้าดีกว่า
lk_vc

ฟังก์ชั่นแน่นอนว่ามันยืนอยู่แล้วส่งกลับดัชนีของที่ใกล้เคียงที่สุด เพื่อให้เป็นไปตามข้อกำหนดของ OP ไม่ควรอ่านบรรทัดที่สองใกล้เคียงที่สุด = myList [i]
Paula Livingstone

2

เป็นสิ่งสำคัญที่จะต้องทราบว่าแนวคิดข้อเสนอแนะของ Lauritz ในการใช้งาน bisect นั้นไม่พบค่าที่ใกล้เคียงที่สุดใน MyList to MyNumber แต่แบ่งครึ่งพบความคุ้มค่าต่อไปในการสั่งซื้อหลังจาก myNumber ใน MyList ดังนั้นในกรณีของ OP คุณจะได้รับตำแหน่ง 44 คืนมาแทนตำแหน่งที่ 4

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

ในการรับค่าที่ใกล้เคียงกับ 5 คุณสามารถลองแปลงรายการเป็นอาร์เรย์และใช้ argmin จาก numpy อย่างนั้น

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

ฉันไม่รู้ว่ามันจะเร็วแค่ไหนฉันเดาว่า "ไม่มาก"


2
ฟังก์ชั่นของ Lauritz ทำงานอย่างถูกต้อง คุณเพิ่งใช้ bisect_left เท่านั้น แต่ Lauritz แนะนำให้ฟังก์ชั่น takeClosest (... ) ที่ทำให้การตรวจสอบเพิ่มเติม
Kanat

หากคุณกำลังจะใช้ NumPy คุณสามารถใช้แทนnp.searchsorted bisect_leftและ @Kanat เป็นสิทธิ - วิธีการแก้ปัญหาของ Lauritz ไม่รวมรหัสที่หยิบที่ของทั้งสองผู้สมัครเป็นผู้ใกล้ชิด
John Y

1

ขยายความตามคำตอบของ Gustavo Lima สิ่งเดียวกันสามารถทำได้โดยไม่ต้องสร้างรายการใหม่ทั้งหมด ค่าในรายการสามารถถูกแทนที่ด้วยส่วนต่างตามFORลูปที่ดำเนินไป

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.

1

ถ้าฉันอาจเพิ่มคำตอบของ @ Lauritz

เพื่อไม่ให้เกิดข้อผิดพลาดในการทำงานอย่าลืมเพิ่มเงื่อนไขก่อนbisect_leftบรรทัด:

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

ดังนั้นรหัสเต็มจะมีลักษณะ:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.