Python heapq vs预排序列表的排序速度

时间:2016-07-12 23:55:14

标签: python list sorting merge

我有一个相当大的数目n = 10000个排序列表,每个长度为k = 100。由于合并两个排序列表需要线性时间,我认为在深度log(n)树中以递归方式将长度为O(nk)的排序列表与heapq.merge()合并比用一次性对整个事物进行排序更便宜在O(nklog(nk))时间sorted()

然而,sorted()方法似乎在我的机器上快了17-44倍。 sorted()的实现是否比heapq.merge()快得多,它超过了经典合并的渐近时间优势?

import itertools
import heapq

data = [range(n*8000,n*8000+10000,100) for n in range(10000)]

# Approach 1
for val in heapq.merge(*data):
    test = val

# Approach 2
for val in sorted(itertools.chain(*data)):
    test = val

2 个答案:

答案 0 :(得分:4)

CPython的list.sort()使用自适应合并排序,它识别输入中的自然运行,然后“智能地”合并它们。它在利用多种预先存在的订单方面非常有效。例如,尝试排序range(N)*2(在Python 2中)以增加N的值,并且您会发现所需的时间在N中线性增长。

因此,heapq.merge()在此应用程序中的唯一真正优势是较低的峰值内存使用如果迭代结果(而不是实现包含所有结果的有序列表)。

事实上,与list.sort()方法相比,heapq.merge()在特定数据中采用了更多优势。我对此有一些了解,因为我编写了Python的list.sort(); - )

(顺便说一句,顺便说一句,我看到你已经接受了答案,这对我来说没问题 - 这是一个很好的答案。我只想提供更多信息。)

关于“更多优势”

正如评论中所讨论的那样,list.sort()扮演了许多工程技巧,可能削减了heapq.merge()所需的比较次数。这取决于数据。以下是您问题中特定数据的快速说明。首先定义一个计算执行的比较次数的类(注意我使用的是Python 3,因此必须考虑所有可能的比较):

class V(object):
    def __init__(self, val):
        self.val = val

    def __lt__(a, b):
        global ncmp
        ncmp += 1
        return a.val < b.val

    def __eq__(a, b):
        global ncmp
        ncmp += 1
        return a.val == b.val

    def __le__(a, b):
        raise ValueError("unexpected comparison")

    __ne__ = __gt__ = __ge__ = __le__
故意编写

sort()仅使用<__lt__)。这更像是heapq中的一次事故(而且,我记得,甚至在Python版本中也有所不同),但事实证明.merge()仅需要<==。所以这些是该类以有用的方式定义的唯一比较。

然后更改您的数据以使用该类的实例:

data = [[V(i) for i in range(n*8000,n*8000+10000,100)]
        for n in range(10000)]

然后运行两种方法:

ncmp = 0
for val in heapq.merge(*data):
    test = val
print(format(ncmp, ","))

ncmp = 0
for val in sorted(itertools.chain(*data)):
    test = val
print(format(ncmp, ","))

输出有点了不起:

43,207,638
1,639,884

对于此特定数据,sorted()要求merge()更少的比较。而这是它更快的主要原因。

长篇短篇

那些比较计数看起来对我来说非常了不起;-) heapq.merge()的计数看起来是我认为合理的两倍。

花了一段时间追踪这一点。简而言之,它是heapq.merge()实现方式的工件:它维护一堆3元素列表对象,每个对象包含来自可迭代的当前下一个值,所有迭代中可迭代的0基索引(打破比较关系),以及可迭代的__next__方法。 heapq函数都比较这些小列表(而不是只是迭代的值),列表比较总是通过列表首先查找不是{{1的第一个相应项目}}

因此,例如,询问== 首先是否询问[0] < [1]。它不是,所以然后继续询问是否0 == 1

因此,在执行0 < 1期间进行的每次<比较实际上进行了两次对象比较(一次heapq.merge(),另一次==)。 <比较是“浪费”的工作,因为它们在逻辑上不是解决问题的必要条件 - 它们只是“优化”(在这种情况下不需要支付!)在内部使用列表比较。

因此,从某种意义上说,将==比较的报告减少一半是更公平的。但它仍然需要heapq.merge()以上,所以我现在就放弃它; - )

答案 1 :(得分:2)

sorted使用adaptive mergesort来检测已排序的运行并有效地合并它们,因此它可以利用heapq.merge可以使用的输入中的所有相同结构。此外,sorted有一个非常好的C实现,其中的优化工作量比heapq.merge要多得多。