【问题标题】:Why is it not recommended to use a heap to sort a LinkedList?为什么不建议使用堆对 LinkedList 进行排序?
【发布时间】:2021-12-31 02:26:48
【问题描述】:

我知道如何使用归并排序对链表进行排序。问题是,我们为什么不直接使用堆来创建排序的 LinkedList?

  1. 遍历链表并不断将项目添加到最小堆中。
  2. 继续从堆中取出项目并堆化堆并添加到新结果 LinkedList。

第一步将有 O(n) 用于遍历列表,O(nlogn) 用于将项目添加到堆中。总计O(nlogn)[有错请指正]。

从堆中取出一个项目是O(1) 添加一个项目作为LinkedList 中的下一个节点是O(1)。 [如有错误请指正]。

所以如果我的理解是正确的,可以在O(nlogn) 中进行排序。这与归并排序相同。在内存方面,我们使用了一个额外的堆,所以我假设总内存可以是O(nlogn)。合并排序也可以采用O(nlogn),但可以改进为O(logn)

堆逻辑同“合并k个有序链表”。我假设每个链表都有 1 个项目。

对于堆版本的复杂性,我可能完全错了。如果有人知道为什么不应该[为什么合并排序更好]使用堆的确切原因,请解释。这不是堆排序,也不是就地算法。如果时间复杂度是O(n²logn),不知道怎么弄。

【问题讨论】:

  • 我没有完全遵循,但我认为您所说的是“枚举链表元素并将每个元素插入二叉树。然后通过对树进行有序遍历来重建列表。这没什么问题。它只是不是合并排序。
  • @selbie 是的,就是这样。因为我有一个最小堆,所以我总是会得到最低的值。在书中,我看到我们无法使用快速排序或堆排序对单链表进行排序。所以很好奇为什么我们不只是用堆排序。如果我对复杂性的理解是正确的,不如堆排序有效,但不会比合并排序差。
  • 您说的是为堆创建一个全新的、独立的结构?所以要设置一些额外的编程时间而不会提高性能?
  • “如果有人知道不应该使用堆的确切原因”:谁说不应该使用它?来源?
  • 诸如“不要使用堆对链表进行排序”这样的分类语句是完全错误的。

标签: c# c++ c algorithm sorting


【解决方案1】:

据我所知,没有法律禁止使用堆对链表进行排序,但请考虑一下:

  • 使用相同的方法,您可以使用数组上使用的任何排序算法:将列表的值复制到数组中;使用您喜欢的算法对数组进行排序,然后从数组中重新创建一个链表。

  • 大多数与链表有关的代码挑战都要求您不使用任何其他 O(n) 数据结构,而只使用链表。根据挑战,甚至可能需要仅使用 O(1) 辅助内存。

  • 如果需要 O(1) 辅助内存,那么将链表转换为堆组织的链表是不切实际的:它无法提供从节点到其堆子节点或堆父节点的有效遍历。另一方面,其他高效算法,如合并排序和某种快速排序的风格可以使用链表结构来实现。

【讨论】:

  • 是的。我同意。它类似于将所有元素添加到数组中并对其进行排序,然后将链表中的所有内容从排序后的数组中添加回来。我相信这种情况下的空间复杂度是 O(n)。
【解决方案2】:

我知道如何使用归并排序对链表进行排序。问题是, 为什么我们不直接使用堆来创建排序的 LinkedList?

  • 遍历链表并不断将项目添加到最小堆中。
  • 继续从堆中取出项目并堆化堆并添加到新结果 LinkedList。

几个原因,其中:

  • 渐近复杂性不是故事的结局。合并排序对于链表来说特别干净和高效,因此它是 O(n log n) 链表排序中性能最好的。

  • 但渐近复杂性仍然是故事的部分。通过使用数组索引之间的关系来提供树的隐式表示,可以使用 O(1) 辅助空间来完成数组的堆排序。这支持总体上以 O(n log n) 步骤进行排序,因为按索引访问数组是 O(1),但对于链表则不然。要获得 O(n log n) 堆排序的链表,您必须将堆构建为实际的树或数组,这需要 O(n) 开销。然后将其称为链表排序有点牵强,因为您可以对数组或许多其他数据结构做完全相同的事情。

  • 此外,归并排序很容易变得稳定,但对于堆排序来说更难,而且可能需要额外的开销。

所以如果我的理解是正确的,那么排序可以在 O(nlogn) 内完成。

当然,可以在 O(n) 步中枚举的任何数据集都可以加载到数组中,并通过任何适用的 O(n log n) 数组排序算法在 O(n log n) 步中进行排序。在链表的特殊情况下,我们也可以将节点重组为有序列表,而不会增加渐近复杂度。

这与归并排序相同。

相同的渐近复杂度并不意味着相同的性能。常数因子的巨大差异仍然可以产生重要的差异。另外,即使所有其他条件相同,您所描述的链表合并排序 vs 的更简单的代码也是一个巨大的工程优势。这意味着更快的开发和更少的错误。

在内存方面,我们使用了一个额外的堆,所以我假设总内存可以是 O(nlogn)。合并排序也可以采用 O(nlogn),但可以改进为 O(logn)。

无论哪种情况,我都不知道您的 O(n log n) 想法从何而来。我计算了堆排序情况下辅助数组的 O(n) 开销。在数组上,典型的归并排序实现有 O(n) 的开销,但在链表上,归并排序自然会有 O(log n) 的开销。

关于此评论:

在一本书中,我看到我们无法使用快速排序或堆排序对单链表进行排序。

那本书是错误的,即使我们不允许将链表读取到辅助数组或其他数据结构中以执行实际排序。如果您确实不允许这样的辅助结构,那么我认为您不能在少于 o(n2 log n) 的步骤中对单链表进行堆排序,但您 可以 做。而且您不需要快速随机访问或双向遍历来执行 O(n log n) 快速排序。

【讨论】:

  • 很好的解释。我自己为原始问题添加了一个答案,但从你的回答中学到了很多东西。谢谢。
【解决方案3】:

将项目推入堆和从堆中弹出都是对数运算。从堆中删除元素是对数的,因为堆需要调整其元素以保证堆不变。所以,你会做2 * n * log n,虽然这在像合并排序这样的大 O 术语中仍然是线性的,但它可能更慢。或者至少可以说,使用线性排序算法而不是使用堆进行排序可以做得更好。

你可以做的,我没有看到你提到它,是在O(n)时间将链表中的所有元素添加到堆的数据存储中,然后对其进行堆化,这也在O(n)中运行如果正确实施。然后你会得到2n + n * log n,它减少到O(n * log n),具有更好的常量。

堆使用的额外空间不是O(n log n)。它是线性的,虽然这取决于堆实现,但假设许多标准堆实现都提供了这一点。

当将k 排序的链表与堆合并时,您将受益于k 远小于列表本身的长度这一事实。这导致O((n1 + n2 +...) * log k) 算法,其中n1, n2,... 是所涉及列表的长度。如果您不使用堆,那么该算法将在O((n1 + n2 +...) * k) 时间内运行。

归根结底,如果堆被正确实现和使用,您将具有线性时间复杂度和线性空间复杂度来对链表进行排序。但是无论你做什么,所有其他变量都相等,标准排序算法在排序方面比堆有更好的常数,除非有一些特殊的要求和约束。让我重申一下,就 Big O 而言,这两种方法都是线性的,但排序算法可以为这个特定目的提供更好的常数,这在现实世界的应用中意义重大。原因是堆需要保证在 O(1) 时间内随时访问最小/最大元素。这对其行为施加了限制,在排序方面您将无法像标准排序算法那样优化它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-25
    • 2019-05-23
    • 1970-01-01
    • 2020-05-24
    • 1970-01-01
    • 1970-01-01
    • 2012-09-20
    • 2017-12-07
    相关资源
    最近更新 更多