da lista de números inteiros, obtenha o número mais próximo de um determinado valor


158

Dada uma lista de números inteiros, quero descobrir qual número é o mais próximo de um número que eu forneço na entrada:

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

Existe alguma maneira rápida de fazer isso?


2
que tal também retornar o índice que isso aconteceu na lista.
Charlie Parker


1
@ sancho.s Bem detectado. Embora as respostas para essa pergunta sejam muito melhores do que as da outra pergunta. Então, eu vou votar para fechar o outro como duplicado deste.
Jean-François Corbett

Respostas:


326

Se não tiver certeza de que a lista é ordenada, poderíamos usar o built-in min()função , para encontrar o elemento que tem a distância mínima entre o número especificado.

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

Observe que ele também funciona com dict com chaves int, como {1: "a", 2: "b"}. Este método leva O (n) tempo.


Se a lista já estiver classificada, ou você pode pagar o preço de classificar a matriz apenas uma vez, use o método de bissecção ilustrado na resposta de @ Lauritz, que leva apenas tempo O (log n) (observe, porém, verificar se uma lista já está classificada é O (n) e a classificação é O (n log n).)


13
Falando em complexidade, é aqui O(n)que um pouco de hacking bisectfornecerá uma grande melhoria O(log n)(se sua matriz de entrada estiver classificada).
mic_e

5
@mic_e: Essa é apenas a resposta de Lauritz .
kennytm

3
Que tal também retornar o índice que isso aconteceu na lista?
Charlie Parker

@CharlieParker Crie sua própria implementação min, execute-a em um dicionário ( items()) em vez de em uma lista e retorne a chave em vez do valor no final.
Dustin Oprea

2
Ou use em numpy.argminvez de minpara obter o índice em vez do valor.

148

Renomearei a função take_closestpara estar em conformidade com as convenções de nomenclatura do PEP8.

Se você quer dizer execução rápida, em vez de gravação rápida, nãomin deve ser sua arma preferida, exceto em um caso de uso muito restrito. A solução precisa examinar todos os números da lista e fazer um cálculo para cada número. O uso é quase sempre mais rápido.minbisect.bisect_left

O "quase" vem do fato de bisect_leftexigir que a lista seja classificada para funcionar. Felizmente, seu caso de uso é tal que você pode classificar a lista uma vez e depois deixá-la em paz. Mesmo se não, contanto que você não precise classificar antes de cada vez que ligar take_closest, o bisectmódulo provavelmente sairá por cima. Se você estiver em dúvida, tente os dois e veja a diferença no mundo real.

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

O Bisect trabalha repetidamente pela metade uma lista e descobre em que metade myNumberdeve constar, olhando o valor médio. Isso significa que ele tem um tempo de execução de O (log n) em oposição ao tempo de execução de O (n) da resposta mais votada . Se compararmos os dois métodos e fornecermos ambos com uma ordenada myList, estes são os resultados:

$ python -m timeit -s "
da importação mais próxima take_closest
de importação aleatória aleatória
a = intervalo (-1000, 1000, 10) "" take_closest (a, randint (-1100, 1100)) ""

100000 loops, o melhor de 3: 2,22 usec por loop

$ python -m timeit -s "
da importação mais próxima com_min
de importação aleatória aleatória
a = intervalo (-1000, 1000, 10) "" com_min (a, randint (-1100, 1100)) ""

10000 loops, o melhor de 3: 43,9 usec por loop

Portanto, neste teste em particular, bisecté quase 20 vezes mais rápido. Para listas mais longas, a diferença será maior.

E se nivelarmos o campo de jogo removendo a pré-condição que myListdeve ser classificada? Digamos que classifiquemos uma cópia da lista sempre que take_closest for chamada, deixando a minsolução inalterada. Usando a lista de 200 itens no teste acima, a bisectsolução ainda é a mais rápida, embora apenas em cerca de 30%.

Esse é um resultado estranho, considerando que a etapa de classificação é O (n log (n)) ! O único motivo que minainda está perdendo é que a classificação é feita em código c altamente otimizado, enquanto miné necessário chamar uma função lambda para cada item. À medida que myListcresce em tamanho, a minsolução será eventualmente mais rápida. Observe que tivemos que empilhar tudo a seu favor para a minsolução vencer.


2
A classificação em si precisa de O (N log N), portanto será mais lenta quando N estiver ficando grande. Por exemplo, se você usar, a=range(-1000,1000,2);random.shuffle(a)verá que takeClosest(sorted(a), b)isso se tornaria mais lento.
Kennytm

3
@KennyTM Eu concedo e concordo com isso na minha resposta. Mas, desde que getClosestpossa ser chamado mais de uma vez para cada classificação, isso será mais rápido e, para o caso de uso de classificação única, é fácil.
Lauritz V. Thaulow 27/08/12

Que tal também retornar o índice que isso aconteceu na lista?
Charlie Parker

Se myListjá é um, np.arrayentão usar np.searchsortedno lugar de bisecté mais rápido.
Michael Hall

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

Um lambda é uma maneira especial de escrever uma função "anônima" (uma função que não tem nome). Você pode atribuir a ele qualquer nome que desejar, porque um lambda é uma expressão.

A maneira "longa" de escrever o texto acima seria:

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

2
Observe, no entanto, que a atribuição de lambda a nomes é desencorajada de acordo com o PEP 8 .
Evert Heylen

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

    return aux.index(min(aux))

Este código fornecerá o índice do número mais próximo de número na lista.

A solução dada pelo KennyTM é a melhor em geral, mas nos casos em que você não pode usá-la (como brython), essa função fará o trabalho


5

Itere a lista e compare o número mais próximo atual com 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
você também pode retornar o índice.
Charlie Parker

1
! Incorreta ! Deveria ser if abs(myList[i] - myNumber) < abs(closest - myNumber): closest = myList[i];. Melhor armazenar esse valor com antecedência.
Lk_vc

Certamente a função como está já retorna o índice do mais próximo. Para isso para satisfazer os requisitos do OP não deve o segundo última linha leia mais próximo = myList [i]
Paula Livingstone

2

É importante observar que a ideia de Lauritz de usar o bisect não encontra o valor mais próximo em MyList ao MyNumber. Em vez disso, o bisect encontra o próximo valor em ordem após MyNumber em MyList. Portanto, no caso do OP, você realmente retornaria a posição de 44 em vez da posição de 4.

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

Para obter o valor mais próximo de 5, você pode tentar converter a lista em uma matriz e usar argmin de numpy dessa maneira.

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

Eu não sei o quão rápido isso seria, meu palpite seria "não muito".


2
A função de Lauritz funciona corretamente. Você acabou de usar apenas bisect_left, mas Lauritz sugeriu uma função takeClosest (...) que faz uma verificação adicional.
precisa saber é

Se você vai usar o NumPy, pode usar em np.searchsortedvez de bisect_left. E @Kanat é certo - a solução da Lauritz faz incluir o código que pega qual dos dois candidatos é mais perto.
John Y

1

Expandindo a resposta de Gustavo Lima. O mesmo pode ser feito sem criar uma lista totalmente nova. Os valores na lista podem ser substituídos pelos diferenciais à medida que o FORloop avança.

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

Se eu puder adicionar à resposta de @ Lauritz

Para não ter um erro de execução, não se esqueça de adicionar uma condição antes da bisect_leftlinha:

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

então o código completo terá a seguinte aparência:

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.