dall'elenco di numeri interi, ottieni il numero più vicino a un determinato valore


158

Dato un elenco di numeri interi, voglio trovare quale numero è il più vicino a un numero che do in input:

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

C'è un modo rapido per farlo?


2
che dire di restituire anche l'indice che ciò è accaduto nell'elenco.
Charlie Parker,


1
@ sancho.s Ben individuato. Sebbene le risposte a questa domanda siano molto migliori di quelle sull'altra domanda. Quindi voterò per chiudere l'altro come duplicato di questo.
Jean-François Corbett,

Risposte:


327

Se non siamo sicuri che la lista è ordinata, potremmo usare il built-in min()funzione , di trovare l'elemento che ha la distanza minima dal numero specificato.

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

Nota che funziona anche con dicts con i tasti int, come {1: "a", 2: "b"}. Questo metodo richiede O (n) tempo.


Se l'elenco è già ordinato o potresti pagare il prezzo di ordinamento dell'array una sola volta, usa il metodo di bisection illustrato nella risposta di @ Lauritz che impiega solo O (log n) tempo (nota tuttavia che verificare se un elenco è già ordinato è O (n) e l'ordinamento è O (n log n).)


13
Parlando in complessità, questo è O(n), dove un po 'di hacking bisectti darà un enorme miglioramento O(log n)(se il tuo array di input è ordinato).
mic_e,

5
@mic_e: questa è solo la risposta di Lauritz .
kennytm,

3
che dire di restituire anche l'indice che ciò è accaduto nell'elenco?
Charlie Parker,

@CharlieParker Crea la tua implementazione di min, eseguila su un dizionario ( items()) anziché su un elenco e restituisce la chiave invece del valore alla fine.
Dustin Oprea,

2
Oppure usa numpy.argmininvece di minper ottenere l'indice invece del valore.

148

take_closestRinominerò la funzione in conformità con le convenzioni di denominazione PEP8.

Se intendi eseguire rapidamente l'esecuzione invece di scrivere rapidamente, nonmin dovrebbe essere la tua arma preferita, tranne in un caso d'uso molto ristretto. La soluzione deve esaminare tutti i numeri nell'elenco e fare un calcolo per ciascun numero. L'uso invece è quasi sempre più veloce.minbisect.bisect_left

Il "quasi" deriva dal fatto che bisect_leftrichiede che l'elenco sia ordinato per funzionare. Eventualmente, il tuo caso d'uso è tale che puoi ordinare l'elenco una volta e poi lasciarlo da solo. Anche in caso contrario, purché non sia necessario effettuare l'ordinamento prima di ogni chiamata take_closest, bisectè probabile che il modulo esca in cima. In caso di dubbi, prova entrambi e osserva la differenza nel mondo reale.

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 lavora dimezzando ripetutamente un elenco e scoprendo quale metà myNumberdeve essere presente osservando il valore medio. Ciò significa che ha un tempo di esecuzione di O (log n) rispetto al tempo di esecuzione O (n) della risposta più votata . Se confrontiamo i due metodi e forniamo entrambi con un ordinato myList, questi sono i risultati:

$ python -m timeit -s "
dalla più vicina importazione take_closest
da randint di importazione casuale
a = range (-1000, 1000, 10) "" take_closest (a, randint (-1100, 1100)) "

100000 loop, meglio di 3: 2,22 usec per loop

$ python -m timeit -s "
dall'importazione più vicina con_min
da randint di importazione casuale
a = range (-1000, 1000, 10) "" with_min (a, randint (-1100, 1100)) "

10000 loop, meglio di 3: 43,9 usec per loop

Quindi, in questo particolare test, bisectè quasi 20 volte più veloce. Per elenchi più lunghi, la differenza sarà maggiore.

E se livelliamo il campo di gioco rimuovendo il presupposto che myListdeve essere ordinato? Diciamo che ordiniamo una copia dell'elenco ogni volta che take_closest viene chiamato, lasciando mininalterata la soluzione. Utilizzando l'elenco di 200 voci nel test sopra, la bisectsoluzione è ancora la più veloce, anche se solo del 30% circa.

Questo è un risultato strano, considerando che la fase di ordinamento è O (n log (n)) ! L'unica ragione che minsta ancora perdendo è che l'ordinamento viene eseguito in codice c altamente ottimizzato, mentre mindeve spostarsi lungo chiamando una funzione lambda per ogni articolo. Con l' myListaumentare delle dimensioni, la minsoluzione alla fine sarà più veloce. Nota che abbiamo dovuto impilare tutto a suo favore affinché la minsoluzione vincesse.


2
L'ordinamento stesso richiede O (N log N), quindi sarà più lento quando N sta diventando grande. Ad esempio, se usi a=range(-1000,1000,2);random.shuffle(a), scoprirai che takeClosest(sorted(a), b)diventerebbe più lento.
kennytm,

3
@KennyTM te lo concedo e lo sottolineo nella mia risposta. Ma finché getClosestpuò essere chiamato più di una volta per ogni tipo, questo sarà più veloce, e per il caso d'uso sort-once, è un gioco da ragazzi.
Lauritz V. Thaulow,

che dire di restituire anche l'indice che ciò è accaduto nell'elenco?
Charlie Parker,

Se myListè già un np.arrayallora usare np.searchsortedal posto di bisectè più veloce.
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 è un modo speciale di scrivere una funzione "anonima" (una funzione che non ha un nome). Puoi assegnargli il nome che vuoi perché un lambda è un'espressione.

Il "lungo" modo di scrivere quanto sopra sarebbe:

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

2
Si noti tuttavia che l'assegnazione di lambda ai nomi è scoraggiata in base a PEP 8 .
Evert Heylen,

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

    return aux.index(min(aux))

Questo codice ti darà l'indice del numero più vicino di Numero nell'elenco.

La soluzione fornita da KennyTM è la migliore in assoluto, ma nei casi in cui non è possibile utilizzarla (come brython), questa funzione farà il lavoro


5

Scorri l'elenco e confronta il numero più vicino corrente 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

1
potresti anche restituire l'indice.
Charlie Parker,

1
! Errato! Dovrebbe essere if abs(myList[i] - myNumber) < abs(closest - myNumber): closest = myList[i];. Meglio conservare quel valore in anticipo però.
lk_vc,

Sicuramente la funzione così com'è restituisce già l'indice del più vicino. Per soddisfare i requisiti dell'OP, la seconda riga non dovrebbe essere la più vicina = myList [i]
Paula Livingstone

2

È importante notare che l'idea del suggerimento di Lauritz di usare bisect in realtà non trova il valore più vicino in MyList a MyNumber. Invece, bisect trova il valore successivo in ordine dopo MyNumber in MyList. Quindi nel caso di OP otterresti effettivamente la posizione 44 restituita invece della posizione 4.

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

Per ottenere il valore più vicino a 5 potresti provare a convertire l'elenco in un array e usare argmin da numpy in questo modo.

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

Non so quanto veloce sarebbe, però, la mia ipotesi sarebbe "non molto".


2
La funzione di Lauritz funziona correttamente. Stai solo usando bisect_left ma Lauritz ha suggerito una funzione takeClosest (...) che effettua un controllo aggiuntivo.
Kanat,

Se hai intenzione di usare NumPy, puoi usare np.searchsortedinvece di bisect_left. E @Kanat è giusto - la soluzione di Lauritz fa includere il codice che raccoglie quale dei due candidati è più vicina.
John Y,

1

Espandendo la risposta di Gustavo Lima. La stessa cosa può essere fatta senza creare un elenco completamente nuovo. I valori nell'elenco possono essere sostituiti con i differenziali mentre il FORciclo avanza.

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 posso aggiungere alla risposta di @ Lauritz

Per non avere un errore di esecuzione, non dimenticare di aggiungere una condizione prima della bisect_leftriga:

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

quindi il codice completo sarà simile a:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.