【问题标题】:Lower bound on heapsort?堆排序的下限?
【发布时间】:2011-01-04 01:42:06
【问题描述】:

众所周知,堆排序的最坏情况运行时是 Ω(n lg n),但我很难理解为什么会这样。特别是,堆排序的第一步(创建一个最大堆)需要时间 Θ(n)。然后是 n 次堆删除。我明白为什么每次堆删除都需要时间 O(lg n);重新平衡堆涉及一个消泡操作,该操作在堆的高度上花费时间 O(h),并且 h = O(lg n)。但是,我不明白为什么第二步应该采用 Ω(n lg n)。似乎任何单独的堆出队都不一定会导致移动到顶部的节点一直沿树冒泡。

我的问题是 - 有没有人知道堆排序的最佳情况行为的良好下限证明?

【问题讨论】:

  • 最好的情况是已经排序的列表吗? :)
  • @marcog 不是基本 DS/本科生问题的 ctheory :-/
  • 基本思想是堆的大小在每一层都翻倍,所以它的元素至少有一半在最低层。(最低两层有3/4)因此,很有可能所有的“气泡”都需要在堆中非常低的位置。

标签: algorithm complexity-theory big-o heapsort lower-bound


【解决方案1】:

所以我自己进行了一些挖掘,看起来这个结果实际上是相当新的!我能找到的第一个下界证明是在 1992 年,尽管堆排序本身是在 1964 年发明的。

正式的下界证明归功于 Schaffer 和 Sedgewick 的“堆排序分析”论文。下面是略过解释的证明版本,省略了一些技术细节。

首先,让我们假设对于某个 k,n = 2k - 1,这保证了我们有一个完整的二叉堆。稍后我将展示如何单独处理这种情况。因为我们有 2k - 1 个元素,所以堆排序的第一遍将在 Θ(n) 中建立一个高度为 k 的堆。现在,考虑这个堆的前半部分出队,它从堆中删除了 2k-1 个节点。第一个关键观察结果是,如果您使用起始堆,然后在此处标记实际上最终出队的所有节点,它们将形成堆的子树(即,每个出队的节点都有一个也出队的父节点)。您可以看到这一点,因为如果不是这种情况,那么尽管节点本身已出队,但某些节点的(较大)父节点并未出队,这意味着值无序。

现在,考虑一下这棵树的节点是如何分布在堆中的。如果您将堆的级别标记为 0、1、2、...、k - 1,那么在级别 0、1、2、...、k - 2 中会有一些这些节点(即除了树的底层之外的所有内容)。为了让这些节点从堆中出列,它们必须向上交换到根节点,并且一次只能向上交换一层。这意味着降低堆排序运行时间的一种方法是计算将所有这些值带到根节点所需的交换次数。事实上,这正是我们要做的。

我们需要回答的第一个问题是——最大的 2k-1 个节点中有多少不在堆的底层?我们可以通过矛盾证明这不大于 2k-2。假设在堆的底层至少有 2k-2 + 1 个最大节点。那么这些节点的每个父节点也必须是级别 k - 2 中的大节点。即使在最好的情况下,这意味着级别中必须至少有 2k-3 + 1 个大节点k - 2,这意味着在第 k - 3 层中至少有 2k-4 + 1 个大节点,依此类推。总结所有这些节点,我们得到 2 k-2 + 2k-3 + 2k-4 + ... + 20 + k大节点。但是这个值严格大于 2k-1,这与我们在这里只使用 2k-1 个节点的事实相矛盾。

好的...我们现在知道底层最多有 2k-2 个大节点。这意味着前 k-2 层中必须至少有 2k-2 个大节点。我们现在要问——在所有这些节点上,从那个节点到根的距离的总和是多少?好吧,如果我们有 2k-2 个节点位于一个完整堆的某个位置,那么它们中的最多 2k-3 个节点可以位于前 k - 3 个级别,因此在第 k - 2 级中至少有 2k-2 - 2k-3 = 2k-3 个重节点。因此, 需要执行的交换总数至少为 (k - 2) 2k-3。由于 n = 2k-1,k = Θ(lg n),所以这个值是 Θ(n lg n)。

【讨论】:

  • 倒数第二段的第一个问题应该是——堆的底层有多少个最大的 2^(k-1) 个节点?或者您的假设句子中似乎有一些错误的矛盾。
  • 在最后一段中,“前k-2层”实际上不应该是“前k-1层”吗?此外,同样在最后一段中的句子,“那么最多 2^(k-3) 个可以在前 k-3 个级别中”我完全被搞糊涂了。这句话是否来自重复倒数第二段的证明?如果是这样,这句话实际上应该读作“那么其中至少有 2^(k-3) 个可以在前 k-2 个级别中”。但如果是这样,那么整个答案就崩溃了?
  • 我相信你的意思是说,由于前k-3层正好包含2^(k-3)-1个元素,所以底层以上的2^(k-2)个大元素必须至少有 2^(k-2)-2^(k-3)+1=2^(k-3)+1 位于 k-2 级或 k-3 级。因此,2^(k-3)+1 个大元素中的每一个都需要至少 k-3 次交换才能到达根。
  • 这个解释只是k-2级别需要的swap。我们不应该将 sum 用于进一步的内部层次吗?
【解决方案2】:

简单的观察答案是这样的: 堆中的项目是:

1
2
4
8
...
2^[log(n/4)]
and last level has between (1..2^[log(n/2)]) ==> (1,[n/2]) item, (by [] I mean Ceiling not roof)

例如,如果您有 7 个项目:

1
2
4

如果你有 8 个项目:

1
2
4
1

有2个不同的堆树,首先至少n / 4 - 1个堆的项目在最后一层,所以在最后一层之前至少有n/4 - 1项目,在第一种情况下需要O((n/4 - 1) * log(n/2)) 从堆中删除最后一级项目,在第二种情况下,需要O((n/4 - 1) * log(n/4)) 从最后一级删除项目。所以在这两种情况下,它只需要 Ω(n log(n)) 用于 n/4 - 1 个项目,所以它是一个下限(很容易说它是严格的下限)。

【讨论】:

    【解决方案3】:

    这是一个使用 CLRS 术语的解决方案:
    我们从一个最大堆开始,它是一个包含n 元素的完整二叉树。
    我们可以说,在一个完整的二进制文件中,有n/2 叶子和n/2 内部节点。
    n/2 HEAP-SORT 的迭代从堆中删除最大的n/2 元素。
    让S 成为最大n/2 元素的集合。
    叶子中最多可以有来自S 的n/4 元素,因为内部节点中必须有额外的n/4。
    让L 成为叶中S 中的这些n/4 最大元素。
    因此,如果在级别 0(叶子级别)有来自 S 的 n/4 元素 那么在级别 1 中必须至少有 n/8。
    让P 成为来自S 的位于级别1 的n/8 元素。
    n/2 HEAP-SORT 的迭代可能会给来自L 的元素提供一条通往根的捷径 然后从堆中移出,但来自P 的元素必须一直到达根,然后才能从堆中删除。
    所以至少有(n/8)(lgn-1)操作, 这给了我们 Ω(nlgn) 的运行时间。
    现在来看一个最大堆的情况,它的所有叶子都不是级别 0。
    设k 为它在第 0 层的叶子数。
    在 HEAP-SORT 的k 迭代之后,我们留下了一个最大堆,即 一棵高度为lgn-1的完全二叉树。
    我们可以用同样的方法继续我们的证明。
    现在针对S 的叶子少于n/4 的情况。
    令k 为来自S 的元素数量,这些元素位于级别0 的叶子中。
    如果 k <= n/8 则必须至少有来自级别 1 的 S 的 n/8 元素。
    这是因为在级别 1 之上总共可以有 n/4 个元素。
    我们以同样的方式继续证明。
    如果k>n/8 则必须至少有来自S 的n/16 元素位于级别1。
    我们以同样的方式继续证明。
    得出HEAP-SORT的运行时间为Ω(nlgn)。

    【讨论】:

    • 在这个解释中,我们只考虑级别 1 中的最大元素。进一步的内部级别呢? n/4 也将有助于找到叶子中的最大节点,对吧?
    猜你喜欢
    • 2018-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-08
    • 1970-01-01
    • 2018-08-28
    • 1970-01-01
    • 2016-12-25
    相关资源
    最近更新 更多