从整数列表中,获取最接近给定值的数字


158

给定一个整数列表,我想找到哪个数字与我在输入中提供的数字最接近:

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

有什么快速的方法可以做到这一点吗?


2
该如何返回列表中发生的索引呢?
查理·帕克


1
@ sancho.s很好发现。尽管该问题的答案比其他问题的答案要好。因此,我将投票关闭另一项,以作为另一项的重复。
让·弗朗索瓦·科贝特

Answers:


326

如果不确定列表是否已排序,则可以使用内置min()函数,查找与指定数字之间的最小距离的元素。

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

请注意,它也可用于带有int键的字典,例如{1: "a", 2: "b"}。此方法花费O(n)时间。


如果列表已经排序,或者您可以只对数组进行一次排序,则使用@Lauritz答案中所示的二等分方法,该方法只需要O(log n)时间(但请注意,检查列表是否已排序为O) (n),排序为O(n log n)。)


13
说到复杂性,这是O(n),在这里进行一些小小的改动bisect将给您带来巨大的改进O(log n)(如果您对输入数组进行了排序)。
mic_e 2014年

5
@mic_e:那只是Lauritz的答案
kennytm 2014年

3
还返回列表中发生的索引该怎么办?
查理·帕克

@CharlieParker创建您自己的实现min,在字典(items())而不是列表上运行它,最后返回键而不是值。
达斯汀·欧普里亚

2
或使用numpy.argmin代替代替min获取索引代替值。

148

我将重命名该函数take_closest以符合PEP8命名约定。

如果您的意思是快速执行而不是快速编写,min那么除非是在一个非常狭窄的用例中,否则不应其作为选择的武器。该min解决方案需要检查列表中的每一个数字,并做到每个号码的计算。使用bisect.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

Bisect的工作方式是反复将列表减半,并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必须为每个项目调用lambda函数。随着myList规模的增长,min解决方案最终将更快。请注意,min为了赢得解决方案,我们必须堆叠所有有利条件。


2
排序本身需要O(N log N),所以当N变大时,排序会变慢。例如,如果您使用a=range(-1000,1000,2);random.shuffle(a)它,将会发现takeClosest(sorted(a), b)速度变慢。
kennytm 2012年

3
@KennyTM我会给你的,我会在答案中指出。但是只要getClosest每次排序都可以被多次调用,它就会更快,而且对于一次排序的用例来说,这很容易。
Lauritz V. Thaulow 2012年

还返回列表中发生的索引该怎么办?
查理·帕克

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

一个拉姆达是写一个“匿名”功能(即没有名称的功能)的一种特殊方式。您可以为它分配任何名称,因为lambda是一个表达式。

上面的“长篇”写法是:

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

2
但是请注意,根据PEP 8,不建议将lambda分配给名称。
Evert Heylen '17

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

    return aux.index(min(aux))

此代码将为您提供列表中最接近的Number的索引。

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

现在的功能肯定已经返回最接近的索引。为了满足OP的要求,不应倒数第二行最接近= myList [i]
Paula Livingstone,

2

重要的是要注意,Lauritz的使用bisect的建议思想实际上并未在MyList中找到与MyNumber最接近的值。相反,bisect会在MyList中的MyNumber之后按顺序查找下一个值。因此,在OP的情况下,您实际上得到的是返回的位置44而不是位置4。

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

要获得最接近5的值,您可以尝试将列表转换为数组,并使用numpy的argmin这样。

>>> 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
Lauritz的功能正常工作。您仅使用bisect_left,但Lauritz建议使用函数takeClosest(...)进行附加检查。
卡纳特

如果您要使用NumPy,则可以使用np.searchsorted代替bisect_left。@Kanat是正确的-Lauritz的解决方案确实包含选择两个候选人中哪个更接近的代码。
约翰Y

1

扩展了古斯塔沃·利马(Gustavo Lima)的答案。无需创建全新的列表即可完成相同的操作。随着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

如果我可以补充@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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.