【问题标题】:Exactly how many comparisons does merge sort make?归并排序到底做了多少比较?
【发布时间】:2012-01-22 01:27:42
【问题描述】:

我读到过,在实践中,快速排序比归并排序快得多,原因是隐藏常量。

好吧,随机快速排序复杂度的解是 2nlnn=1.39nlogn,这意味着快速排序中的常数是 1.39。

但是归并排序呢?归并排序中的常数是什么?

【问题讨论】:

  • 这个问题没有更多细节就没有答案。答案取决于(1)您对复杂性的定义:操作数?比较次数? (2) 不同机器的答案可能不同,具体取决于每台机器的指令集。
  • 当然是比较次数。

标签: algorithm sorting complexity-theory quicksort mergesort


【解决方案1】:

让我们看看能不能解决这个问题!

在归并排序中,在递归的每一级,我们执行以下操作:

  1. 将数组分成两半。
  2. 对每一半进行递归排序。
  3. 使用合并算法将两部分合并在一起。

那么每个步骤进行多少次比较?好吧,除法步骤没有进行任何比较;它只是将数组分成两半。步骤 2 不(直接)进行任何比较;所有比较都是通过递归调用完成的。在第 3 步中,我们有两个大小为 n/2 的数组,需要合并它们。这需要最多 n 次比较,因为合并算法的每一步都会进行一次比较,然后消耗一些数组元素,所以我们最多只能进行 n 次比较。

结合起来,我们得到以下递归:

C(1) = 0
C(n) = 2C(n / 2) + n

(如 cmets 中所述,线性项更准确地说是 (n - 1),但这不会改变总体结论。我们将使用上述递归作为上限。)

为了简化这一点,让我们定义 n = 2k 并用 k 重写这个递归:

C'(0) = 0
C'(k) = 2C'(k - 1) + 2^k

这里的前几个术语是 0, 2, 8, 24, ... 。这看起来像 k 2k,我们可以通过归纳来证明这一点。作为我们的基本情况,当 k = 0 时,第一项为 0,并且 k 2k 的值也为 0。对于归纳步​​骤,假设声明对于某些 k 成立并考虑 k + 1. 那么值为 2(k 2k) + 2k + 1 = k 2 k + 1 + 2k + 1 = (k + 1)2k + 1,所以对于 k + 1 成立,完成归纳。因此C'(k)的值为k 2k。由于 n = 2 k,这意味着,假设 n 是 2 的完美幂,我们有进行比较的次数是

C(n) = n lg n

令人印象深刻的是,这比快速排序更好!那么为什么快速排序比归并排序更快呢?这与其他与比较次数无关的因素有关。首先,由于快速排序在适当的位置工作,而合并排序在不适当的位置工作,因此在合并排序中的引用局部性几乎不如在快速排序中那样好。这是一个如此巨大的因素,以至于在实践中快速排序最终比合并排序好得多,因为缓存未命中的成本非常高。此外,对数组进行排序所需的时间并不仅仅考虑比较的次数。其他因素,例如每个数组元素移动的次数也很重要。例如,在合并排序中,我们需要为缓冲的元素分配空间,移动元素以便它们可以合并,然后合并回数组中。我们的分析中没有计算这些动作,但它们肯定会加起来。将此与快速排序的分区步骤进行比较,后者将每个数组元素仅移动一次并保留在原始数组中。这些额外的因素,而不是进行的比较次数,决定了算法的运行时间。

这个分析比最优分析稍微不精确,但Wikipedia 确认分析大约是 n lg n,这确实比快速排序的平均情况要少。

希望这会有所帮助!

【讨论】:

  • 非常感谢!是否有任何分析将空间分配考虑在内?
  • 公式不应该是 C(1) = 0 C(n) = 2C(n / 2) + n-1。因为如果我们有 2 个大小为 n/2 的数组,我们最多需要 n-1 个比较来将它们合并到一个大小为 n 的数组中?
  • @Johnson 是的!这是一个很好的观点。这最终将使整体分析减少 2n - 1(每个递归调用一个),我相信这不会改变结论。谢谢你的运动!
  • 合并比较的次数不应该是(n-1)吗?
【解决方案2】:

在最坏的情况下并假设一个直接的实现,对 n 个元素进行排序的比较次数是

n ⌈lg n⌉ − 2⌈lg n + 1

其中lg n表示nbase-2 logarithm

这个结果可以在 the corresponding Wikipedia article 或 Donald Knuth 的 The Art of Computer Programming 的最新版本中找到,我刚刚为 this answer 写了一个证明。

【讨论】:

  • 对快速排序有什么想法吗?
【解决方案3】:

合并两个大小为k 的排序数组(或列表)。 m 最多进行 k+m-1 比较,min{k,m} 最多。 (每次比较后,我们可以向目标写入一个值,当两者中的一个用完时,就不需要再进行比较了。)

C(n) 成为n 元素的数组(列表)的合并排序的最坏情况比较次数。

然后我们有C(1) = 0C(2) = 1,很明显。此外,我们有递归

C(n) = C(floor(n/2)) + C(ceiling(n/2)) + (n-1)

简单的归纳展示

C(n) <= n*log_2 n

另一方面,很容易看出我们可以任意接近边界(对于每个ε &gt; 0,我们可以构造需要超过(1-ε)*n*log_2 n 比较的情况),所以归并排序的常数是1。

【讨论】:

  • 那么这意味着我的快速排序的1.39常量不正确。
  • @geniaz1- 您的快速排序常量确实是正确的,但由于其他原因,快速排序更快。详情请看我的帖子。
【解决方案4】:

合并排序是 O(n log n) 并且在每个步骤中,在“最坏”情况下(对于比较次数)执行比较。

另一方面,快速排序在最坏的情况下是 O(n^2)。

【讨论】:

    【解决方案5】:

    在归并排序中计算比较次数的 C++ 程序。 首先程序会对给定的数组进行排序,然后会显示比较的次数。

     #include<iostream>
     using namespace std;
     int  count=0; /* to count the number of comparisions */
    
     int merge( int arr [ ], int l, int m, int r)
    {
     int i=l; /* left subarray*/
     int j=m+1; /* right  subarray*/
     int k=l; /* temporary array*/
     int temp[r+1];
     while( i<=m && j<=r)
     {
       if ( arr[i]<= arr[j])
      {
        temp[k]=arr[i];
        i++;
      }
       else
      {
        temp[k]=arr[j];
        j++;
      }
        k++;
        count++;
    
      }
       while( i<=m)
      {
        temp[k]=arr[i];
        i++;
        k++;
      }
        while( j<=r)
      {
        temp[k]=arr[j];
        j++;
        k++;
      }
      for( int p=l; p<=r; p++)
      {
        arr[p]=temp[p];
      }
       return count;
      }
    
    
      int  mergesort( int arr[ ], int l, int r)
      {
        int comparisons;
        if(l<r)
      {
       int m= ( l+r)/2;
       mergesort(arr,l,m);
       mergesort(arr,m+1,r);
       comparisions = merge(arr,l,m,r);
      }
       return comparisons;
      }
    
     int main ()
     {
       int size;
       cout<<" Enter the size of an array "<< endl;
       cin>>size;
       int myarr[size];
       cout<<"  Enter the elements of array "<<endl;
       for ( int i=0; i< size; i++)
     {
       cin>>myarr[i];
     }
     cout<<"  Elements of array before sorting are  "<<endl;
     for ( int i=0; i< size; i++)
     {
       cout<<myarr[i]<<"  " ;
     }
      cout<<endl;
      int c=mergesort(myarr, 0, size-1);
      cout<<"  Elements of array after sorting are  "<<endl;
      for ( int i=0; i< size; i++)
     {
       cout<<myarr[i]<<"  " ;
     }
       cout<<endl;
       cout<<"  Number of comaprisions while sorting the given array"<< c <<endl;
       return 0;
     }
    

    【讨论】:

      【解决方案6】:

      我假设读者知道合并排序。只有当两个排序数组合并时才会发生比较。为简单起见,假设 n 为 2 的幂。要在最坏的情况下合并两个 (n/2) 大小的数组,我们需要 (n - 1) 次比较。 -1 出现在这里,因为合并时剩下的最后一个元素不需要任何比较。首次找到总比较数,假设一段时间为n,我们可以通过(-1)部分对其进行更正。合并的级别数为 log2(n)(想象为树结构)。在每一层中都会有 n 个比较(需要减去一些数字,因为 -1 部分),所以总比较是 nlog2(n) - (尚未找到)。 “尚未找到”部分没有给出 nlog2(n) 常数,它实际上是 (1 + 2 + 4 + 8 + ... + (n/2) = n - 1)。 归并排序中的总比较次数 = n*log2(n) - (n - 1)。 所以,你的常数是 1。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-06-01
        • 2010-12-03
        • 2016-10-02
        • 2021-11-03
        • 1970-01-01
        • 1970-01-01
        • 2021-03-28
        • 1970-01-01
        相关资源
        最近更新 更多