【问题标题】:Python QuickSort maximum recursion depthPython QuickSort 最大递归深度
【发布时间】:2014-11-25 08:14:38
【问题描述】:

(Python 2.7.8 Windows)

我正在对不同的排序算法(快速、冒泡和插入)进行比较,并且大多数情况下都按预期进行,对于长列表,快速排序要快得多,对于非常短的列表和已经排序的列表,冒泡和插入更快.

引发问题的是快速排序和前面提到的“已排序”列表。我可以对甚至 100000 个项目的列表进行排序而不会出现问题,但是对于从 0...n 开始的整数列表,限制似乎要低得多。 0...500 有效,但即使 0...1000 也有效:

RuntimeError: maximum recursion depth exceeded in cmp

快速排序:

def quickSort(myList):
    if myList == []:
        return []
    else:
        pivot = myList[0]
        lesser = quickSort([x for x in myList[1:] if x < pivot])
        greater = quickSort([x for x in myList[1:] if x >= pivot])
        myList = lesser + [pivot] + greater
        return myList

代码有问题,还是我遗漏了什么?

【问题讨论】:

    标签: python sorting


    【解决方案1】:

    发生了两件事。

    首先,Python 有意将递归限制在一个固定的深度。例如,Scheme 会一直为递归调用分配帧,直到内存不足,Python(至少是最流行的实现,CPython)在失败之前只会分配 sys.getrecursionlimit() 帧(默认为 1000)。这是有原因的,* 但实际上,这与这里无关;您需要了解的是它确实做到了这一点。

    其次,您可能已经知道,虽然 QuickSort 是O(N log N)大多数 列表,但它的最坏情况是O(N^2) — 特别是(使用标准数据透视规则)已经-排序列表。发生这种情况时,您的堆栈深度最终可能是O(N)。所以,如果你有 1000 个元素,按照最坏情况的顺序排列,并且你已经进入堆栈的一帧,你就会溢出。

    您可以通过以下几种方式解决此问题:

    • 使用显式堆栈将代码重写为可迭代的,因此您只受堆内存而不是堆栈深度的限制。
    • 确保始终先递归到较短的一侧,而不是左侧。这意味着即使在O(N^2) 的情况下,您的堆栈深度仍然是O(log N)。但前提是您已经完成了上一步。**
    • 使用随机、三中位数或其他枢轴规则,使常见情况不像已经排序的最坏情况。 (当然,仍然有人可以故意拒绝您的代码;使用快速排序确实无法避免这种情况。)Wikipedia article 对此进行了一些讨论,并提供了经典 Sedgewick 和 Knuth 论文的链接。
    • 使用具有无限堆栈的 Python 实现。***
    • sys.setrecursionlimit(max(sys.getrecursionlimit(), len(myList)+CONSTANT))。这样一来,如果你不能腾出足够的空间,你就会因为一个明显的原因立即失败,否则通常不会失败。 (但你可能——你可能已经在堆栈中开始了 900 步的排序……)但这是个坏主意。****。况且CONSTANT要搞清楚,一般情况下是不可能的。*****

    * 从历史上看,CPython 解释器递归调用自身以进行递归 Python 函数调用。并且C栈的大小是固定的;如果你超出了最后,你可能会出现段错误,踩到堆内存或其他各种问题。这个可以改变——事实上,Stackless Python 开始时基本上只是带有这个改变的 CPython。但核心开发人员有意选择不这样做,部分原因是他们不想鼓励人们编写深度递归代码。

    ** 或者如果您的语言会自动消除尾调用,但 Python 不会这样做。但是,正如 gnibbler 指出的那样,您可以编写一个混合解决方案——在小端递归,然后在大端手动展开尾递归——这不需要显式堆栈。

    *** Stackless 和 PyPy 都可以这样配置。

    **** 一方面,最终你会导致 C 堆栈崩溃。

    ***** 常量并不是真正的常量;这取决于您已经在堆栈中的深度(可通过将sys._getframe() 走到顶部进行不可移植计算)以及比较函数需要多少松弛度等(根本无法计算,您只需要猜测) .

    【讨论】:

    • sys.setrecursionlimit() 技巧假定您是唯一使用堆栈的例程...没有任何空间可供可能呼叫您的人使用。 :-)
    • @kindall:是的,没有可移植的方法来找出你在堆栈中的深度,也根本没有办法找出你需要多少额外的深度。我使用常量 1003 作为 hack 来解决其他人的代码并让服务器备份一两个星期,直到我可以替换它,但否则我不会推荐它......
    • 在较短的一侧进行递归调用是个好主意。较长的一侧可以迭代处理(while 而不是if),只需进行一些重构,无需显式堆栈。这应该使最坏情况堆栈 O(log N)
    • @gnibbler:好点子。如果您只是对尾递归部分进行反递归,则可以在没有堆栈的情况下完成。我已将其编辑到脚注中;你认为它需要更高吗?
    【解决方案2】:

    您选择每个子列表的第一个项目作为枢轴。如果列表已经有序,这意味着您的greater 子列表是除第一个之外的所有项目,而不是其中的大约一半。本质上,每个递归调用只能处理一个项目。这意味着您需要进行的递归调用的深度将与完整列表中的项目数大致相同。一旦你达到大约 1000 个项目,它就会溢出 Python 的内置限制。排序已经倒序的列表时也会遇到类似的问题。

    要纠正此问题,请使用文献中建议的解决方法之一,例如随机选择一个项目作为第一个、中间和最后一个项目的枢轴或中值。

    【讨论】:

    • 由于我无法投票,只能选择一个答案,所以我将给予老式的感谢:谢谢。
    【解决方案3】:

    总是选择第一个(或最后一个)元素作为枢轴将给快速排序带来问题 - 如您所见,某些常见输入的最坏情况下性能

    一种相当有效的技术是选择第一个、中间和最后一个元素的平均值

    您不想让枢轴选择过于复杂,否则它将主导搜索的运行时间

    【讨论】:

    • 请注意:选择不一定在列表中的枢轴(例如您可能通过使用平均值获得)将需要更改算法,因为代码明确包含枢轴值。
    • 由于我无法投票,只能选择一个答案,所以我将给予老式的感谢:谢谢。
    猜你喜欢
    • 1970-01-01
    • 2011-12-31
    • 1970-01-01
    • 1970-01-01
    • 2020-07-13
    • 2021-02-07
    • 2016-07-09
    • 2017-03-18
    • 2015-04-15
    相关资源
    最近更新 更多