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

喜夏-厌秋 提交于 2020-02-26 11:35:50

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

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

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

我想念什么?


#1楼

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

  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楼

直观地:

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

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

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

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

(步骤i 下一个n/2 i元素从底部开始在第i行中。 h=i ,heapify过滤器i降级。

(步骤log(n) 最后n/2 log 2 (n) = 1元素从底部向上进入log(n)行。 h=log(n) ,heapify向下过滤log(n)级别。

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


理论上:

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

在高度i ,我们(上面)表明将有n/2 i+1元素需要调用heapify,并且我们知道在高度i处的heapify为O(i) 。 这给出:

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

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

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


#3楼

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

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

如何实现buildHeap ,使其在O(n)时间内运行?

通常,这些问题的答案集中在siftUpsiftDown之间的差异上。 在siftUpsiftDown之间做出正确的选择对于获得siftDown O(n)性能buildHeap ,但通常无助于帮助您了解buildHeapheapSort之间的区别。 确实, buildHeapheapSort正确实现都只会使用siftDown 。 仅在向现有堆中执行插入操作时才需要siftUp操作,因此,该操作将用于例如使用二进制堆来实现优先级队列。

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

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

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

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

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实现的低效率版本buildHeap一样,该总和为O(n log n) 。 但是在这种情况下,我们别无选择,因为我们正在尝试排序,因此我们要求下一个最大的项目被删除。

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


#4楼

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

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

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

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


#5楼

我真的很喜欢杰里米·韦斯特(Jeremy West)的解释。...此处提供了另一种非常容易理解的方法, 网址为http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity

因为,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)。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!