из списка целых чисел, получить номер, ближайший к заданному значению

158

Учитывая список целых чисел, я хочу найти, какое число является самым близким к числу, которое я даю во входных данных:

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

Есть ли быстрый способ сделать это?

Рики Робинсон
источник
2
а как насчет возврата индекса, что это произошло в списке.
Чарли Паркер
1
@ sancho.s Красиво замечен. Хотя ответы на этот вопрос намного лучше, чем ответы на этот другой вопрос. Так что я собираюсь голосовать, чтобы закрыть другой как дубликат этого.
Жан-Франсуа Корбетт

Ответы:

327

Если мы не уверены, что список отсортирован, мы могли бы использовать встроенную min()функцию , чтобы найти элемент, который имеет минимальное расстояние от указанного числа.

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

Обратите внимание, что он также работает с dicts с ключами типа int, например {1: "a", 2: "b"}. Этот метод занимает O (n) времени.


Если список уже отсортирован, или вы можете заплатить цену сортировки массива только один раз, используйте метод деления пополам, показанный в ответе @ Lauritz, который занимает всего O (log n) времени (однако обратите внимание, что проверка, если список уже отсортирован, равна O (n) и сортировка O (n log n).)

kennytm
источник
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
от случайного импорта randint
a = range (-1000, 1000, 10) "" take_closest (a, randint (-1100, 1100)) "

100000 циклов, лучшее из 3: 2,22 пользователя на цикл

$ python -m timeit -s "
из ближайшего импорта with_min
от случайного импорта randint
a = range (-1000, 1000, 10) "" with_min (a, randint (-1100, 1100)) "

10000 петель, лучшее из 3: 43,9 мксек на петлю

Так что в этом конкретном тесте, bisectэто почти в 20 раз быстрее. Для более длинных списков разница будет больше.

Что если мы выровняем игровое поле, удалив предварительное условие, которое myListдолжно быть отсортировано? Допустим, мы сортируем копию списка каждый раз, когда take_closest вызывается, оставляя minрешение без изменений. Используя список из 200 пунктов в приведенном выше тесте, bisectрешение все еще остается самым быстрым, хотя только на 30%.

Это странный результат, учитывая, что шаг сортировки O (n log (n)) ! Единственная причина, minкоторая все еще проигрывает, заключается в том, что сортировка выполняется в высокооптимизированном коде c, в то время как minдля каждого элемента приходится использовать лямбда-функцию. По мере myListувеличения размера minрешение в конечном итоге будет быстрее. Обратите внимание, что нам пришлось сложить все в свою пользу, чтобы minрешение победило.

Lauritz V. Thaulow
источник
2
Сама сортировка требует O (N log N), поэтому она будет медленнее, когда N становится большим. Например, если вы используете, a=range(-1000,1000,2);random.shuffle(a)вы обнаружите, takeClosest(sorted(a), b)что станет медленнее.
Kennytm
3
@KennyTM Я дам вам это и укажу на это в своем ответе. Но так как long 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
Конечно, функция в том виде, в каком она есть, уже возвращает индекс ближайшего. Для того, чтобы он соответствовал требованиям ОП, не следует читать вторую последнюю строку: closest = myList [i]
Паула Ливингстон
2

Важно отметить, что идея предложения Лаурица об использовании bisect на самом деле не находит в MyList значение, наиболее близкое к MyNumber. Вместо этого bisect находит следующее значение по порядку после 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

Я не знаю, как быстро это будет, хотя, я думаю, будет "не очень".

jmdeamer
источник
2
Функция Лорица работает правильно. Вы используете только bisect_left, но Lauritz предложил функцию 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.
JayJay123
источник
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
итп
источник