插入排序的运行时间为 Ω(n)(当输入被排序时)和 O(n 2 )(当输入被反向排序时)。平均而言,它在 Θ(n 2 ) 时间内运行。
为什么是这样?例如,为什么平均情况不接近 O(n log n)?
插入排序的运行时间为 Ω(n)(当输入被排序时)和 O(n 2 )(当输入被反向排序时)。平均而言,它在 Θ(n 2 ) 时间内运行。
为什么是这样?例如,为什么平均情况不接近 O(n log n)?
要回答这个问题,我们首先要确定如何评估插入排序的运行时间。如果我们能为运行时间找到一个很好的数学表达式,那么我们就可以操纵该表达式来确定平均运行时间。
我们需要的关键观察是插入排序的运行时间与输入数组中的反转次数密切相关。数组中的反转是一对元素 A[i] 和 A[j] 的相对顺序错误 - 即 i < j,但 A[j] < A[i]。例如,在这个数组中:
0 1 3 2 4 5
有一个反转:3 和 2 应该交换。在这个数组中:
4 1 0 3 2
有6个反转:
反转的一个重要属性是排序数组中没有反转,因为每个元素都应该小于它之后的所有元素,并且大于它之前的所有元素。
这很重要的原因是插入排序中完成的工作量与原始数组中的反转次数之间存在直接联系。为了看到这一点,让我们回顾一下插入排序的一些快速伪代码:
通常,在确定这样的函数完成的总工作量时,我们可以确定内部循环完成的最大工作量,然后将其乘以外部循环的迭代次数。这将给出一个上限,但不一定是一个严格的界限。解释完成的总工作的更好方法是认识到有两种不同的工作来源:
那个外循环总是做 Θ(n) 的工作。然而,内部循环所做的工作量与算法整个运行时进行的交换总数成正比。要查看该循环将完成多少工作,我们需要确定在算法的所有迭代中进行了多少总交换。
这就是反转出现的地方。请注意,当插入排序运行时,它总是交换数组中的相邻元素,并且只有当它们形成反转时才会交换两个元素。那么在我们执行交换之后,数组中的反转总数会发生什么变化?好吧,从图形上看,我们有这个:
[---- X ----] A[j] A[j+1] [---- Y ----]
这里,X 是在交换对之前的数组部分,Y 是在交换对之后的数组部分。
假设我们交换 A[j] 和 A[j+1]。反转次数会发生什么变化?好吧,让我们考虑一下两个元素之间的一些任意反转。有6种可能:
这意味着在执行交换之后,我们将反转的数量减少了 1,因为只有相邻对的反转消失了。由于以下原因,这非常重要:如果我们从 I 反转开始,每次交换都会将数字恰好减少 1。一旦没有反转,就不再执行交换。因此,交换次数等于反转次数!
鉴于此,我们可以准确地将插入排序的运行时间表示为 Θ(n + I),其中 I 是原始数组的反转次数。这符合我们最初的运行时界限——在排序数组中,有 0 次反转,运行时间是 Θ(n + 0) = Θ(n),而在反向排序数组中,有 n(n - 1)/ 2 次反转,运行时间为 Θ(n + n(n-1)/2) = Θ(n 2 )。漂亮!
所以现在我们有了一种超级精确的方法来分析给定特定数组的插入排序的运行时间。让我们看看如何分析它的平均运行时间。为此,我们需要对输入的分布做出假设。由于插入排序是一种基于比较的排序算法,输入数组的实际值实际上并不重要;只有它们的相对顺序才是真正重要的。在下文中,我将假设所有数组元素都是不同的,但如果不是这种情况,分析不会有太大变化。当我们到达那里时,我会指出事情偏离脚本的地方。
为了解决这个问题,我们将引入一堆 X ij形式的指示变量,其中 X ij是一个随机变量,如果 A[i] 和 A[j] 形成反转则为 1,否则为 0。这些变量中有 n(n - 1)/2 个,每个不同的元素对都有一个。请注意,这些变量说明了数组中每个可能的反转。
给定这些 X,我们可以定义一个新的随机变量 I,它等于数组中反转的总数。这将由 X 的总和给出:
I = Σ X ij
我们对 E[I] 感兴趣,它是数组中预期的反转次数。使用期望的线性,这是
E[I] = E[Σ X ij ] = Σ E[X ij ]
所以现在如果我们能得到 E[X ij ] 的值,我们就可以确定预期的反转次数,从而确定预期的运行时间!
幸运的是,由于所有 X ij都是二进制指示变量,我们有
E[X ij ] = Pr[X ij = 1] = Pr[A[i] 和 A[j] 是反转]
那么,给定一个没有重复的随机输入数组,A[i] 和 A[j] 是反转的概率是多少?好吧,有一半的时间,A[i] 将小于 A[j],而另一半的时间 A[i] 将大于 A[j]。(如果允许重复,则有一个偷偷摸摸的额外术语来处理重复,但我们现在将忽略它)。因此,A[i] 和 A[j] 之间发生反转的概率是 1 / 2。因此:
E[I] = ΣE[X ij ] = Σ (1 / 2)
由于总和中有 n(n - 1)/2 项,因此可以得出
E[I] = n(n - 1) / 4 = Θ(n 2 )
因此,根据预期,会有 Θ(n 2 ) 反转,因此预期运行时间将是 Θ(n 2 + n) = Θ(n 2 )。这解释了为什么插入排序的平均情况行为是 Θ(n 2 )。
希望这可以帮助!
为了好玩,我编写了一个程序,它遍历所有数据组合,对一个大小为 n 的向量进行计数比较,发现最好的情况是 n-1(全部排序),最坏的情况是 (n*(n-1))/2。
不同 n 的一些结果:
n min ave max ave/(min+max) ave/max
2 1 1 1 0.5000
3 2 2.667 3 0.5334
4 3 4.917 6 0.5463
5 4 7.717 10 0.5512
6 5 11.050 15 0.5525
7 6 14.907 21 0.5521
8 7 19.282 28 0.5509
9 8 24.171 36 0.5493
10 9 29.571 45 0.5476
11 10 35.480 55 0.5458
12 11 41.897 66 0.5441
似乎平均值比最大值更接近最小值。
编辑:一些附加值
13 12 48.820 78 0.5424
14 13 56.248 91 0.5408
编辑:价值 15
15 14 64.182 105 0.5393
编辑:选择更高的值
16 15 72.619 120 - 0.6052
32 31 275.942 496 - 0.5563
64 63 1034.772 1953 - 0.5294
128 127 4186.567 8128 - 0.5151
256 255 16569.876 32640 - 0.5077
我最近编写了一个程序来计算对于更高的 n 值的插入排序的平均比较次数。从这些我得出的结论是,当 n 接近无穷大时,平均情况接近最坏情况除以二。
大多数算法的平均情况与最坏情况相同。要了解为什么会这样,我们将 O 称为最坏情况,将 Ω 称为最佳情况。据推测,当 n 趋于无穷时,O >= Ω。对于大多数分布,平均情况将接近最佳和最坏情况的平均值 - 即 (O + Ω)/2 = O/2 + Ω/2。由于我们不关心系数,并且 O >= Ω,这与 O 相同。
显然,这是过于简单化了。有些运行时间分布是倾斜的,因此假设平均情况是最坏情况和最好情况的平均值是不成立的*。但这应该让您对为什么会这样有一个不错的直觉。
*正如 templatetypedef 在评论中提到的,一些示例是快速排序/快速选择、BST 查找(除非您平衡树)、哈希表查找和单纯形法。