de la lista de enteros, obtenga el número más cercano a un valor dado

158

Dada una lista de enteros, quiero encontrar qué número es el más cercano a un número que ingreso como entrada:

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

¿Hay alguna forma rápida de hacer esto?

Ricky Robinson
fuente
2
¿qué hay de devolver también el índice de que esto sucedió en la lista?
Charlie Parker
1
@ sancho.s Muy bien visto. Aunque las respuestas a esta pregunta son mucho mejores que las de esa otra pregunta. Así que voy a votar para cerrar el otro como duplicado de este.
Jean-François Corbett

Respuestas:

327

Si no estamos seguros de que la lista está ordenada, podríamos utilizar el incorporado en min()la función , para encontrar el elemento que tiene la distancia mínima desde el número especificado.

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

Tenga en cuenta que también funciona con dictos con claves int, como {1: "a", 2: "b"}. Este método lleva O (n) tiempo.


Si la lista ya está ordenada, o podría pagar el precio de ordenar la matriz solo una vez, use el método de bisección ilustrado en la respuesta de @ Lauritz que solo toma tiempo O (log n) (tenga en cuenta que verificar si una lista ya está ordenada es O (n) y la clasificación es O (n log n).)

kennytm
fuente
13
Hablando en complejidad, esto es O(n), donde un poco de pirateo bisectle dará una mejora masiva O(log n)(si su matriz de entrada está ordenada).
mic_e
55
@mic_e: Esa es solo la respuesta de Lauritz .
kennytm
3
¿Qué hay de devolver también el índice de que esto sucedió en la lista?
Charlie Parker
@CharlieParker Cree su propia implementación de min, ejecútela en un diccionario ( items()) en lugar de una lista, y devuelva la clave en lugar del valor al final.
Dustin Oprea
2
O use en numpy.argminlugar de minpara obtener el índice en lugar del valor.
148

take_closestCambiaré el nombre de la función para cumplir con las convenciones de nomenclatura PEP8.

Si te refieres a rápido de ejecutar en lugar de rápido de escribir, nomin debería ser tu arma de elección, excepto en un caso de uso muy limitado. La solución debe examinar cada número de la lista y hacer un cálculo para cada número. Usar en cambio es casi siempre más rápido.minbisect.bisect_left

El "casi" proviene del hecho de que bisect_leftla lista debe estar ordenada para funcionar. Con suerte, su caso de uso es tal que puede ordenar la lista una vez y luego dejarla en paz. Incluso si no es así, siempre que no necesite ordenar cada vez que llame take_closest, bisectes probable que el módulo quede en la parte superior. Si tiene dudas, pruebe ambas y observe la diferencia del 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

Bisect funciona reduciendo a la mitad repetidamente una lista y descubriendo en qué mitad myNumbertiene que estar mirando el valor medio. Esto significa que tiene un tiempo de ejecución de O (log n) en oposición al tiempo de ejecución de O (n) de la respuesta más votada . Si comparamos los dos métodos y suministramos ambos con un orden myList, estos son los resultados:

$ python -m timeit -s "
de la importación más cercana take_closest
de importación aleatoria randint
a = rango (-1000, 1000, 10) "" take_closest (a, randint (-1100, 1100)) "

100000 bucles, lo mejor de 3: 2.22 usec por bucle

$ python -m timeit -s "
desde la importación más cercana con_min
de importación aleatoria randint
a = rango (-1000, 1000, 10) "" with_min (a, randint (-1100, 1100)) "

10000 bucles, lo mejor de 3: 43.9 usec por bucle

Entonces, en esta prueba en particular, bisectes casi 20 veces más rápido. Para listas más largas, la diferencia será mayor.

¿Qué sucede si nivelamos el campo de juego eliminando la condición previa que myListdebe clasificarse? Digamos que clasificamos una copia de la lista cada vez que take_closest se llama, dejando la minsolución sin alterar. Usando la lista de 200 elementos en la prueba anterior, la bisectsolución sigue siendo la más rápida, aunque solo en un 30%.

¡Este es un resultado extraño, considerando que el paso de clasificación es O (n log (n)) ! La única razón por la minque sigue perdiendo es que la clasificación se realiza en un código c altamente optimizado, mientras que mintiene que seguir adelante llamando a una función lambda para cada elemento. A medida que myListcrece en tamaño, la minsolución eventualmente será más rápida. Tenga en cuenta que tuvimos que apilar todo a su favor para que la minsolución ganara.

Lauritz V. Thaulow
fuente
2
La clasificación en sí misma necesita O (N log N), por lo que será más lento cuando N se haga grande. Por ejemplo, si usa a=range(-1000,1000,2);random.shuffle(a), encontrará que takeClosest(sorted(a), b)se volvería más lento.
kennytm
3
@KennyTM Te lo concederé, y lo señalaré en mi respuesta. Pero siempre que getClosestse pueda llamar más de una vez para cada tipo, será más rápido, y para el caso de uso de ordenar una vez, es obvio.
Lauritz V. Thaulow
¿Qué hay de devolver también el índice de que esto sucedió en la lista?
Charlie Parker
Si myListya es un np.arrayuso np.searchsorteden lugar de bisectes más 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

Una lambda es una forma especial de escribir una función "anónima" (una función que no tiene nombre). Puede asignarle el nombre que desee porque una lambda es una expresión.

La forma "larga" de escribir lo anterior sería:

def takeClosest(num,collection):
   return min(collection,key=lambda x:abs(x-num))
Burhan Khalid
fuente
2
Sin embargo, tenga en cuenta que no se recomienda asignar lambda a los nombres de acuerdo con 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 le dará el índice del número más cercano de Número en la lista.

La solución dada por KennyTM es la mejor en general, pero en los casos en que no puede usarla (como brython), esta función hará el trabajo.

Gustavo Lima
fuente
5

Iterar sobre la lista y comparar el número más cercano actual con 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
João Silva
fuente
1
También puede devolver el índice.
Charlie Parker
1
! Incorrecto! Debe ser if abs(myList[i] - myNumber) < abs(closest - myNumber): closest = myList[i];. Sin embargo, es mejor almacenar ese valor de antemano.
lk_vc
Seguramente la función tal como está ya devuelve el índice del más cercano. Para que cumpla con los requisitos del OP, ¿no debería leer la segunda última línea más cercana = myList [i]
Paula Livingstone
2

Es importante tener en cuenta que la idea de sugerencia de Lauritz de usar bisect en realidad no encuentra el valor más cercano en MyList a MyNumber. En cambio, bisect encuentra el siguiente valor en orden después de MyNumber en MyList. Entonces, en el caso de OP, en realidad obtendría la posición de 44 en lugar de la posición de 4.

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

Para obtener el valor más cercano a 5, puede intentar convertir la lista a una matriz y usar argmin de numpy de esta manera.

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

Sin embargo, no sé qué tan rápido sería, supongo que sería "no muy".

jmdeamer
fuente
2
La función de Lauritz funciona correctamente. Solo usa bisect_left, pero Lauritz sugirió una función takeClosest (...) que realiza una verificación adicional.
Kanat
Si vas a usar NumPy, puedes usarlo en np.searchsortedlugar de bisect_left. Y @Kanat es correcto - La solución de Lauritz hace incluya el código que recoge cuál de los dos candidatos está más cerca.
John Y
1

Ampliando la respuesta de Gustavo Lima. Se puede hacer lo mismo sin crear una lista completamente nueva. Los valores en la lista se pueden reemplazar con los diferenciales a medida que FORavanza el ciclo.

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

Si puedo agregar a la respuesta de @ Lauritz

Para no tener un error de ejecución, no olvide agregar una condición antes de la bisect_leftlínea:

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

entonces el código completo se verá así:

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