让我“以数学方式”向您展示我们如何计算将任意数组转换为堆(让我称之为“堆构建”)然后使用堆排序对其进行排序的复杂性。
堆构建时间分析
为了将数组转换为堆,我们必须查看每个具有子节点的节点并“堆”(接收)该节点。您应该问问自己我们进行了多少次比较;如果你仔细想想,你会看到(h = 树高):
- 对于第 i 层的每个节点,我们进行 hi 比较:#comparesOneNode(i) = hi
- 在第 i 层,我们有 2^i 个节点:#nodes(i) = 2^i
- 因此,通常 T(n,i) = #nodes(i) * #comparesOneNode(i) = 2^i *(hi),是在“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
好的,一般来说:
T(n) = sum(i=0 to h) 2^i * (hi)
但如果你记得 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 堆排序
直观地,您可以看到堆排序无法“摊销”他的成本,因为每次我们增加节点数量时,我们必须做更多的比较,而堆构建功能恰恰相反!你可以在这里看到:
- 堆构建:T(n, i) ~ 2^i * (hi),如果 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 次比较