关于python:从整数列表中获取最接近给定值的数字

from list of integers, get number closest to a given value

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

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

有什么快速的方法吗?


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

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

注意,它还可以与带int键的dict一起使用,比如{1:"a", 2:"b"}。这个方法需要O(N)时间。

如果列表已经排序,或者您只需支付一次数组排序的费用,请使用@lauritz's answer中所示的平分方法,该方法只需要O(log n)时间(注意,检查列表是否已经排序为O(n),排序是否为O(n logn)。


如果您的意思是"快速执行"而不是"快速写入",那么min不应该是您的首选武器,除非在一个非常狭窄的用例中。min解决方案需要检查列表中的每个数字,并对每个数字进行计算。相反,使用bisect.bisect_left几乎总是更快。

"几乎"来自这样一个事实,即bisect_left要求对列表进行排序才能工作。希望,您的用例是这样的,您可以对列表进行一次排序,然后将其单独保存。即使不需要,只要在每次调用takeClosest之前不需要排序,bisect模块可能会出现在最上面。如果你有疑问,尝试两者,看看现实世界的差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.
   """

    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

通过反复地将列表减半,并通过查看中间值找出哪个部分(9)必须位于哪个部分中,从而进行对分。这意味着它的运行时间为O(log n),而不是最高投票答案的运行时间。如果我们比较这两种方法,并将它们与排序后的myList一起提供,则结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ python -m timeit -s"
from closest import takeClosest
from random import randint
a = range(-1000, 1000, 10)"
"takeClosest(a, randint(-1100, 1100))"

100000 loops, best of 3: 2.22 usec per loop

$ python -m timeit -s"
from closest import with_min
from random import randint
a = range(-1000, 1000, 10)"
"with_min(a, randint(-1100, 1100))"

10000 loops, best of 3: 43.9 usec per loop

所以在这个特定的测试中,bisect快了20倍。对于更长的列表,差异将更大。

如果我们取消了必须对myList进行排序的前提条件来平衡竞争环境呢?假设我们每次调用takeClosest时都对列表进行排序,而不更改min解决方案。使用上述测试中的200个项目列表,bisect解决方案仍然是最快的,尽管只有30%左右。

这是一个奇怪的结果,考虑到排序步骤是O(n log(n))!min仍然失败的唯一原因是排序是在高度优化的C代码中完成的,而min必须为每个项目调用lambda函数。随着myList的尺寸增加,min的解决方案最终会更快。请注意,我们必须将所有有利于min解决方案的内容都放在一边才能获胜。


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

lambda是编写"匿名"函数(一个没有名称的函数)的特殊方法。因为lambda是一个表达式,所以可以给它指定任何名称。

写上述内容的"长"方法是:

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


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

    return aux.index(min(aux))

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

KennyTM给出的解决方案总体上是最好的,但是在您不能使用它的情况下(如Bryton),这个函数将完成这项工作。


遍历列表并将当前最接近的数字与abs(currentNumber - myNumber)进行比较:

1
2
3
4
5
6
def takeClosest(myList, myNumber):
    closest = myList[0]
    for i in range(1, len(myList)):
        if abs(i - myNumber) < closest:
            closest = i
    return closest


重要的是要注意,劳里茨使用平分的建议思想实际上并没有在mylist中找到与mynumber最接近的值。相反,二分法在mylist中按mynumber之后的顺序查找下一个值。所以在OP的例子中,你会得到44的位置,而不是4的位置。

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

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

1
2
3
4
5
6
7
8
>>> 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

不过,我不知道这会有多快,我想"不是很快"。


如果我可以加上@lauritz的答案

为了避免出现运行错误别忘了在bisect_left行之前添加一个条件:

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

所以完整的代码看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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


扩展古斯塔沃·利马的回答。同样的事情也可以在不创建一个全新列表的情况下完成。随着FOR循环的进行,列表中的值可以替换为微分。

1
2
3
4
5
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))

1
2
3
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.