зі списку цілих чисел, отримайте число, найближче до заданого значення


158

Давши список цілих чисел, я хочу знайти, яке число є найближчим до числа, яке я ввожу:

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

Чи є швидкий спосіб зробити це?


2
як щодо повернення індексу, що це сталося у списку.
Чарлі Паркер


1
@ sancho.s Добре помічений. Хоча відповіді на це питання набагато кращі, ніж відповіді на це інше питання. Тож я буду голосувати за закриття другого як дубліката цього.
Жан-Франсуа Корбетт

Відповіді:


326

Якщо ми не впевнені, що список відсортований, ми могли б використовувати вбудовану min()функцію , щоб знайти елемент, який має мінімальну відстань від вказаного числа.

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

Зауважте, що він також працює з диктами з клавішами int, наприклад {1: "a", 2: "b"}. Цей метод займає O (n) час.


Якщо список вже відсортований, або ви можете заплатити ціну сортування масиву лише один раз, використовуйте метод розбиття, проілюстрований у відповіді @ Лауріца, який займає лише час O (log n) (зауважте, проте перевірка, чи список вже відсортований - O (n) і сортування - O (n log n).)


13
Якщо говорити про складність, то це O(n), де невеликий злом bisectдозволить вам покращити O(log n)(якщо ваш вхідний масив відсортований).
mic_e

5
@mic_e: Це лише відповідь Лауріца .
kennytm

3
як щодо повернення індексу, що це сталося у списку?
Чарлі Паркер

@CharlieParker Створіть власну реалізацію min, запустіть його над словником ( items()) замість списку та поверніть ключ замість значення в кінці.
Дастін Опреа

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

Бісект працює, повторюючи вдвічі список і з’ясовуючи, на яку половину myNumberпотрібно потрапити, переглянувши середнє значення. Це означає, що у нього є час запуску O (log n) на відміну від часу (O) n запуску найвищої відповіді . Якщо ми порівнюємо два способи і надаємо обидва сортовані myList, то це результати:

$ python -m timeit -s "
від найближчого імпорту take_closest
від випадкового ранжиру на імпорт
a = діапазон (-1000, 1000, 10) "" take_closest (a, randint (-1100, 1100)) "

100000 петель, найкраще 3: 2,22 Usec на цикл

$ python -m timeit -s "
від найближчого імпорту з_min
від випадкового ранжиру на імпорт
a = діапазон (-1000, 1000, 10) "" з_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можна назвати не раз для кожного виду, це буде швидше, а для випадку сортування, який використовується колись, це нереалізатор.
Лауріц В. Таулов

як щодо повернення індексу, що це сталося у списку?
Чарлі Паркер

Якщо myListвже є, np.arrayто використання np.searchsortedзамість bisectшвидше.
Майкл Холл

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 .
Еверт Гейлен

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
ви також можете повернути індекс.
Чарлі Паркер

1
! Неправильно! Повинно бути if abs(myList[i] - myNumber) < abs(closest - myNumber): closest = myList[i];. Краще зберігайте цю вартість заздалегідь.
lk_vc

Безумовно, що функція, що стоїть, вже повертає індекс найближчих. Щоб він не відповідав вимогам ОП, не слід читати другий останній рядок найближче = myList [i]
Паула Лівінгстон

2

Важливо зауважити, що ідея навіювання Лауріца щодо використання бісектії насправді не знаходить найближчого значення в MyList для MyNumber. Натомість бісект знаходить наступне значення для порядку після MyNumber у MyList. Тож у випадку з ОП ви фактично отримаєте позицію 44 повернуто замість позиції 4.

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

Щоб отримати значення, найближче до 5, ви можете спробувати перетворити список в масив і використовувати аргумент з 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
Функція Лауріца працює правильно. Ви використовуєте лише bisect_left, але Лауріц запропонував функцію takeClosest (...), яка робить додаткову перевірку.
Канат

Якщо ви збираєтесь використовувати NumPy, ви можете використовувати np.searchsortedзамість bisect_left. І @Kanat прав - рішення Lauritz в дійсно включає в себе код , який знімає який з двох кандидатів ближче.
Джон Y

1

Розгортання відповіді Густаво Ліма. Те ж саме можна зробити без створення абсолютно нового списку. Значення у списку можуть бути замінені диференціалами в міру 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

Якщо я можу додати відповідь @ Лауріца

Щоб не мати помилки запуску, не забудьте додати умову перед 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.