stdlib 的 qsort 是递归的吗?
Posted
技术标签:
【中文标题】stdlib 的 qsort 是递归的吗?【英文标题】:Is stdlib's qsort recursive? 【发布时间】:2011-03-23 17:24:01 【问题描述】:我读到qsort
只是一个通用的排序,没有关于实现的承诺。我不知道库因平台而异,但假设 Mac OS X 和 Linux 实现大致相似,qsort
实现是递归的和/或需要大量堆栈?
我有一个大数组(数十万个元素),我想对它进行排序,而不会使我的堆栈被遗忘。或者,对于大型数组的等效项有什么建议吗?
【问题讨论】:
【参考方案1】:这是一个来自 BSD 的版本,Apple 版权,大概在某个时候在 OS X 中使用过:
http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/bsd/kern/qsort.c
它是调用递归的,尽管递归深度的上限很小,正如 Blindy 解释的那样。
这是来自 glibc 的一个版本,大概在某个时候用于 Linux 系统:
http://www.umcs.maine.edu/~chaw/200801/capstone/n/qsort.c
不是递归调用。出于同样的原因,调用递归的限制很小,它可以使用少量的固定堆栈来管理它的循环递归。
我可以麻烦查找最新版本吗?不;-)
对于几十万个数组元素,即使调用递归实现也不会调用超过 20 层。在不深的宏伟计划中,除了在非常有限的嵌入式设备上,它没有足够的内存让你首先拥有一个那么大的数组来排序。当 N 有界时,O(log N) 显然是一个常数,但更重要的是,它通常是一个非常易于管理的常数。通常 32 或 64 倍的“小”是“合理的”。
【讨论】:
+1 用于实际查看源代码。有趣的是,glibc 在其 qsort() 中使用了快速排序/插入排序混合 @nos:IIRC 这就是 Knuth 告诉你的,很有趣,但希望不会令人惊讶 ;-)【参考方案2】:你知道,递归部分是 logn deep。在 64 级递归中(总共 ~64*4=~256 字节的堆栈),您可以对大小为 ~2^64 的数组进行排序,即您可以在 64 位 cpu 上寻址的最大数组,即 147573952589676412928 64 位整数的字节。你甚至无法记住它!
担心重要的事情。
【讨论】:
+1。它可能比 256 多几个字节,具体取决于每个级别在堆栈上推送的数量,但它仍然是一个小常数。 -1:这是错误的。快速排序的最坏情况空间复杂度为 O(n),而不是 O(log n)。大型数组可以炸毁堆栈。 @Luther:当正确实现时(递归时,先对较小的分区进行排序),堆栈使用被限制为近似对数增长。确切地说,Knuth 将其给出为 [lg (N+1)/(M+2)] (其中“[]”表示“地板”),其中 N=被排序的元素数,M=您所在分区的大小停止递归(假设一个“改进的”快速排序在整个事情几乎排序时切换到插入排序)。 Luther,qsort() 不是“快速排序”——或者更确切地说,实际算法是由实现定义的。例如,Glibc 的 qsort() 切换到插入排序以避免最坏情况下的空间复杂度问题。 @0A0D:阿尔伯塔幻灯片没用。出于教学目的,可能是一个很好的简化,但实际上没有人通过分配两个新数组来实现分区步骤,一个用于枢轴的每一侧,并将元素复制到其中。因此,该分析与由知道自己在做什么的人编写的任何 Quicksort 实现无关 - Quicksort 的部分好处在于它是(几乎)就地算法。【参考方案3】:是的,它是递归的。不,它可能不会使用大量堆栈。为什么不简单地尝试一下?递归不是某种柏忌——它是许多问题的首选解决方案。
【讨论】:
@Joe Depths 喜欢什么?快速排序中的递归将堆栈帧(即局部变量和返回地址)推送到堆栈,而不是被排序事物的副本。这是非常少的数据。 如果 @Joe qsort 不能很好地处理非常大的数据集,它就不是那种选择。不过,这个问题并没有错,只是我确实发现这里的许多人都不愿意实际尝试一下,这有点令人不快。 完全偏离主题:Neither is the Pope catholic, nor do bears mostly shit in the woods -1:快速排序的最坏情况空间复杂度为 O(n),这意味着对大型数组进行排序可能破坏堆栈。如果堆栈空间不充足(例如在线程或协程中),则需要考虑这一点。 叹息;这句俏皮话引起了相当多的“攻击性”,因此被删掉了。【参考方案4】:正确实现的qsort
不需要超过 log2(N) 级别的递归(即堆栈深度),其中 N 是给定平台上的最大数组大小。请注意,此限制适用于 分区的好坏,即它是递归的最坏情况深度。例如,在 32 位平台上,递归深度在最坏的情况下永远不会超过 32,给定 qsort
的合理实现。
换句话说,如果您特别关心堆栈的使用,则无需担心,除非您正在处理一些奇怪的低质量实现。
【讨论】:
【参考方案5】:我记得读过这本书:C Programming: A Modern Approach ANSI C 规范没有定义如何实现 qsort。
这本书写道qsort
实际上可能是另一种排序,合并排序,插入排序,为什么不是冒泡排序:P
因此,qsort
实现可能不是递归的。
【讨论】:
好的标准不会描述如何实现任何东西——尽管它们会为诸如排序之类的事情指定最低复杂度保证,这可能会限制实现算法的选择。 @Neil:不管有什么好的标准,碰巧C 标准并没有指定qsort
和bsearch
的复杂性。幸运的是,这个问题特别是关于两个实现,所以标准几乎无关紧要。如果 Apple 打算在下一个版本中将 OS X 切换到 Bogosort,那么他们是否能侥幸成功将不取决于它是否违反了 C 标准......【参考方案6】:
使用快速排序,堆栈将以对数方式增长。您将需要 很多 元素来炸毁您的堆栈。
【讨论】:
@msw:看到您坚持要迂腐,您忘记将 N 定义为数组的大小。就我而言,“对数增长”一词在谈论算法时通常被理解为 O(lg(n))。【参考方案7】:我猜qsort
的大多数现代实现实际上都使用 Introsort 算法。合理编写的快速排序无论如何都不会破坏堆栈(它会首先对较小的分区进行排序,这将堆栈深度限制为对数增长)。
不过,Introsort 更进了一步——为了限制最坏情况的复杂性,如果它发现 Quicksort 运行不佳(递归过多,因此它可能具有 O(N2) 复杂性),它将切换到保证 O(N log2 N) 复杂度的 Heapsort 并且 限制堆栈的使用。因此,即使它使用的 Quicksort 写得很草率,切换到 Heapsort 无论如何都会限制堆栈的使用。
【讨论】:
【参考方案8】:在大型阵列上可能失败的qsort
实现已严重损坏。如果你真的担心我会去 RTFS,但我怀疑任何半体面的实现要么使用就地排序算法,要么使用malloc
临时空间,如果malloc
回退到就地算法失败。
【讨论】:
【参考方案9】:简单的快速排序实现(仍然是 qsort 的流行选项)的最坏情况空间复杂度是 O(N)。 如果修改实现以首先对较小的数组进行排序和尾递归优化或显式堆栈和迭代使用然后最坏情况空间可以降低到 O(log N),(这里的大多数答案已经写过了)。所以,如果快速排序的实现没有被破坏并且库没有被不正确的编译器标志破坏,你就不会炸毁你的堆栈。但是,例如,大多数支持尾递归消除的编译器不会在未优化的调试版本中进行此优化。使用错误标志构建的库(即没有足够的优化,例如在您有时构建自己的调试 libc 的嵌入式域中)可能会导致堆栈崩溃。
对于大多数开发人员来说,这永远不会成为问题(他们已经对具有 O(log N) 空间复杂度的 libc 进行了供应商测试),但我想说,不时关注潜在的库问题是个好主意到时间。
更新:这是我的意思的一个示例:libc 中的一个错误(从 2000 年开始),其中 qsort 将开始颠簸虚拟内存,因为 qsort 实现将在内部切换到 mergesort,因为它虽然有足够的内存来保存临时数组。
http://sources.redhat.com/ml/libc-alpha/2000-03/msg00139.html
【讨论】:
提问者询问的是具有合理实施质量的特定系统。 “天真的快速排序实现仍然是一个流行的选择”是完全错误的。它不受编写 C 库的人的欢迎,这是问题所关注的。 提问者询问“Linux”。 Linux 没有实现 qsort,因为它是一个内核。 qsort 是 C 运行时库的一个函数,它有几个选项(glibc、uclibc、newlib、dietlibc ..然后他们把这个东西放到了 android 中)。另外:请参阅我的更新。 -1 来自我:假设的糟糕实现的 qsort 是无关紧要的。 glibc qsort 实现得很好,我假设 OS X 也是如此。 qsort 的错误实现是一个错误,需要修复。 @Lars:我只是举了一个例子,glibc 的 qsort 是如何以一种你认为是假设的方式实现的,这让一些人非常头疼。当然是固定的。 +1 这是一个很好的答案。事实上,它与 AndreyT 的路线相同,只是 Luther 的声望不超过 30K。以上是关于stdlib 的 qsort 是递归的吗?的主要内容,如果未能解决你的问题,请参考以下文章