【发布时间】:2021-05-12 08:59:53
【问题描述】:
我想使用 Python3 以最快的方式实现以下过程:给定一个 N 随机整数列表,我需要返回 K 最小的整数(并且我不需要对返回的整数进行排序)。
我以三种不同的方式实现了它(如下面的代码所示)。
-
test_sorted()函数使用内置的sorted()函数对整个整数列表进行排序,然后对第一个K元素进行切片。这个操作的代价本质上应该是运行sorted()函数的代价,它的时间复杂度是O(N log(N))。 -
test_heap()函数使用堆来仅存储最低的K元素并返回它们。在堆上插入一个元素的时间复杂度为O(log(N)),理论上我们需要在堆中推送一个项目的时间是N。但是,在第一次K插入之后,我们将从堆中推送和弹出,我希望如果传入元素大于堆中的任何元素,则不会发生插入,时间复杂度应该介于O(K log(N))和O(N log(N))(取决于输入列表的实际排序)。无论如何,即使我的假设不正确,最糟糕的复杂性应该是O(N log(N))(像往常一样,我认为我们需要的所有比较的成本可以忽略不计)。 -
test_nsmallest()函数使用来自heapq模块的nsmallest()函数。我对这种方法没有任何期望,因为在官方 python 文档中我只发现了对于较大的值,使用 sorted() 函数更有效。 我决定试一试。
# test.py
from heapq import heappush, heappushpop, nsmallest
from random import randint
from timeit import timeit
N, K = 1000, 50
RANDOM_INTS = [randint(1,100) for _ in range(N)]
def test_sorted():
return sorted(RANDOM_INTS)[:K]
def test_heap():
heap = []
for val in RANDOM_INTS:
if len(heap) < K:
heappush(heap, -val)
else:
heappushpop(heap, -val)
return [-val for val in heap]
def test_nsmallest():
return nsmallest(K, RANDOM_INTS)
def main():
sorted_result = timeit("test_sorted()", globals=globals(), number=100_000)
print(f"test_sorted took: {sorted_result}")
heap_result = timeit("test_heap()", globals=globals(), number=100_000)
print(f"test_heap took: {heap_result}")
nsmallest_result = timeit("test_nsmallest()", globals=globals(), number=100_000)
print(f"test_nsmallest took: {nsmallest_result}")
r1, r2, r3 = test_sorted(), test_heap(), test_nsmallest()
assert len(r1) == len(r2) == len(r3)
assert set(r1) == set(r2) == set(r3)
if __name__ == '__main__':
main()
在我的(旧)2011 年末 MacBook Pro 上使用 2.4GHz i7 处理器的输出如下。
$ python --version
Python 3.9.2
$ python test.py
test_sorted took: 8.389572635999999
test_heap took: 18.586762750000002
test_nsmallest took: 13.772040639000004
使用sorted() 的最简单解决方案是迄今为止最好的,谁能详细说明为什么结果不符合我的预期(即test_heap() 函数应该至少快一点)?我错过了什么?
如果我用 pypy 运行相同的代码,结果是相反的。
$ pypy --version
Python 3.7.10 (51efa818fd9b, Apr 04 2021, 12:03:51)
[PyPy 7.3.4 with GCC Apple LLVM 12.0.0 (clang-1200.0.32.29)]
$ pypy test.py
test_sorted took: 7.1336525249998886
test_heap took: 3.1177806880004937
test_nsmallest took: 7.5453417899998385
这更接近我的期望。
假设我对 python 内部一无所知,并且我对为什么 pypy 比 python 快只有一个非常粗略的了解,任何人都可以详细说明这些结果并添加一些关于正在发生的事情的信息,以便让我正确预见未来类似情况的最佳选择?
另外,如果您对其他比上述运行速度更快的实现有任何建议,请随时分享!
更新:
如果我们需要根据某些不是项目本身值的标准对输入列表进行排序(正如我在实际用例中实际需要的那样;以上只是一个简化)?好吧,在这种情况下,结果更令人惊讶:
# test2.py
from heapq import heappush, heappushpop, nsmallest
from random import randint
from timeit import timeit
N, K = 1000, 50
RANDOM_INTS = [randint(1,100) for _ in range(N)]
def test_sorted():
return sorted(RANDOM_INTS, key=lambda x: x)[:K]
def test_heap():
heap = []
for val in RANDOM_INTS:
if len(heap) < K:
heappush(heap, (-val, val))
else:
heappushpop(heap, (-val, val))
return [val for _, val in heap]
def test_nsmallest():
return nsmallest(K, RANDOM_INTS, key=lambda x: x)
def main():
sorted_result = timeit("test_sorted()", globals=globals(), number=100_000)
print(f"test_sorted took: {sorted_result}")
heap_result = timeit("test_heap()", globals=globals(), number=100_000)
print(f"test_heap took: {heap_result}")
nsmallest_result = timeit("test_nsmallest()", globals=globals(), number=100_000)
print(f"test_nsmallest took: {nsmallest_result}")
r1, r2, r3 = test_sorted(), test_heap(), test_nsmallest()
assert len(r1) == len(r2) == len(r3)
assert set(r1) == set(r2) == set(r3)
if __name__ == '__main__':
main()
哪些输出:
$ python test2.py
test_sorted took: 18.740868524
test_heap took: 27.694126547999996
test_nsmallest took: 25.414596833000004
$ pypy test2.py
test_sorted took: 65.88409741500072
test_heap took: 3.9442632220016094
test_nsmallest took: 19.981832798999676
这至少告诉我两件事:
-
使用外部键进行排序非常昂贵,无论是使用
keykwarg 提供 lambda 函数,还是需要构建元组(sorting_value, actual_value)以获得堆中所需的排序时。 -
将 lambdas 与 pypy 一起使用似乎非常昂贵,但我不知道为什么……也许 pypy 无法优化它们,这与它执行的其他优化不兼容???
【问题讨论】:
-
您的
test_heap有使用直接python 的开销,而test_sorted是用C 实现的,只有将输入参数和输出结果从/转换为python 对象的开销很小。跨度> -
此外,内置的
sort经过高度优化,可以处理输入中的预排序序列,考虑到您构建输入的方式,这可能很常见。
标签: python python-3.x performance time-complexity pypy