有人可以帮助解释如何建立堆的O(n)复杂性吗?
将项目插入堆中是O(log n)
,插入重复n / 2次(其余为叶子,并且不能违反堆属性)。因此,O(n log n)
我认为这意味着复杂度应为。
换句话说,对于我们“堆砌”的每个项目,它有可能不得不针对到目前为止的堆的每个级别(即log n个级别)过滤一次。
我想念什么?
有人可以帮助解释如何建立堆的O(n)复杂性吗?
将项目插入堆中是O(log n)
,插入重复n / 2次(其余为叶子,并且不能违反堆属性)。因此,O(n log n)
我认为这意味着复杂度应为。
换句话说,对于我们“堆砌”的每个项目,它有可能不得不针对到目前为止的堆的每个级别(即log n个级别)过滤一次。
我想念什么?
Answers:
我认为此主题中存在几个问题:
buildHeap
它以O(n)时间运行?buildHeap
可以在O(n)时间内运行?buildHeap
它以O(n)时间运行?通常情况下,这些问题的答案集中在之间的区别siftUp
和siftDown
。制作的正确选择siftUp
和siftDown
重要的是获得O(n)的性能buildHeap
,但没有帮助一个明白之间的差别buildHeap
和heapSort
一般。事实上,两者的正确实施buildHeap
和heapSort
将仅使用siftDown
。siftUp
仅需要执行此操作才能在现有堆中执行插入操作,因此该操作将用于例如使用二进制堆来实现优先级队列。
我已经写了这本书来描述最大堆的工作方式。这是堆的类型,通常用于堆排序或优先级队列,其中较高的值表示较高的优先级。最小堆也很有用;例如,当使用整数键以升序或字符串以字母顺序检索项目时。原理完全相同;只需切换排序顺序即可。
所述堆属性规定,在二元堆的每个节点必须至少一样大其两个孩子。特别是,这意味着堆中最大的项位于根。向下筛选和向上筛选本质上是在相反方向上的相同操作:移动有问题的节点,直到其满足heap属性为止:
siftDown
用最大的子节点交换太小的节点(从而将其向下移动),直到其大小至少与其下面的两个节点一样大。 siftUp
与其父节点交换一个太大的节点(从而将其向上移动),直到它不大于其上方的节点为止。 所需的操作的数量siftDown
和siftUp
正比于节点可能必须移动的距离。对于siftDown
,它是到树底的距离,因此对于树顶siftDown
的节点来说代价很高。使用siftUp
,功与到树顶部的距离成比例,因此siftUp
对于树底部的节点而言代价昂贵。尽管在最坏的情况下两个操作都为O(log n),但在堆中,只有一个节点位于顶部,而一半的节点位于底层。所以应该不会太出人意料,如果我们要申请一个操作的每一个节点,我们宁愿siftDown
过siftUp
。
该buildHeap
函数接收未排序项的数组并将其移动,直到它们全部满足heap属性,从而生成有效堆。buildHeap
使用我们描述的siftUp
和siftDown
操作可能有两种方法。
从堆的顶部(数组的开头)开始,并调用siftUp
每个项目。在每个步骤中,先前筛选的项目(数组中当前项目之前的项目)形成有效堆,然后向上筛选下一个项目将其放入堆中的有效位置。筛选每个节点后,所有项目均满足heap属性。
或者,朝相反的方向:从数组的末尾开始,然后向前移。在每次迭代中,您向下筛选一个项目,直到其位于正确的位置。
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)?一种方法(也可以使用其他分析方法)是将有限和变成无限级数,然后使用泰勒级数。我们可能会忽略第一项,它是零:
如果不确定每个步骤为何有效,请使用以下文字说明该过程的合理性:
由于无穷大正好是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)。通常通过删除根(堆中剩余的最大项)并将其替换为堆中的最后一项(叶),因此是最小项之一来实现。这个新的根几乎肯定会违反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
。
siftUp
每个项开始,或者从结束向后前进并调用siftDown
。无论选择哪种方法,都将在数组的未排序部分中选择下一项,并执行适当的操作以将其移到数组的有序部分中的有效位置。唯一的区别是性能。
您的分析是正确的。但是,它并不紧密。
解释为什么构建堆是线性操作不是很容易,您最好阅读一下。
一个伟大的分析算法可以看出这里。
主要思想是在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)
。
Heapify
是O(n)
何时完成siftDown
但O(n log n)
何时完成siftUp
。实际排序(通过一个从一个堆拉项目)必须与进行siftUp
这样因此O(n log n)
。
i
对高度为h的树的底部处的高度处的节点进行的#比较也必须进行2* log(h-i)
比较,并且也应将@ The111进行考虑。你怎么看?
“复杂度应该为O(nLog n)...对于我们“堆砌”的每个项目,到目前为止,它有可能必须为堆的每个级别(即log n个级别)过滤一次。
不完全的。您的逻辑不会产生严格的界限,而是会高估每个堆化的复杂性。如果从下往上构建,则插入(堆砌)可能比少得多O(log(n))
。流程如下:
(步骤1) 第一个n/2
元素位于堆的底部行。h=0
,因此不需要heapify。
(第2步) 接下来的元素从底部开始在第1行。,heapify将过滤器降低1级。n/22
h=1
(步骤i)
接下来的元素从底部开始向上排。,heapify过滤器级别降低。n/2i
i
h=i
i
(步骤log(n)) 最后一个元素从底部开始向上排。,heapify过滤器级别降低。n/2log2(n) = 1
log(n)
h=log(n)
log(n)
注意:第一步之后,1/2
元素(n/2)
已经在堆中了,我们甚至不需要调用一次heapify。另外,请注意,实际上只有一个元素(即根)会带来全部log(n)
复杂性。
可以通过数学方式写出N
构建size堆的步骤总数n
。
在height处i
,我们已经显示了(上面)需要调用heapify的元素,并且我们知道在height 处的heapify 为。这给出:n/2i+1
i
O(i)
可以通过采用众所周知的几何级数方程两侧的导数来找到最后求和的解:
最后,插入x = 1/2
上述方程式可得出2
。将其插入第一个方程式可得出:
因此,步骤总数为 O(n)
如果通过重复插入元素来构建堆,则将为O(n log n)。但是,可以通过以任意顺序插入元素,然后应用算法将它们“堆化”为正确的顺序(当然取决于堆的类型),从而更有效地创建新的堆。
有关示例,请参见http://en.wikipedia.org/wiki/Binary_heap “构建堆”。在这种情况下,您实际上是从树的最低层开始工作,交换父节点和子节点,直到满足堆条件为止。
已经有了一些不错的答案,但是我想添加一些视觉上的解释
现在,看看在图像中,有
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)
我们知道堆的高度是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)
在建立堆时,可以说您正在采用自下而上的方法。
在构建堆的情况下,我们从高度 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)
连续插入可以通过以下方式描述:
T = O(log(1) + log(2) + .. + log(n)) = O(log(n!))
n! =~ O(n^(n + O(1)))
因此,通过加星号近似,T =~ O(nlog(n))
希望这会O(n)
有所帮助,最佳方法是对给定的集合使用构建堆算法(顺序无关紧要)。
基本上,仅在构建堆时才在非叶节点上完成工作...完成的工作是向下交换以满足堆条件的数量...换句话说(在最坏的情况下)该数量与高度成比例整个问题的复杂度与所有非叶子节点的高度之和成正比..(2 ^ h + 1-1)-h-1 = nh-1 =上)
假设您在堆中有N个元素。则其高度将为Log(N)
现在您要插入另一个元素,那么复杂度将是:Log(N),我们必须一直将UP与根进行比较。
现在您有N + 1个元素,高度= Log(N + 1)
使用归纳技术可以证明插入的复杂度为∑logi。
现在使用
日志a +日志b =日志ab
简化为:∑logi = log(n!)
实际上是O(NlogN)
但
我们在这里做错了,因为在所有情况下我们都没有到达顶部。因此,在执行大多数时间时,我们可能会发现,我们甚至没有走到树的一半。因此,可以通过使用上面答案中给出的数学将该边界优化为更严格的边界。
经过对Heaps的详细了解和实验之后,我才意识到了这一点。
我们通过计算每个节点可以执行的最大移动来获得堆构建的运行时。因此,我们需要知道每行中有多少个节点,以及每个节点可以离开的距离。
从根节点开始,下一行的节点数是上一行的两倍,因此通过回答多久可以增加一倍的节点数,直到没有剩余的节点,我们就可以得到树的高度。或者用数学术语来说,树的高度是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。
认为你在犯错。看一下这个:http : //golang.org/pkg/container/heap/构建堆不是O(n)。但是,插入是O(lg(n)。我假设初始化是O(n),如果您设置了b / c的堆大小,则堆需要分配空间并设置数据结构。进入堆,然后是,每个插入都是lg(n),并且有n个项,因此您得到n * lg(n),如您所说