堆排序时间复杂性深刻理解

时间:2015-08-20 16:02:10

标签: sorting heap time-complexity heapsort amortized-analysis

当我在大学学习数据结构课程时,我学到了以下公理:

  1. 在最坏的情况下,将新数字插入堆需要 O(logn)(取决于作为叶插入时树到达的高度)

    < / LI>
  2. 从空堆开始,使用 n 插入构建一堆 n 节点,总计为 O(n)时间,使用摊销分析

  3. 在最坏情况下删除最小值 O(logn)时间(取决于新顶节点在与最后一个叶子交换后达到的低点)

  4. 逐个删除所有最小值,直到堆为空,取 O(nlogn)时间复杂度


  5. 提醒:&#34; heapsort&#34;的步骤算法是:

    • 将所有数组值添加到堆中:使用摊销分析技巧求和 O(n)时间复杂度
    • 弹出堆 n 次的最小值,并将 i -th值放在数组的 i -th索引中: O(nlogn)时间复杂度,因为当弹出最小值
    • 时,摊销分析技巧不起作用


    我的问题是:为什么在清空堆时,摊销分析技巧不起作用,导致堆排序算法采用 O(nlogn)时间而不是 O(n)时间?

2 个答案:

答案 0 :(得分:1)

假设您只是通过比较它们来了解两个对象的相对排名,那么就无法在时间O(n)中将所有元素从二进制堆中出列。如果你能做到这一点,那么你可以通过在时间O(n)中构建一个堆,然后在时间O(n)中出列所有内容,在时间O(n)中对列表进行排序。但是,排序下限表示比较排序,为了正确,平均必须具有Ω(n log n)的运行时间。换句话说,你不能太快从堆中出队,或者你打破了排序障碍。

还有一个问题是,为什么从二进制堆中出列n个元素需要时间O(n log n)而不是更快。这显示有点棘手,但这是基本的想法。考虑一下你在堆上出列的前半部分。查看实际出列的值,并考虑它们在堆中的位置。排除底行的那些,所有出列的其他东西必须一次渗透到堆顶部一次,以便被移除。您可以显示堆中有足够的元素来保证单独使用时间Ω(n log n),因为这些节点中大约有一半将位于树的深处。这解释了为什么摊销的论证不起作用 - 你不断地将深层节点拉到堆上,因此节点必须行进的总距离很大。将其与heapify操作进行比较,其中大多数节点行进的距离非常短。

答案 1 :(得分:1)

让我“数学地”向您展示我们如何计算将任意数组转换为堆(让我称之为“堆构建”)然后使用堆排序对其进行排序的复杂性。

堆构建时间分析

为了将数组转换为堆,我们必须查看每个有子节点的节点并“堆化”(接收)该节点。您应该问问自己我们进行了多少次比较;如果你仔细想想,你会看到(h = 树高):

  • 对于第 i 层的每个节点,我们进行 h-i 比较:#comparesOneNode(i) = h-i
  • 在第 i 层,我们有 2^i 个节点:#nodes(i) = 2^i
  • 因此,通常 T(n,i) = #nodes(i) * #comparesOneNode(i) = 2^i *(h-i),是在“i”级“比较”所花费的时间

让我们举个例子。假设有一个包含 15 个元素的数组,即树的高度为 h = log2(15) = 3:

  • 在第 i=3 级,我们有 2^3=8 个节点,我们对每个节点进行 3-3 次比较:正确,因为在第 3 级我们只有没有孩子的节点,即叶子。 T(n, 3) = 2^3*(3-3) = 0
  • 在第 i=2 级,我们有 2^2=4 个节点,我们对每个节点进行 3-2 次比较:正确,因为在第 2 级,我们只有第 3 级可以进行比较。 T(n, 2) = 2^2*(3-2) = 4 * 1
  • 在第 i=1 层,我们有 2^1=2 个节点,我们对每个节点进行 3-1 次比较:T(n, 1) = 2^1*(3-1) = 2 * 2
  • 在第 i=0 层,我们有 2^0=1 个节点,即根节点,我们进行 3-0 比较:T(n, 0) = 2^0*(3-0) = 1 * 3< /li>

好的,一般来说:

T(n) = sum(i=0 to h) 2^i * (h-i)

但是如果你还记得 h = log2(n),我们有

T(n) = sum(i=0 to log2(n)) 2^i * (log2(n) - i) =~ 2n

堆排序时间分析

现在,这里的分析非常相似。每次我们“删除”最大元素(根)时,我们都会移动到树中最后一片叶子的根,堆化它并重复直到最后。那么,我们在这里进行了多少次比较?

  • 在第 i 层,我们有 2^i 个节点:#nodes(i) = 2^i
  • 对于级别“i”的每个节点,在最坏的情况下,heapify 将始终执行与级别“i”完全相同的相同数量的比较(我们从级别 i 中取出一个节点,将其移动到根, 调用 heapify, heapify 在最坏的情况下会将节点带回到级别 i, 执行“i”比较): #comparesOneNode(i) = i
  • 所以,一般来说 T(n,i) = #nodes(i) * #comparesOneNode(i) = 2^i*i,就是移除前 2^i 个根并带回到正确位置所花费的时间临时根。

让我们举个例子。假设有一个包含 15 个元素的数组,即树的高度为 h = log2(15) = 3:

  • 在第 i=3 层,我们有 2^3=8 个节点,我们需要将每个节点移动到根位置,然后对每个节点进行堆化。每个 heapify 将在最坏的情况下执行“i”比较,因为根可能下沉到仍然存在的级别“i”。 T(n, 3) = 2^3 * 3 = 8*3
  • 在第 i=2 层,我们有 2^2=4 个节点,我们对每个节点进行 2 次比较:T(n, 2) = 2^2*2 = 4 * 2
  • 在第 i=1 层,我们有 2^1=2 个节点,我们对每个节点进行 1 次比较:T(n, 1) = 2^1*1 = 2 * 1
  • 在第 i=0 层,我们有 2^0=1 个节点,即根节点,我们进行 0 次比较:T(n, 0) = 0

好的,一般来说:

T(n) = sum(i=0 to h) 2^i * i

但是如果你还记得 h = log2(n),我们有

T(n) = sum(i=0 to log2(n)) 2^i * i =~ 2nlogn

堆构建 VS 堆排序

直观地,您可以看到 heapsort 无法“摊销”他的成本,因为每次我们增加节点数量时,我们必须做更多的比较,而我们在堆构建功能中正好相反!你可以在这里看到:

  • 堆构建:T(n, i) ~ 2^i * (h-i),如果 i 增加,#nodes 增加,但 #compares 减少
  • 堆排序:T(n, i) ~ 2^i * i,如果 i 增加,#nodes 增加,#compares 增加

所以:

  • 级别 i=3,#nodes(3)=8,堆构建进行 0 次比较,堆排序进行 8*3 = 24 次比较
  • 级别 i=2,#nodes(2)=4,堆构建进行 4 次比较,堆排序进行 4*2 = 8 次比较
  • 级别 i=1,#nodes(1)=2,堆构建进行 4 次比较,堆排序进行 2*1 = 2 次比较
  • 级别 i=0,#nodes(0)=1,堆构建进行 3 次比较,堆排序进行 1*0 = 1 次比较