如何构建堆是O(n)时间复杂度?

时间:2012-03-18 03:15:59

标签: algorithm heap complexity-theory construction

有人可以帮助解释如何构建堆是O(n)复杂性吗?

将项插入堆中是O(log n),并且插入重复n / 2次(其余为叶子,并且不能违反堆属性)。所以,这意味着复杂性应该是O(n log n),我想。

换句话说,对于我们“堆积”的每个项目,它有可能必须针对堆的每个级别过滤掉一次(这是log n级别)。

我错过了什么?

17 个答案:

答案 0 :(得分:305)

我认为这个主题有几个问题:

  • 如何实现buildHeap以便它在 O(n)时间运行?
  • 如果正确实施,如何显示buildHeap在 O(n)时间运行?
  • 为什么没有相同的逻辑工作使堆排序在 O(n)时间运行而不是 O(n log n)

通常,这些问题的答案都集中在siftUpsiftDown之间的区别。在siftUpsiftDown之间做出正确的选择对于获得buildHeap O(n)性能至关重要,但无助于理解buildHeap之间的差异{1}}和heapSort一般。实际上,buildHeapheapSort的正确实现只会 使用siftDown。只有在现有堆中执行插入时才需要siftUp操作,因此它将用于使用二进制堆实现优先级队列。

我写这篇文章来描述最大堆的工作原理。这是通常用于堆排序或优先级队列的堆类型,其中较高的值表示较高的优先级。最小堆也很有用;例如,当按升序检索具有整数键的项目或按字母顺序检索字符串时。原则完全一样;只需切换排序顺序。

堆属性指定二进制堆中的每个节点必须至少与其两个子节点一样大。特别是,这意味着堆中的最大项目位于根。向下筛选和筛选在相反的方向上基本上是相同的操作:移动违规节点直到它满足堆属性:

  • siftDown交换一个太小的节点与其最大的子节点(从而向下移动),直到它至少与它下面的两个节点一样大。
  • siftUp交换一个与其父级太大的节点(从而将其向上移动),直到它不大于其上方的节点。

siftDownsiftUp所需的操作数量与节点可能必须移动的距离成正比。对于siftDown,它是到树底部的距离,因此对于树顶部的节点来说siftDown是昂贵的。对于siftUp,工作与到树顶部的距离成比例,因此对于树底部的节点,siftUp是昂贵的。虽然在最坏的情况下两个操作都是 O(log n),但在堆中,只有一个节点位于顶部,而一半节点位于底层。因此,如果我们必须对每个节点应用操作,那么就不会太令人惊讶,我们希望siftDown优先于siftUp

buildHeap函数接受一组未排序的项并移动它们直到它们都满足heap属性,从而生成一个有效的堆。使用我们已描述的buildHeapsiftUp操作,siftDown可能采用两种方法。

  1. 从堆的顶部(数组的开头)开始,并在每个项目上调用siftUp。在每一步中,先前筛选的项(数组中当前项之前的项)形成有效堆,并筛选下一项将其置于堆中的有效位置。筛选每个节点后,所有项都满足堆属性。

  2. 或者,向相反方向前进:从阵列末端开始向前移动。在每次迭代时,您都要向下筛选一个项目,直到它位于正确的位置。

  3. 这两种解决方案都会产生有效的堆。问题是:buildHeap的哪种实施更有效?不出所料,这是使用siftDown的第二个操作。

    h = log n 表示堆的高度。 siftDown方法所需的工作由总和

    给出
    (0 * n/2) + (1 * n/4) + (2 * n/8) + ... + (h * 1).
    

    总和中的每个项都具有给定高度处的节点必须移动的最大距离(底层为零,根为h)乘以该高度处的节点数。相反,每个节点上调用siftUp的总和是

    (h * n/2) + ((h-1) * n/4) + ((h-2)*n/8) + ... + (0 * 1).
    

    应该很清楚,第二笔金额更大。单独的第一项是 hn / 2 = 1/2 n log n ,因此这种方法最多具有复杂度 O(n log n)。但是,我们如何证明siftDown方法的总和确实是 O(n)?一种方法(还有其他分析也有效)是将有限和转换为无限级数,然后使用泰勒级数。我们可能会忽略第一项,即零:

    Taylor series for buildHeap complexity

    如果你不确定为什么每个步骤都有效,那么这就是用语言证明这个过程的理由:

    • 这些术语都是正数,因此有限和必须小于无限和。
    • 该系列等于在 x = 1/2 评估的幂级数。
    • 对于 f(x)= 1 /(1-x)
    • ,幂级数等于(常数)泰勒级数的导数。
    • x = 1/2 在该泰勒级数的收敛区间内。
    • 因此,我们可以用 1 /(1-x)替换泰勒级数,进行区分,并评估以找到无限级数的值。

    由于无限和恰好是 n ,我们得出结论,有限和并不大,因此, O(n)

    接下来的问题是:如果可以在线性时间内运行buildHeap,为什么堆排序需要 O(n log n)时间?堆排序包括两个阶段。首先,我们在数组上调用buildHeap,如果以最佳方式实现,则需要 O(n)时间。下一步是重复删除堆中最大的项并将其放在数组的末尾。因为我们从堆中删除了一个项目,所以在堆的末尾之后总是存在一个开放点,我们可以存储该项目。因此,堆排序通过连续删除下一个最大项并将其放入从最后位置开始并向前移动的数组来实现排序顺序。最后一部分的复杂性在堆排序中占主导地位。循环看起来像这样:

    for (i = n - 1; i > 0; i--) {
        arr[i] = deleteMax();
    }
    

    显然,循环运行O(n)次(确切地说, n - 1 ,最后一项已经到位)。堆的deleteMax的复杂性是 O(log n)。它通常通过删除根(堆中剩余的最大项)并将其替换为堆中的最后一项(即叶子)来实现,因此将其替换为最小项之一。这个新的root几乎肯定会违反堆属性,所以你必须调用siftDown,直到你将它移回到可接受的位置。这也具有将下一个最大项目移动到根目录的效果。请注意,与buildHeap相反,对于我们从树的底部调用siftDown的大多数节点,我们现在在每次迭代时从树的顶部调用siftDown虽然树正在缩小,但它没有足够快地缩小:树的高度保持不变,直到你移除了节点的前半部分(当你完全清除底层时) 。然后在下一季度,高度 h - 1 。所以第二阶段的总工作是

    h*n/2 + (h-1)*n/4 + ... + 0 * 1.
    

    注意开关:现在零工作情况对应于单个节点, h 工作情况对应于一半节点。这个总和是 O(n log n),就像使用siftUp实现的buildHeap的低效版本一样。但在这种情况下,我们别无选择,因为我们正在尝试排序,我们要求下一个最大的项目被删除。

    总之,堆排序的工作是两个阶段的总和:buildHeap的 O(n)时间和 O(n log n)按顺序删除每个节点,所以复杂性是O(n log n)。您可以证明(使用信息理论中的一些想法),对于基于比较的排序, O(n log n)是您可能希望的最佳方式,因此没有理由对此感到失望或期望堆排序达到buildHeap所做的O(n)时间限制。

答案 1 :(得分:290)

您的分析是正确的。但是,它并不紧张。

解释为什么构建堆是线性操作并不是很容易,你应该更好地阅读它。

可以看到算法的精彩分析 here


主要想法是,在build_heap算法中,所有元素的实际heapify费用都不是O(log n)

调用heapify时,运行时间取决于在进程终止之前元素在树中向下移动的距离。换句话说,它取决于堆中元素的高度。在最坏的情况下,元素可能会一直下降到叶级。

让我们逐级计算完成的工作。

在最底层,有2^(h)个节点,但我们不会在其中任何一个上调用heapify,因此工作为0.在下一个级别有2^(h − 1)节点,每个节点可能向下移动1级。在底部的第3层,有2^(h − 2)个节点,每个节点可能向下移动2个级别。

正如您所看到的,并非所有的堆化操作都是O(log n),这就是您获得O(n)的原因。

答案 2 :(得分:87)

直观地:

  

“复杂性应该是O(nLog n)...对于我们”堆积“的每个项目,它有可能必须为目前为止的堆的每个级别过滤掉一次(这是log n级别)。 “

不完全。你的逻辑不会产生严格的限制 - 它估计每个堆的复杂性。如果从下到上构建,则插入(heapify)可能远小于O(log(n))。过程如下:

(步骤1) 第一个n/2元素位于堆的底行。 h=0,因此不需要堆化。

(第2步) 下一个n/22元素从底部开始排在第1行。 h=1,将过滤器堆叠1级。

(步骤 下一个n/2i元素从底部向上排i行。 h=i,将过滤器i级别堆积起来。

(步骤 log(n) 最后一个n/2log2(n) = 1元素从底部向上行log(n)h=log(n),将过滤器log(n)级别堆积起来。

注意:在第一步之后,1/2元素(n/2)已经在堆中,我们甚至不需要调用一次heapify。另外,请注意,只有一个元素(根)实际上会产生完整的log(n)复杂度。


理论上:

构建大小为N的堆的总步骤n可以用数学方式写出来。

i高度,我们已经显示(上图)需要调用heapify的n/2i+1个元素,并且我们知道在i处的heapify是{{1} }。这给出了:

  

enter image description here

通过得到众所周知的几何级数方程两边的导数,可以找到最后求和的解:

  

enter image description here

最后,将O(i)插入上述等式会产生x = 1/2。将其插入第一个等式给出:

  

enter image description here

因此,步骤总数的大小为2

答案 3 :(得分:33)

如果通过重复插入元素来构建堆,那么它将是O(n log n)。但是,您可以通过以任意顺序插入元素然后应用算法将它们“堆积”到正确的顺序(根据堆的类型当然)来更有效地创建新堆。

有关示例,请参阅http://en.wikipedia.org/wiki/Binary_heap,“构建堆”。在这种情况下,您基本上从树的底层开始,交换父节点和子节点,直到满足堆条件。

答案 4 :(得分:7)

我们知道堆的高度是 log(n),其中n是元素的总数。让我们将其表示为 h
当我们执行heapify操作时,最后一级( h )的元素甚至不会移动一步。
第二级的元素数量( h-1 2 h-1 ,他们可以在最大 1 级别移动(在堆化期间) 。
同样,对于 i th 级别,我们 2 i 可以移动 hi 位置的元素。

因此总移动次数= S = 2 h * 0 + 2 H-1 * 1 + 2 H-2 * 2 + ... 2 0 * H

S = 2 h {1/2 + 2/2 2 + 3/2 3 + ... h / 2 h } ------------------------------------ ------------- 1个
这是 AGP 系列,用2解决这个划分双方 S / 2 = 2 h {1/2 2 + 2/2 3 + ... h / 2 h + 1 } ------------------------- ------------------------ 2
1 中减去等式 2 给出了 S / 2 = 2 h {1/2> 1/2 2 < / strong> + 1/2 3 + ... + 1/2 h + h / 2 h + 1 } <登记/> S = 2 h + 1 {1/2> 1/2 2 < / strong> + 1/2 3 + ... + 1/2 h + h / 2 h + 1 } <登记/> 现在 1/2 + 1/2 2 + 1/2 3 + ... + 1/2 h 正在减少 GP ,其总和较少比 1 (当h趋于无穷大时,总和趋于1)。在进一步分析中,让我们取总和为1的上限 这使得 S = 2 h + 1 {1 + h / 2 h + 1 }
= 2 H + 1 + h
~2 h + h
as h = log(n) 2 h = n

因此 S = n + log(n)
T(C)= O(n)

答案 5 :(得分:5)

在构建堆时,假设您采用自下而上的方法。

  1. 您将每个元素与其子元素进行比较,以检查该元素是否符合堆规则。因此,叶子可以免费包含在堆中。那是因为他们没有孩子。
  2. 向上移动,叶子正上方节点的最坏情况是1比较(最大值,它们只与一代孩子进行比较)
  3. 进一步向上移动,他们的直系父母最多可以与两代孩子进行比较。
  4. 继续朝着相同的方向,在最坏的情况下,您将对根进行log(n)比较。并为其直接子项记录log(n)-1,为其直接子项记录log(n)-2,依此类推。
  5. 总而言之,你得到的东西就像log(n)+ {log(n)-1} * 2 + {log(n)-2} * 4 + ..... + 1 * 2 ^ {(logn)-1}这只是O(n)。

答案 6 :(得分:4)

已经有了一些不错的答案,但是我想添加一些视觉上的解释

enter image description here

现在,看看图像,有
n/2^1具有高度0 (此处为23/2 = 12)的绿色节点
n/2^2具有高度1 (此处为23/4 = 6)的红色节点
n/2^3具有高度2 (此处为23/8 = 3)的蓝色节点
n/2^4具有高度3 (此处为23/16 = 2)的紫色节点
因此存在n/2^(h+1)个高度为 h
的节点 要找到时间复杂度,请计算每个节点的已完成工作量最大迭代次数
现在可以注意到,每个节点都可以(最多)执行迭代==节点的高度

Green = n/2^1 * 0 (no iterations since no children)  
red   = n/2^2 * 1 (*heapify* will perform atmost one swap for each red node)  
blue  = n/2^3 * 2 (*heapify* will perform atmost two swaps for each blue node)  
purple = n/4^3 * 3  

因此,对于任何高度为h的 个节点 ,最大工作量为 n / 2 ^(h + 1)* h

现在完成的总工作量是

->(n/2^1 * 0) + (n/2^2 * 1)+ (n/2^3 * 2) + (n/2^4 * 3) +...+ (n/2^(h+1) * h)  
-> n * ( 0 + 1/4 + 2/8 + 3/16 +...+ h/2^(h+1) ) 

现在对于任何 h

-> ( 0 + 1/4 + 2/8 + 3/16 +...+ h/2^(h+1) ) 

永远不会超过1
因此,用于构建堆的时间复杂度永远不会超过 O(n)

答案 7 :(得分:2)

在构建堆的情况下,我们从高处开始,      logn -1 (其中logn是n个元素的树的高度)。 对于高度为'h'的每个元素,我们最多可以达到(logn -h)高度。

    So total number of traversal would be:-
    T(n) = sigma((2^(logn-h))*h) where h varies from 1 to logn
    T(n) = n((1/2)+(2/4)+(3/8)+.....+(logn/(2^logn)))
    T(n) = n*(sigma(x/(2^x))) where x varies from 1 to logn
     and according to the [sources][1]
    function in the bracket approaches to 2 at infinity.
    Hence T(n) ~ O(n)

答案 8 :(得分:1)

连续插入可以通过以下方式描述:

T = O(log(1) + log(2) + .. + log(n)) = O(log(n!))

通过八度近似,n! =~ O(n^(n + O(1))),因此T =~ O(nlog(n))

希望这会有所帮助,O(n)为给定集合使用构建堆算法的最佳方式(排序无关紧要)。

答案 9 :(得分:1)

Proof of O(n)

证明并不简单,而且非常简单,我只证明了完整二叉树的情况,结果可以推广到完整的二叉树。

答案 10 :(得分:1)

@bcorso已经证明了复杂性分析的证据。但是为了那些仍在学习复杂性分析的人,我要补充一下:

原始错误的基础是由于对语句含义的误解,&#34;插入堆需要O(log n)时间&#34;。插入堆确实是O(log n),但你必须认识到n是插入期间的大小。

在将n个对象插入堆中的上下文中,第i个插入的复杂性为O(log n_i),其中n_i是插入i时堆的大小。只有最后一次插入的复杂度为O(log n)。

答案 11 :(得分:0)

我们通过计算每个节点可以执行的最大移动来获得堆构建的运行时。 因此,我们需要知道每行中有多少个节点,以及每个节点可以离开的距离。

从根节点开始,下一行的节点数是前一行的两倍,因此,通过回答可以将节点数增加一倍,直到没有剩余的节点,我们才能得到树的高度。 或者用数学术语来说,树的高度是log2(n),n是数组的长度。

要计算从背面开始的一行中的节点,我们知道n / 2个节点在底部,因此将其除以2得到上一行,依此类推。

基于此,我们得到以下用于Siftdown方法的公式: (0 * n / 2)+(1 * n / 4)+(2 * n / 8)+ ... +(log2(n)* 1)

最后一个括号中的项是树的高度乘以根处的一个节点,第一个括号中的项是底行中的所有节点乘以它们可以通过的长度,0。 智能中的相同公式: enter image description here

Math

将n带回2 * n,可以丢弃2,因为它的常数和tada是Siftdown方法的最差运行时间:n。

答案 12 :(得分:0)

让我们假设您在堆中有 N 个元素。 然后其高度将为 Log(N)

现在您要插入另一个元素,那么复杂度将是: Log(N),我们必须将 UP 一直与根进行比较。

现在您有 N + 1 个元素,高度= Log(N + 1)

使用induction技术,可以证明插入的复杂度为 ∑logi

现在使用

  

日志a +日志b =日志ab

这可以简化为: ∑logi = log(n!)

实际上是 O(NlogN)

但是

我们在这里做错了,因为在所有情况下我们都没有到达顶部。 因此,在执行大多数时间时,我们可能会发现,我们甚至没有走到树的一半。因此,可以使用上面答案中给出的数学方法将此边界优化为更严格的边界。

经过对Heaps的详细了解和实验之后,我才意识到这一点。

答案 13 :(得分:0)

基本上,在构建堆时,只在非叶节点上完成工作......并且完成的工作是交换以满足堆条件的数量...换句话说(在最坏的情况下)数量是成比例的到节点的高度......总而言之,问题的复杂性与所有非叶节点的高度之和成正比..这是(2 ^ h + 1 - 1)-h-1 = nh -1 = O(n)

答案 14 :(得分:0)

&#34;构建堆的线性时间限制可以通过计算堆中所有节点的高度之和来显示,这是最大虚线数。 对于包含N = 2 ^(h + 1)-1个节点的高度为h的完美二叉树,节点高度之和为N-H-1。 因此它是O(N)。&#34;

答案 15 :(得分:0)

我真的很喜欢Jeremy west的解释......这里给出了另一种非常容易理解的方法http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity

因为,buildheap依赖于使用依赖于heapify和shiftdown方法,它取决于所有节点的高度之和。因此,要找到由给定的节点高度之和           S =从i = 0到i = h的总和(2 ^ i *(h-i)),其中h = logn是树的高度         求s,得s = 2 ^(h + 1) - 1 - (h + 1)         因为,n = 2 ^(h + 1) - 1         s = n - h - 1 = n-logn - 1         s = O(n),因此buildheap的复杂度为O(n)。

答案 16 :(得分:-6)

认为你犯了一个错误。看看这个:http://golang.org/pkg/container/heap/建立堆不是O(n)。但是,插入是O(lg(n)。我假设初始化为O(n),如果设置堆大小b / c,则堆需要分配空间并设置数据结构。 如果你有n个项目放入堆中然后是,每个插入是lg(n)并且有n个项目,所以你得到n * lg(n),如你所说