【问题标题】:Why is insertion sort Θ(n^2) in the average case?为什么在平均情况下插入排序 Θ(n^2)?
【发布时间】:2013-06-07 23:10:47
【问题描述】:

Insertion sort 的运行时间为 Ω(n)(当输入被排序时)和 O(n2)(当输入被反向排序时)。平均而言,它在 Θ(n2) 时间内运行。

这是为什么?例如,为什么平均情况不接近 O(n log n)?

【问题讨论】:

    标签: algorithm sorting big-o insertion-sort


    【解决方案1】:

    为了好玩,我编写了一个程序,它遍历所有数据组合,对大小为 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 接近无穷大时,平均情况接近最坏情况除以 2。

    【讨论】:

    • 看平均运行时间的增长率。请注意,当输入大小翻倍时,它会增加大约四倍。这意味着它是二次的,因此跟踪最大值比跟踪最小值更接近。我敢打赌,如果你得到更大的 n 值,最小值和平均值之间的差距会大得多。
    • @templatetypedef 我的错误。随着 n 翻倍,最小值和最大值稳定在 2 倍和 4 倍的恒定增加。查看 ave 数据,我得出结论,它将稳定在 3.7 倍附近。
    • 结果为 n^1.888。
    • 您是否计算过较大的 n?我的证明预测二次增长,对于较小的 n,低阶项仍然有很大贡献。
    • 我正在计划何时运行 n=15,这需要 15! (1.31 * 10 ^ 12) 不同的组合乘以每个平均 64-65 次比较。我的 CPU 以 2.67 GHz 或 2.67 * 10 ^ 9 运行,这意味着 31646 * 每次比较的平均时钟周期数。至少,执行时间超过 24 小时。
    【解决方案2】:

    大多数算法的平均情况与最坏情况相同。要了解为什么会这样,我们将 O 称为最坏情况,将 Ω 称为最佳情况。据推测,当 n 趋于无穷大时,O >= Ω。对于大多数分布,平均情况将接近最佳和最坏情况的平均值 - 即 (O + Ω)/2 = O/2 + Ω/2。由于我们不关心系数,并且 O >= Ω,因此这与 O 相同。

    显然,这过于简单化了。有些运行时间分布是倾斜的,因此假设平均情况是最坏情况和最好情况的平均值是不成立的*。但这应该让您对这是为什么会有一个不错的直觉。

    *正如 cmets 中的 templatetypedef 所提到的,一些示例是快速排序/快速选择、BST 查找(除非您平衡树)、哈希表查找和单纯形法。

    【讨论】:

    • 许多重要算法的最坏情况运行时不同于一般情况运行时:快速排序、快速选择、BST 查找、哈希表查找和单纯形法。
    • @templatetypedef 我很确定有一些,但我什么也没想到。感谢您的建议!
    【解决方案3】:

    要回答这个问题,我们首先要确定如何评估插入排序的运行时间。如果我们能为运行时间找到一个很好的数学表达式,我们就可以操纵该表达式来确定平均运行时间。

    我们需要观察的关键是插入排序的运行时间与输入数组中inversions的数量密切相关。数组中的反转是一对元素 A[i] 和 A[j] 的相对顺序错误 - 即 i

    0 1 3 2 4 5
    

    有一个反转:3 和 2 应该交换。在这个数组中:

    4 1 0 3 2
    

    有6个反转:

    • 4 和 1
    • 4 和 0
    • 4 和 3
    • 4 和 2
    • 1 和 0
    • 3 和 2

    反转的一个重要属性是排序数组中没有反转,因为每个元素都应该小于它之后的所有元素,并且大于它之前的所有元素。

    这很重要的原因是插入排序中完成的工作量与原始数组中的反转次数之间存在直接联系。为了看到这一点,让我们回顾一下插入排序的一些快速伪代码:

    • 对于 i = 2 .. n:(假设 1 索引)
      • 设置 j = i - 1。
      • 当 A[j] > A[j + 1] 时:
        • 交换 A[j] 和 A[j + 1]。
        • 设置 j = j - 1。

    通常,在确定此类函数完成的总工作量时,我们可以确定内部循环完成的最大工作量,然后将其乘以外部循环的迭代次数。这将给出一个上限,但不一定是一个严格的界限。说明完成的总工作的更好方法是认识到有两种不同的工作来源:

    • 外循环,计数 2、3、...、n 和
    • 执行交换的内部循环。

    那个外循环总是做 Θ(n) 的工作。然而,内部循环所做的工作量与算法整个运行时进行的交换总数成正比。要查看该循环将完成多少工作,我们需要确定在算法的所有迭代中进行了多少总交换。

    这就是反转出现的地方。请注意,当插入排序运行时,它总是交换数组中的相邻元素,并且只有当它们形成反转时才会交换两个元素。那么在我们执行交换之后,数组中的反转总数会发生什么变化?好吧,从图形上看,我们有这个:

     [---- X ----] A[j] A[j+1] [---- Y ----]
    

    这里,X 是数组中交换对之前的部分,Y 是数组中交换对之后的部分。

    假设我们交换 A[j] 和 A[j+1]。反转次数会发生什么变化?好吧,让我们考虑一下两个元素之间的一些任意反转。有6种可能:

    • 两个元素都在 X 中,或者两个元素都在 Y 中,或者一个元素在 X 中,一个元素在 Y 中。那么反转仍然存在,因为我们没有移动这些元素中的任何一个。
    • 一个元素在 X 或 Y 中,另一个是 A[j] 或 A[j+1]。然后反转仍然存在,因为元素的相对顺序没有改变,即使它们的绝对位置可能已经改变。
    • 一个元素是 A[j],另一个是 A[j+1]。然后在交换后移除反转。

    这意味着在执行交换之后,我们将反转的数量恰好减少 1,因为只有相邻对的反转消失了。由于以下原因,这非常重要:如果我们从 I 反转开始,每次交换都会将数字恰好减少 1。一旦没有反转,就不再执行交换。因此,交换次数等于反转次数

    鉴于此,我们可以准确地将插入排序的运行时间表示为 Θ(n + I),其中 I 是原始数组的反转次数。这符合我们最初的运行时界限——在排序数组中,有 0 次反转,运行时是 Θ(n + 0) = Θ(n),而在反向排序数组中,有 n(n - 1)/ 2 次反转,运行时间为 Θ(n + n(n-1)/2) = Θ(n2)。漂亮!

    所以现在我们有了一种超级精确的方法来分析给定特定数组的插入排序的运行时间。让我们看看如何分析它的平均运行时间。为此,我们需要对输入的分布做出假设。由于插入排序是一种基于比较的排序算法,输入数组的实际值实际上并不重要;只有它们的相对顺序才是真正重要的。在下文中,我将假设所有数组元素都是不同的,但如果不是这种情况,分析不会有太大变化。当我们到达那里时,我会指出事情偏离脚本的地方。

    为了解决这个问题,我们将引入一堆 Xij 形式的指标变量,其中 Xij 是一个随机变量,如果为 1 A[i] 和 A[j] 形成反转,否则为 0。这些变量中有 n(n - 1)/2 个,每个不同的元素对都有一个。请注意,这些变量说明了数组中每个可能的反转。

    给定这些 X,我们可以定义一个新的随机变量 I,它等于数组中反转的总数。这将由 X 的总和给出:

    I = Σ Xij

    我们对 E[I] 感兴趣,即数组中的预期反转次数。使用线性期望,这是

    E[I] = E[Σ Xij] = Σ E[Xij]

    所以现在如果我们可以得到 E[Xij] 的值,我们就可以确定预期的反转次数,从而确定预期的运行时间!

    幸运的是,由于所有 Xij 都是二进制指示变量,我们有这个

    E[Xij] = Pr[Xij = 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[Xij] = Σ (1 / 2)

    由于总和中有 n(n - 1)/2 项,因此结果为

    E[I] = n(n - 1) / 4 = Θ(n2)

    因此,在预期中,会有 Θ(n2) 反转,所以预期运行时间将是 Θ(n2 + n) = Θ(n2)。这就解释了为什么插入排序的平均情况是 Θ(n2)。

    希望这会有所帮助!

    【讨论】:

    • 我可能是错的(希望我是错的),但这对我来说更像是冒泡排序而不是插入排序......我认为插入排序找到了正确的位置当前项目并将列表的其余部分向下移动,而不是像那样交换元素......不过,分析是相当相关的,因为冒泡排序和插入排序在性能方面非常相似。
    • @twalberg- 你在考虑选择排序吗?我一直看到插入排序是这样定义的。
    • 可能......我可能只是读错了伪代码......或者我可能正在考虑一个异地插入排序而不是就地......我猜就地插入排序会合理地使用交换来移动事物。
    • @twalberg:您可以在交换值期间找到值的正确位置,或者您可以将交换保存到实际插入之前,然后移动一堆值以提供最终位置。虽然第二种形式更快,但它仍然遵循 n^2 次比较 - 至少在最坏的情况下。
    • @phougatv 是 (n-1) + (n-2) + ... + 3 + 2 + 1。第一个数组元素与所有其他元素配对形成反转。第二个数组元素与它之后的所有元素配对以形成反转等。
    猜你喜欢
    • 2011-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-30
    • 1970-01-01
    • 1970-01-01
    • 2020-06-20
    • 1970-01-01
    相关资源
    最近更新 更多