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

Posted

技术标签:

【中文标题】Python heapq 与预排序列表的排序速度【英文标题】:Python heapq vs sorted speed for pre-sorted lists 【发布时间】:2016-11-15 09:16:47 【问题描述】:

我有一个相当大的数量 n=10000 个长度为 k=100 的排序列表。由于合并两个排序列表需要线性时间,我想在深度 log(n) 的树中递归合并长度为 O(nk) 和 heapq.merge() 的排序列表比使用 @987654322 一次对整个事物进行排序更便宜@ 在 O(nklog(nk)) 时间内。

但是,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

【问题讨论】:

【参考方案1】:

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

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

事实上,list.sort()heapq.merge() 方法更多地利用了您特定数据中的结构。我对此有所了解,因为我写了 Python 的 list.sort() ;-)

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

关于“更多优势”

正如在 cmets 中所讨论的,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; (__lt__)。这在heapq 中更像是一个意外(而且,我记得,甚至在 Python 版本之间有所不同),但事实证明.merge() 只需要&lt;==。因此,这些是该类以有用的方式定义的唯一比较。

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

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 函数都比较这些小列表(而不是只是 iterables 的值),并且列表比较总是通过列表首先寻找不是== 的第一个对应项。

所以,例如,询问是否[0] &lt; [1] 首先询问是否0 == 1。不是,所以然后继续询问0 &lt; 1是否。

因此,在执行heapq.merge() 期间完成的每​​个&lt; 比较实际上都会进行两个对象比较(一个==,另一个&lt;)。 == 比较是“浪费”的工作,因为它们在逻辑上不是解决问题所必需的 - 它们只是列表比较内部使用的“优化”(在这种情况下碰巧不会支付!) .

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

【讨论】:

谢谢,关于模式利用和内存使用的额外见解实际上对我的应用程序非常有帮助。我切换了我的答案选择,我是 @user2357112 不介意,他已经有很多代表了 :) 哦,嗨,蒂姆! sortedheapq.merge 更好地利用现有订单?这些列表并没有完全链接成一个大的排序运行,所以sorted 不会只是去“嘿,我们完成了!”您是在谈论合并相邻运行时减少合并区域的二进制搜索吗? 这部分是我的想法,尽管它实际上取决于列表中的确切值。 heapq.merge() 必须对每个输出进行 O(log(n)) 比较;无论确切的值如何。根据确切的值,sort() 可以由于预合并二进制搜索和合并中的“疾驰模式”而变得更便宜。我没有对问题中给出的range(n*8000,n*8000+10000,100) 进行分析,因为如果那是他们的 actual 数据,那么 OP 就不必运行程序来获得结果 ;-) 更精确说sort()可能下车更便宜。【参考方案2】:

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

【讨论】:

以上是关于Python heapq 与预排序列表的排序速度的主要内容,如果未能解决你的问题,请参考以下文章

heapq取列表最大或最小值元素

Python堆排序之heapq

python之使用heapq()函数计算列表中数值大小

每日python—— 1.4 python中的堆排序模块heapq

Python列表降序排序

Python 基础 2-3 列表的反转与排序