如何建立堆的时间复杂度为O(n)?


494

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

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

换句话说,对于我们“堆砌”的每个项目,它有可能不得不针对到目前为止的堆的每个级别(即log n个级别)过滤一次。

我想念什么?


“堆”堆到底是什么意思?
mfrankli 2012年

正如你会在一个堆排序,采取排序的数组和filterdown每个上半部分元素,直到它符合堆规则
GBA

2
我唯一能找到的就是这个链接:Buildheap的复杂性似乎是Θ(n lg n)–每次调用Heapify的n次调用,每次调用的代价为Θ(lg n),但是此结果可以提高到Θ(n) cs.txstate.edu/~ch04/webtest/teaching/courses/5329/lectures/...
GBA

2
@Gba观看麻省理工学院的这段视频:他用一点点数学就很好地解释了我们如何获得O(n)youtube.com/watch?v=B7hVxCmfPtM
CodeShadow

2
直接提到说明@CodeShadow的说明的链接:youtu.be/B7hVxCmfPtM?
sha1

Answers:


435

我认为此主题中存在几个问题:

  • 您如何实现buildHeap它以O(n)时间运行?
  • 如果正确实施,您如何证明它buildHeap可以在O(n)时间内运行?
  • 为什么相同的逻辑不能使堆排序在O(n)时间而不是O(n log n)中运行

您如何实现buildHeap它以O(n)时间运行?

通常情况下,这些问题的答案集中在之间的区别siftUpsiftDown。制作的正确选择siftUpsiftDown重要的是获得O(n)的性能buildHeap,但没有帮助一个明白之间的差别buildHeapheapSort一般。事实上,两者的正确实施buildHeapheapSort使用siftDownsiftUp仅需要执行此操作才能在现有堆中执行插入操作,因此该操作将用于例如使用二进制堆来实现优先级队列。

我已经写了这本书来描述最大堆的工作方式。这是堆的类型,通常用于堆排序或优先级队列,其中较高的值表示较高的优先级。最小堆也很有用;例如,当使用整数键以升序或字符串以字母顺序检索项目时。原理完全相同;只需切换排序顺序即可。

所述堆属性规定,在二元堆的每个节点必须至少一样大其两个孩子。特别是,这意味着堆中最大的项位于根。向下筛选和向上筛选本质上是在相反方向上的相同操作:移动有问题的节点,直到其满足heap属性为止:

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

所需的操作的数量siftDownsiftUp正比于节点可能必须移动的距离。对于siftDown,它是到树底的距离,因此对于树顶siftDown的节点来说代价很高。使用siftUp,功与到树顶部的距离成比例,因此siftUp对于树底部的节点而言代价昂贵。尽管在最坏的情况下两个操作都为O(log n),但在堆中,只有一个节点位于顶部,而一半的节点位于底层。所以应该不会太出人意料,如果我们要申请一个操作的每一个节点,我们宁愿siftDownsiftUp

buildHeap函数接收未排序项的数组并将其移动,直到它们全部满足heap属性,从而生成有效堆。buildHeap使用我们描述的siftUpsiftDown操作可能有两种方法。

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

  2. 或者,朝相反的方向:从数组的末尾开始,然后向前移。在每次迭代中,您向下筛选一个项目,直到其位于正确的位置。

哪种实现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)

一种方法(也可以使用其他分析方法)是将有限和变成无限级数,然后使用泰勒级数。我们可能会忽略第一项,它是零:

构建堆复杂性的泰勒级数

如果不确定每个步骤为何有效,请使用以下文字说明该过程的合理性:

  • 这些项都是正数,因此有限和必须小于无限和。
  • 该级数等于在x = 1/2处评估的幂级数。
  • 该幂级数等于(恒定倍)f(x)= 1 /(1-x)的泰勒级数的导数。
  • x = 1/2在该泰勒级数的收敛区间内。
  • 因此,我们可以用1 /(1-x)代替泰勒级数,求微分并求值以找到无限级数的值。

由于无穷大正好是n,因此我们得出结论:无穷大并不大,因此为O(n)

为什么堆排序需要O(n log 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)。通常通过删除根(堆中剩余的最大项)并将其替换为堆中的最后一项(叶),因此是最小项之一来实现。这个新的根几乎肯定会违反heap属性,因此您必须调用,siftDown直到将其移回可接受的位置为止。这也具有将下一个最大的项目移到根目录的作用。注意,与buildHeap大多数节点siftDown从树的底部调用的位置相反,现在我们siftDown在每次迭代中从树的顶部调用!尽管树正在收缩,但收缩得不够快:树的高度保持恒定,直到您删除了节点的前半部分(完全清除了底层)。然后对于下一个季度,高度为h-1。所以第二阶段的总工作是

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

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

总而言之,堆排序的工作是两个阶段的总和: buildHeap的时间为O(n),顺序删除每个节点的时间为O(n log n),因此复杂度为O(n log n)。您可以证明(使用信息论的一些想法),对于基于比较的排序,无论如何,O(n log n)是您所希望的最好的选择,因此没有理由对此感到失望或期望堆排序实现O(n)的时间限制buildHeap


2
我编辑了使用最大堆的答案,因为似乎大多数其他人都在使用它,这是堆排序的最佳选择。
杰里米·韦斯特

28
这使我直观地明白了这一点:“只有一个节点位于顶部,而一半节点位于底层。因此,如果我们必须对每个节点应用一个操作,我们会感到惊讶。比siftUp更喜欢siftDown。”
Vicky Chijwani 2014年

3
@JeremyWest“一个方法是从堆的顶部(数组的开头)开始,并在每个项目上调用siftUp。” -您是要从堆的底部开始吗?
aste123

4
@ aste123不,它是正确的。想法是在满足堆属性的数组部分与数组的未排序部分之间保持屏障。您可以从开始前进并调用siftUp每个项开始,或者从结束向后前进并调用siftDown。无论选择哪种方法,都将在数组的未排序部分中选择下一项,并执行适当的操作以将其移到数组的有序部分中的有效位置。唯一的区别是性能。
杰里米·韦斯特

2
这是我对世界上任何问题都从未见过的最佳答案。解释得很好,我真的很可能……非常感谢。
哈希尔·詹恩19/12/26

314

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

解释为什么构建堆是线性操作不是很容易,您最好阅读一下。

一个伟大的分析算法可以看出这里


主要思想是在build_heap算法中,heapify并非O(log n)所有元素的实际成本。

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

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

在最底层,有2^(h)节点,但是我们没有调用heapify任何节点,因此工作为0。在下一层,有2^(h − 1)节点,每个节点可能向下移动1层。从底部开始的第3层有2^(h − 2)节点,每个节点可能向下移动2层。

如您所见,并不是所有的heapify操作都是如此O(log n),这就是为什么要这样做O(n)


17
这是一个很好的解释...但是为什么堆排序在O(n log n)中运行。为什么相同的推理不适用于堆排序?
hba

49
@hba我想回答你的问题在于了解这一形象,从这篇文章HeapifyO(n)何时完成siftDownO(n log n)何时完成siftUp。实际排序(通过一个从一个堆拉项目)必须与进行siftUp这样因此O(n log n)
The111 2013年

3
我真的很喜欢您外部文档底部的直观说明。
Lukas Greblikas

1
@hba杰里米·韦斯特(Jeremy West)的“下面的答案”以更精细,易于理解的细节解决了您的问题,并在此处进一步解释了The111的评论答案。
cellepo 2015年

一个问题。在我看来,i对高度为h的树的底部处的高度处的节点进行的#比较也必须进行2* log(h-i)比较,并且也应将@ The111进行考虑。你怎么看?
2016年

94

直观地:

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

不完全的。您的逻辑不会产生严格的界限,而是会高估每个堆化的复杂性。如果从下往上构建,则插入(堆砌)可能比少得多O(log(n))。流程如下:

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

(第2步) 接下来的元素从底部开始在第1行。,heapify将过滤器降低1级。n/22h=1

(步骤i 接下来的元素从底部开始向上排。,heapify过滤器级别降低。n/2iih=ii

(步骤log(n) 最后一个元素从底部开始向上排。,heapify过滤器级别降低。n/2log2(n) = 1log(n)h=log(n)log(n)

注意:第一步之后,1/2元素(n/2)已经在堆中了,我们甚至不需要调用一次heapify。另外,请注意,实际上只有一个元素(即根)会带来全部log(n)复杂性。


理论上:

可以通过数学方式写出N构建size堆的步骤总数n

在height处i,我们已经显示了(上面)需要调用heapify的元素,并且我们知道在height 处的heapify 为。这给出:n/2i+1iO(i)

在此处输入图片说明

可以通过采用众所周知的几何级数方程两侧的导数来找到最后求和的解:

在此处输入图片说明

最后,插入x = 1/2上述方程式可得出2。将其插入第一个方程式可得出:

在此处输入图片说明

因此,步骤总数为 O(n)


35

如果通过重复插入元素来构建堆,则将为O(n log n)。但是,可以通过以任意顺序插入元素,然后应用算法将它们“堆化”为正确的顺序(当然取决于堆的类型),从而更有效地创建新的堆。

有关示例,请参见http://en.wikipedia.org/wiki/Binary_heap “构建堆”。在这种情况下,您实际上是从树的最低层开始工作,交换父节点和子节点,直到满足堆条件为止。


12

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

在此处输入图片说明

现在,看看在图像中,有
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/2^4 * 3 (heapify will perform atmost three swaps for each purple node)   

因此,对于任何高度为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

我们知道堆的高度是log(n),其中n是元素的总数,让它表示为h
   当我们执行heapify操作时,即使是单个元素,最后一层(h)的元素也不会移动步。
   位于倒数第二级(h-1)的元素数为2 h-1,并且它们最多可以移动1级(在heapify期间)。
   类似地,对于水平我们有2个元件,其可以移动位置。

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

                                               S = 2 小时 {1/2 + 2/2 2 + 3/2 3 + ... h / 2 小时 } ----------------------- -------------------------- 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 + 1/2 3 + ... + 1 / 2 h + h / 2 h + 1 }
                                               
                                                S = 2 H + 1 {1/2 + 1/2 2 + 1/2 3 + ... + 1/2 ħ + 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 =日志(n) 2 小时 = n

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


6

在建立堆时,可以说您正在采用自下而上的方法。

  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)。

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)

1

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

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

n! =~ O(n^(n + O(1)))因此,通过加星号近似,T =~ O(nlog(n))

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


1

基本上,仅在构建堆时才在非叶节点上完成工作...完成的工作是向下交换以满足堆条件的数量...换句话说(在最坏的情况下)该数量与高度成比例整个问题的复杂度与所有非叶子节点的高度之和成正比..(2 ^ h + 1-1)-h-1 = nh-1 =上)


1

@bcorso已经展示了复杂性分析的证据。但是为了那些仍然在学习复杂性分析的人,我要添加以下内容:

您最初错误的基础是由于对语句含义的错误解释,即“插入到堆中需要O(log n)时间”。插入到堆中确实是O(log n),但是您必须认识到n是插入期间堆的大小。

在将n个对象插入到堆中的情况下,第i个插入的复杂度为O(log n_i),其中n_i是在插入i时堆的大小。仅最后一次插入的复杂度为O(log n)。


1

假设您在堆中有N个元素。则其高度将为Log(N)

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

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

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

现在使用

日志a +日志b =日志ab

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

实际上是O(NlogN)

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

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


0

我真的很喜欢杰里米·韦斯特(Jeremy West)的解释。...此处提供了另一种真正易于理解的方法

因为,buildheap的使用取决于堆的大小,而shiftdown的方法则取决于所有节点的高度之和。因此,要找到由S =从(2 ^ i *(hi))的i = 0到i = h的总和得出的节点的高度之和,其中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),因此,构建堆的复杂度为O(n)。


0

“构建堆的线性时间边界可以通过计算堆中所有节点的高度之和来表示,即虚线的最大数量。对于高度为h的理想二叉树,其N = 2 ^( h + 1)– 1个节点,节点的高度之和为N – H –1。因此,它为O(N)。”


0

O(n)的证明

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


0

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

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

为了计算从后方开始的一行中的节点,我们知道n / 2个节点在底部,因此通过除以2得到上一行,依此类推。

在此基础上,我们得到了Siftdown方法的公式:(0 * n / 2)+(1 * n / 4)+(2 * n / 8)+ ... +(log2(n)* 1)

最后一个括号中的术语是树的高度乘以根处的一个节点,第一个括号中的术语是底部行中的所有节点乘以它们可以通过的长度,0。智能中的相同公式: 在此处输入图片说明

数学

将n带回我们有2 * n,因为它是一个常数和tada,所以我们可以舍弃2,因为Siftdown方法的运行时间最差:n。


-6

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


2
不,它不紧。对建筑堆产量的更严格分析O(n)
emre nevayeshirazi

看起来这是一个估计。他引用的文章中的引用是“直觉上,大多数对heapify的调用都在很短的堆上”。但这是在做一些假设。据推测,对于大堆,即使通常可以接近O(n),最坏的情况仍然是O(n * lg(n))。但是我可能错了
Mike Schachter 2012年

是的,这也是我的直觉答案,但是诸如Wikipedia这样的引用指出“可以在O(n)中自底向上构造具有n个元素的堆”。
GBa 2012年

1
我在考虑一个完全排序的数据结构。我忘记了堆的特定属性。
Mike Schachter 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.