【问题标题】:Mergesort - Is Bottom-Up faster than Top-Down?Mergesort - 自下而上比自上而下更快吗?
【发布时间】:2012-04-26 13:25:12
【问题描述】:

我一直在阅读 Sedgewick & Wayne 的“Algorithms, 4th Ed”,在此过程中我一直在实现 JavaScript 中讨论的算法。

我最近使用书中提供的合并排序示例来比较自上而下和自下而上的方法......但我发现自下而上的运行速度更快(我认为)。在我的博客上查看我的分析。 - http://www.akawebdesign.com/2012/04/13/javascript-mergesort-top-down-vs-bottom-up/

我找不到任何讨论说一种合并排序方法应该比另一种更快。我的实现(或分析)有缺陷吗?

注意:我的分析测量算法的迭代循环,而不是严格地数组比较/移动。也许这是有缺陷或无关紧要的?

编辑:我的分析实际上并没有计时,所以我关于它运行“更快”的说法有点误导。我正在通过递归方法跟踪“迭代”(顶部- down) 和 for 循环(自下而上)- 自下而上似乎使用较少的迭代。

【问题讨论】:

  • 比较和交换是排序分析中的关键成本项目,我很确定。
  • @Pointy 是的,它们通常是比较不同排序算法时要分析的项目。但在这种情况下,它们应该是相同的......它们是相同的算法,所以这不是我所追求的。我的实现反映了书中的内容......是否有可能自下而上在数组上/通过数组使用更少的循环但具有相同数量的比较/移动?
  • @NiklasB。我明白你的意思......但这些并没有导致我的迭代次数出现差异。如果您查看我的代码,我只会跟踪递归/迭代循环内的迭代。 Math.floor() 与它无关 - 我没有使用基于时间的分析
  • 也许我原帖中的“跑得更快”是不正确的。我在数组上发现自下而上的循环次数更少,但这可能与“速度”无关
  • 对大小正好是 2 的幂的数组进行排序有什么不同吗?

标签: javascript algorithm sorting language-agnostic mergesort


【解决方案1】:

如果更快意味着更少的“迭代”,那么可以。如果您想知道执行时间。

原因是这 21,513 次迭代中的一些迭代次数超过了 22,527 次迭代。

从源代码来看,您图中的某些叶节点似乎是一起排序的,而不是单独排序,导致合并和排序减少,但它们花费的时间更长。

【讨论】:

  • 很好的解释,谢谢!我可能需要更多地消化实现,但至少我知道我并不完全疯了。考虑到我的两种算法都使用相同的 merge() 代码,我只是没想到会有区别。
【解决方案2】:

我找不到任何讨论说一种合并排序方法应该比另一种更快。

自下而上和自上而下的归并排序,以及其他变体,在 90 年代得到了很好的研究。简而言之,如果以单个key的比较次数来衡量成本,最好的成本相同(~(n lg n)/2),自顶向下的最差成本低于或等于最差自下而上的情况(但都〜n lg n)和自上而下的平均成本低于或等于自下而上的平均情况(但都〜n lg n),其中“lg n”是二进制对数。差异源于线性项。当然,如果n=2^p,这两种变体其实是完全一样的。这意味着,比较而言,自上而下总是比自下而上好。进一步证明了自顶向下归并排序的“半半”分裂策略是最优的。研究论文来自 Flajolet、Golin、Panny、Prodinger、Chen、Hwang 和 Sedgewick。

这是我在 Erlang 的 Design and Analysis of Purely Functional Programs(College Publications,英国)一书中提出的内容:

tms([X|T=[_|U]]) -> cutr([X],T,U);
tms(T)           -> T.

cutr(S,[Y|T],[_,_|U]) -> cutr([Y|S],T,U);
cutr(S,    T,      U) -> mrg(tms(S),tms(T)).

mrg(     [],    T)            -> T;
mrg(      S,   [])            -> S;
mrg(S=[X|_],[Y|T]) when X > Y -> [Y|mrg(S,T)];
mrg(  [X|S],    T)            -> [X|mrg(S,T)].

请注意,这不是稳定的排序。此外,在 Erlang(和 OCaml)中,如果要节省内存,则需要在模式中使用 aliases (ALIAS=...)。这里的诀窍是在不知道列表长度的情况下找到列表的中间位置。这是由 cutr/3 完成的,它处理两个指向输入列表的指针:一个增加一,另一个增加二,所以当第二个到达末尾时,第一个在中间。 (我从 Olivier Danvy 的一篇论文中了解到这一点。)这样,您不需要跟踪长度,也不需要复制列表后半部分的单元格,因此您只需要 (1/2 )n lg n 额外空间,与 n lg n。这不是众所周知的。

人们经常声称自下而上的变体更适合函数式语言或链表(Knuth、Panny、Prodinger),但我认为这不是真的。

我和你一样对没有关于归并排序的讨论感到困惑,所以我做了自己的研究并写了一大章来讨论它。我目前正在准备一个新版本,其中包含更多关于合并排序的材料。

顺便说一句,还有其他变体:队列合并排序和在线合并排序(我在书中讨论了后者)。

[编辑:由于成本的衡量标准是比较次数,因此选择数组与链表之间没有区别。当然,如果你用链表实现自上而下的变体,你必须很聪明,因为你不一定知道键的数量,但你每次都需要遍历至少一半的键,并且重新分配总共 (1/2)n lg n 个单元格(如果你很聪明的话)。使用链表的自下而上合并排序实际上需要更多的额外内存,n lg n + n 个单元格。因此,即使使用链表,自上而下的变体也是最佳选择。就程序的长度而言,您的里程可能会有所不同,但在函数式语言中,如果不需要稳定性,自顶向下的合并排序可以比自底向上的排序更短。有一些论文讨论了合并排序的实现问题,例如就地(您需要数组)或稳定性等。例如,A Meticulous Analysis of Mergesort Programs,作者 Katajainen 和 Larsson Traff (1997 年)。]

【讨论】:

  • 你写“并且自上而下的平均成本低于或等于自下而上的最坏情况(但两者都〜n lg n)”是这样,还是你的意思是“自下而上的平均情况”?是对数组进行了分析,还是对链表也有效?
  • 谢谢;我非常有兴趣看到您的最佳自上而下链表功能合并排序,并将其与此进行比较:mgsort xs = foldt merge [] [[x]|x<-xs]
  • (我的意思不是暗示它是单行的;所有的功能都集中在一起并且有点内联,it becomes this 9-liner in Haskell)。
  • 我编辑了答案。您所指的 Haskell 版本似乎是自下而上的变体。很难与其他语言进行比较,因为它使用了 Haskell 特有的功能。这就是为什么我为我的书选择了一个简化版的 Erlang,所以这些程序很容易适应和比较。我还将上面的代码翻译成Java,保持函数式风格:-)(有一章是关于Java中的函数式风格的,还有一章是关于XSLT的。)
  • 对于数组或向量,自顶向下合并排序涉及 n-2 个递归调用实例,通常每次调用有两个指针和两个索引。尽管自上而下与自下而上的遍历相比,在几级递归方面具有缓存友好优势,但我的比较发现自下而上的合并排序要快 5% 到 10%。在 Intel 3770k、64 位模式下,排序 2000 万 (20*1024*1024) 个 64 位整数需要 2.07 秒自上而下,1.92 秒自下而上,大约快 8%。
【解决方案3】:

我曾在this course 的 2012 年 8 月版课程论坛上问过同样的问题。普林斯顿大学的 Kevin wayne 教授回答说,在很多情况下,递归比迭代更快,因为缓存提高了性能。

所以我当时得到的简短回答是,由于缓存原因,自上而下的归并排序会比自下而上的归并排序更快。

请注意,该课程是用 Java 编程语言(不是 Javascript)教授的。

【讨论】:

  • 迟到总比不评论好?自上而下的缓存改进可能发生在小型子阵列上,其中刚刚合并的输出仍在缓存中以用于下一个输入。但是,对于大多数处理器,该缓存在用于合并输入的同时被刷新到内存中。我在 X86 处理器上运行的基准测试的最终结果是自下而上更快,但速度并不快,因为递归开销为 O(log2(n)),而总时间为 O(n log2(n))。例如,在我的系统上对 1600 万个 64 位整数进行排序,自下而上大约需要 1.5 秒,自上而下大约需要 1.6 秒。
猜你喜欢
  • 1970-01-01
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 2010-09-26
  • 2013-10-05
  • 2021-10-30
  • 1970-01-01
  • 2013-09-15
相关资源
最近更新 更多