合并排序无法在N logN中工作

Merge sort failing to work in N logN

本文关键字:logN 工作 排序 合并      更新时间:2023-10-16

我写了一个合并排序算法,但它在N log N中排序失败。对数组列表进行排序的代码是:

void merge(int start, int mid, int end) {
int i,j,q;
for (i=start; i<=end; i++) 
l[i]=list[i]; 
i=start; j=mid+1; q=start; 
while (i<=mid and j<=end) { 
if (l[i]<l[j]) 
list[q++]=l[i++];
else
list[q++]=l[j++];
}
while (i<=mid) 
list[q++]=l[i++];
}

void mergesort(int start, int end) {
int mid;
if (start<end) {
mid=(start+end)/2;
mergesort(start, mid);   
mergesort(mid+1, end);  
merge(start, mid, end); 
}
}

然而,例如,如果我对7800个数字进行排序,运行时间大约为1.243毫秒。同一个样本按std::sort排序,时间为0.668毫秒,我知道排序的复杂度为N logN。我的代码出了什么问题?我似乎不觉得浪费时间。

时间测量:

#include <time.h>
clock_t start = clock();
//SORTING ALGORITHM HERE//
clock_t stop = clock();
double elapsed =(stop - start) * 1000.0 / CLOCKS_PER_SEC;

假设您的实现是正确的,两个O(N logN)不一定会在相同的时间内运行。无症状复杂性是衡量运行程序所需的资源随着输入量的增加而增加的程度。举个例子,下面的循环都是O(1),因为每个循环总是运行恒定数量的步骤:

for (i = 0; i < 10; i++) {
printf("%dn", i);
}
for (i = 0; i < 1000000000; i++) {
printf("%dn", i);
}

但毫无疑问,第二场比赛需要更长的时间。事实上,这两个循环之间的运行时差距将明显大于您观察到的排序算法与std::sort之间的差距。这是因为渐近分析忽略了常数。

此外,无症状复杂性通常适用于平均或最坏情况。对于大小相等的输入,相同的算法可以在或多或少的时间内运行,这取决于数据。

更不用说std::sort很可能不是单个排序算法。根据阵列的大小,它可能使用不同的策略。事实上,std::sort的一些实现使用混合算法。

分析程序复杂性的正确方法是阅读代码。对于数值方法,最接近的方法是运行您的程序,而不将其与其他程序进行比较,以获得不同大小的几个输入。画一张图,观察曲线。

在Visual Studio中,std::sort()混合了快速排序、堆排序(只是为了防止最坏情况下的O(n^2)时间复杂性)和插入排序,而std::stable_sort()则混合了合并排序和插入排序。两者都相当快,但可以编写更快的代码。问题中的示例代码是在每次合并之前复制数据,这会消耗时间。这可以通过一次性分配工作缓冲区,并根据递归级别切换合并方向,使用一对相互递归的函数(如下所示)或布尔参数来控制合并方向(以下示例中未使用)来避免。

经过合理优化的自上而下合并排序的示例C++代码(自下而上合并排序会稍微快一点,因为它跳过了用于生成索引的递归,而是使用迭代)。

// prototypes
void TopDownSplitMergeAtoA(int a[], int b[], size_t ll, size_t ee);
void TopDownSplitMergeAtoB(int a[], int b[], size_t ll, size_t ee);
void TopDownMerge(int a[], int b[], size_t ll, size_t rr, size_t ee);
void MergeSort(int a[], size_t n)       // entry function
{
if(n < 2)                           // if size < 2 return
return;
int *b = new int[n];
TopDownSplitMergeAtoA(a, b, 0, n);
delete[] b;
}
void TopDownSplitMergeAtoA(int a[], int b[], size_t ll, size_t ee)
{
if((ee - ll) == 1)                  // if size == 1 return
return;
size_t rr = (ll + ee)>>1;           // midpoint, start of right half
TopDownSplitMergeAtoB(a, b, ll, rr);
TopDownSplitMergeAtoB(a, b, rr, ee);
TopDownMerge(b, a, ll, rr, ee);     // merge b to a
}
void TopDownSplitMergeAtoB(int a[], int b[], size_t ll, size_t ee)
{
if((ee - ll) == 1){                 // if size == 1 copy a to b
b[ll] = a[ll];
return;
}
size_t rr = (ll + ee)>>1;           // midpoint, start of right half
TopDownSplitMergeAtoA(a, b, ll, rr);
TopDownSplitMergeAtoA(a, b, rr, ee);
TopDownMerge(a, b, ll, rr, ee);     // merge a to b
}
void TopDownMerge(int a[], int b[], size_t ll, size_t rr, size_t ee)
{
size_t o = ll;                      // b[]       index
size_t l = ll;                      // a[] left  index
size_t r = rr;                      // a[] right index
while(1){                           // merge data
if(a[l] <= a[r]){               // if a[l] <= a[r]
b[o++] = a[l++];            //   copy a[l]
if(l < rr)                  //   if not end of left run
continue;               //     continue (back to while)
while(r < ee)               //   else copy rest of right run
b[o++] = a[r++];
break;                      //     and return
} else {                        // else a[l] > a[r]
b[o++] = a[r++];            //   copy a[r]
if(r < ee)                  //   if not end of right run
continue;               //     continue (back to while)
while(l < rr)               //   else copy rest of left run
b[o++] = a[l++];
break;                      //     and return
}
}
}