từ danh sách các số nguyên, lấy số gần nhất với một giá trị đã cho


158

Đưa ra một danh sách các số nguyên, tôi muốn tìm số nào gần nhất với số tôi đưa vào đầu vào:

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

Có cách nào nhanh chóng để làm điều này?


2
những gì về cũng trả lại chỉ số mà điều này đã xảy ra trong danh sách.
Charlie Parker


1
@ sancho.s Phát hiện độc đáo. Mặc dù câu trả lời cho câu hỏi này tốt hơn những câu hỏi khác. Vì vậy, tôi sẽ bỏ phiếu để đóng cái khác như bản sao của cái này.
Jean-François Corbett

Câu trả lời:


326

Nếu chúng ta không chắc chắn rằng danh sách được sắp xếp, chúng ta có thể sử dụng được xây dựng trong min()chức năng , để tìm các yếu tố trong đó có khoảng cách tối thiểu từ số lượng quy định.

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

Lưu ý rằng nó cũng hoạt động với dicts với phím int, như {1: "a", 2: "b"}. Phương pháp này mất thời gian O (n).


Nếu danh sách đã được sắp xếp hoặc bạn chỉ có thể trả giá cho việc sắp xếp mảng một lần, hãy sử dụng phương pháp chia đôi được minh họa trong câu trả lời của @ Lauritz , chỉ mất thời gian O (log n) (lưu ý kiểm tra xem danh sách đã được sắp xếp chưa (n) và sắp xếp là O (n log n).)


13
Nói một cách phức tạp, đây là O(n)nơi mà một chút hack bisectsẽ mang đến cho bạn một sự cải tiến lớn O(log n)(nếu mảng đầu vào của bạn được sắp xếp).
mic_e


3
Điều gì về việc trả lại chỉ số mà điều này đã xảy ra trong danh sách?
Charlie Parker

@CharlieParker Tạo việc triển khai của riêng bạn min, chạy nó qua từ điển ( items()) thay vì danh sách và trả lại khóa thay vì giá trị cuối cùng.
Dustin Oprea

2
Hoặc sử dụng numpy.argminthay vì minđể lấy chỉ số thay vì giá trị.

148

Tôi sẽ đổi tên hàm take_closestđể phù hợp với quy ước đặt tên PEP8.

Nếu bạn có nghĩa là thực hiện nhanh trái ngược với viết nhanh, minthì đó không phải là vũ khí bạn chọn, ngoại trừ trong một trường hợp sử dụng rất hẹp. Các mingiải pháp cần phải kiểm tra tất cả các số trong danh sách làm một phép tính cho mỗi số. Sử dụng bisect.bisect_leftthay thế hầu như luôn luôn nhanh hơn.

"Gần như" xuất phát từ thực tế bisect_leftđòi hỏi danh sách phải được sắp xếp để hoạt động. Hy vọng, trường hợp sử dụng của bạn là như vậy mà bạn có thể sắp xếp danh sách một lần và sau đó để nó một mình. Ngay cả khi không, miễn là bạn không cần sắp xếp trước mỗi lần bạn gọi take_closest, bisectmô-đun có thể sẽ xuất hiện trên đầu. Nếu bạn nghi ngờ, hãy thử cả hai và nhìn vào sự khác biệt trong thế giới thực.

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 hoạt động bằng cách liên tục giảm một nửa danh sách và tìm ra một nửa myNumberphải có bằng cách nhìn vào giá trị trung bình. Điều này có nghĩa là nó có thời gian chạy O (log n) trái ngược với thời gian chạy O (n) của câu trả lời được bình chọn cao nhất . Nếu chúng ta so sánh hai phương thức và cung cấp cả hai với một sắp xếp myList, đây là kết quả:

$ python -m timeit -s "
từ lần nhập gần nhất Take_closest
từ randint nhập khẩu ngẫu nhiên
a = phạm vi (-1000, 1000, 10) "" Take_closest (a, randint (-1100, 1100)) "

100000 vòng, tốt nhất là 3: 2,22 usec mỗi vòng

$ python -m timeit -s "
từ lần nhập gần nhất với_min
từ randint nhập khẩu ngẫu nhiên
a = phạm vi (-1000, 1000, 10) "" with_min (a, randint (-1100, 1100)) "

10000 vòng, tốt nhất là 3: 43,9 usec mỗi vòng

Vì vậy, trong thử nghiệm đặc biệt này, bisectnhanh hơn gần 20 lần. Đối với danh sách dài hơn, sự khác biệt sẽ lớn hơn.

Điều gì xảy ra nếu chúng ta san bằng sân chơi bằng cách loại bỏ điều kiện tiên quyết myListphải được sắp xếp? Giả sử chúng ta sắp xếp một bản sao của danh sách mỗi lần take_closest được gọi, trong khi để mingiải pháp không bị thay đổi. Sử dụng danh sách 200 mục trong thử nghiệm trên, bisectgiải pháp vẫn là nhanh nhất, mặc dù chỉ khoảng 30%.

Đây là một kết quả kỳ lạ, xem xét rằng bước sắp xếp là O (n log (n)) ! Lý do duy nhất minvẫn còn thua là việc sắp xếp được thực hiện theo mã c được tối ưu hóa cao, trong khi minphải thực hiện theo cách gọi hàm lambda cho mọi mục. Khi myListtăng kích thước, mingiải pháp cuối cùng sẽ nhanh hơn. Lưu ý rằng chúng tôi phải xếp mọi thứ có lợi cho mingiải pháp để giành chiến thắng.


2
Sắp xếp chính nó cần O (N log N), vì vậy nó sẽ chậm hơn khi N trở nên lớn. Ví dụ, nếu bạn sử dụng, a=range(-1000,1000,2);random.shuffle(a)bạn sẽ thấy điều đó takeClosest(sorted(a), b)sẽ trở nên chậm hơn.
kennytm

3
@KennyTM Tôi sẽ cấp cho bạn điều đó và tôi sẽ chỉ ra trong câu trả lời của tôi. Nhưng miễn là getClosestcó thể được gọi nhiều hơn một lần cho mỗi loại, điều này sẽ nhanh hơn và đối với trường hợp sử dụng một lần, đó là điều không có trí tuệ.
Lauritz V. Thaulow

Điều gì về việc trả lại chỉ số mà điều này đã xảy ra trong danh sách?
Charlie Parker

Nếu myListđã là một np.arraythì sử dụng np.searchsortedthay thế bisectnhanh hơn.
Michael Hall

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

Một lambda là một cách đặc biệt viết một chức năng "ẩn danh" (một chức năng mà không có một tên). Bạn có thể gán cho nó bất kỳ tên nào bạn muốn vì lambda là một biểu thức.

Cách viết "dài" ở trên sẽ là:

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

2
Tuy nhiên, xin lưu ý rằng việc gán lambda cho các tên không được khuyến khích theo PEP 8 .
Evert Heylen

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

    return aux.index(min(aux))

Mã này sẽ cung cấp cho bạn chỉ mục về số Số gần nhất trong danh sách.

Giải pháp được đưa ra bởi KennyTM là tổng thể tốt nhất, nhưng trong trường hợp bạn không thể sử dụng nó (như brython), chức năng này sẽ thực hiện công việc


5

Lặp lại danh sách và so sánh số gần nhất hiện tại với 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
bạn cũng có thể trả về chỉ mục.
Charlie Parker

1
! Không chính xác! Nên if abs(myList[i] - myNumber) < abs(closest - myNumber): closest = myList[i];. Tốt hơn lưu trữ giá trị đó trước mặc dù.
lk_vc

Chắc chắn là chức năng của nó đã trả về chỉ số gần nhất. Để nó đáp ứng các yêu cầu của OP không nên đọc dòng cuối cùng thứ hai gần nhất = myList [i]
Paula Livingstone

2

Điều quan trọng cần lưu ý là ý tưởng đề xuất sử dụng bisect của Lauritz không thực sự tìm thấy giá trị gần nhất trong MyList cho MyNumber. Thay vào đó, bisect tìm giá trị tiếp theo theo thứ tự sau MyNumber trong MyList. Vì vậy, trong trường hợp của OP, bạn thực sự có được vị trí 44 được trả về thay vì vị trí 4.

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

Để có được giá trị gần nhất với 5, bạn có thể thử chuyển đổi danh sách thành một mảng và sử dụng argmin từ numpy như vậy.

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

Tôi không biết điều này sẽ nhanh đến mức nào, tôi đoán là "không" lắm.


2
Chức năng của Lauritz hoạt động chính xác. Bạn chỉ sử dụng bisect_left nhưng Lauritz đã đề xuất một hàm TakeClosest (...) để kiểm tra bổ sung.
Kanat

Nếu bạn sẽ sử dụng NumPy, bạn có thể sử dụng np.searchsortedthay vì bisect_left. Và @Kanat là đúng - giải pháp Lauritz của không bao gồm mã đó chọn một trong hai ứng cử viên là gần gũi hơn.
John Y

1

Mở rộng dựa trên câu trả lời của Gustavo Lima. Điều tương tự có thể được thực hiện mà không cần tạo một danh sách hoàn toàn mới. Các giá trị trong danh sách có thể được thay thế bằng các vi sai khi FORvòng lặp tiến triển.

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

Nếu tôi có thể thêm vào câu trả lời của @ Lauritz

Để không xảy ra lỗi chạy, đừng quên thêm một điều kiện trước bisect_leftdòng:

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

vì vậy mã đầy đủ sẽ trông như sau:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.