📜  排序算法的渐近分析与比较

📅  最后修改于: 2021-10-19 04:53:55             🧑  作者: Mango

归并排序比插入排序运行得更快,这是一个公认的事实。使用渐近分析,我们可以证明归并排序的运行时间为 O(nlogn),而插入排序的运行时间为 O(n^2)。这是显而易见的,因为归并排序使用分而治之的方法,通过递归解决插入排序遵循增量方法的问题。
如果我们进一步仔细检查时间复杂度分析,我们就会知道插入排序还不够糟糕。令人惊讶的是,插入排序在较小的输入大小上胜过归并排序。这是因为在推导时间复杂度时我们忽略的常量很少。在 10^4 阶的较大输入大小上,这不会影响我们函数的行为。但是,当输入大小低于 40 时,例如小于 40,则等式中的常量将支配输入大小“n”。
到现在为止还挺好。但我对这样的数学分析并不满意。作为一名计算机科学本科生,我们必须相信编写代码。我编写了一个 C 程序来了解算法如何在各种输入大小上相互竞争。而且,为什么要对建立这些排序算法的运行时间复杂性进行如此严格的数学分析。

执行:

//C++ code to compare performance of sorting algorithms
#include 
#include 
#include 
#include 
#define MAX_ELEMENT_IN_ARRAY 1000000001
  
int cmpfunc (const void * a, const void * b)
{
    // Compare function used by qsort
    return ( *(int*)a - *(int*)b );
}
  
int* generate_random_array(int n)
{
    srand(time(NULL));
    int *a = malloc(sizeof(int) * n), i;
    for(i = 0; i < n; ++i)
        a[i] = rand() % MAX_ELEMENT_IN_ARRAY;
    return a;
}
  
int* copy_array(int a[], int n)
{
    int *arr = malloc(sizeof(int) * n);
    int i;
    for(i = 0; i < n ;++i)
        arr[i] = a[i];
    return arr;
}
  
//Code for Insertion Sort
void insertion_sort_asc(int a[], int start, int end)
{
    int i;
    for(i = start + 1; i <= end ; ++i)
    {
        int key = a[i];
        int j = i - 1;
        while(j >= start && a[j] > key)
        {
            a[j + 1] = a[j];
            --j;
        }
        a[j + 1] = key;
    }
}
  
//Code for Merge Sort
void merge(int a[], int start, int end, int mid)
{
    int i = start, j = mid + 1, k = 0;
    int *aux = malloc(sizeof(int) * (end - start + 1));
    while(i <= mid && j <= end)
    {
        if(a[i] <= a[j])
            aux[k++] = a[i++];
        else
            aux[k++] = a[j++];
    }
    while(i <= mid)
        aux[k++] = a[i++];
    while(j <= end)
        aux[k++] = a[j++];
    j = 0;
    for(i = start;i <= end;++i)
        a[i] = aux[j++];
    free(aux);
}
  
void _merge_sort(int a[],int start,int end)
{
    if(start < end)
    {
        int mid = start + (end - start) / 2;
        _merge_sort(a,start,mid);
        _merge_sort(a,mid + 1,end);
        merge(a,start,end,mid);
    }
}
void merge_sort(int a[],int n)
{
    return _merge_sort(a,0,n - 1);
}
  
  
void insertion_and_merge_sort_combine(int a[], int start, int end, int k)
{
    // Performs insertion sort if size of array is less than or equal to k
    // Otherwise, uses mergesort
    if(start < end)
    {
        int size = end - start + 1;
          
        if(size <= k)
        {
            //printf("Performed insertion sort- start = %d and end = %d\n", start, end);
            return insertion_sort_asc(a,start,end);
        }
        int mid = start + (end - start) / 2;
        insertion_and_merge_sort_combine(a,start,mid,k);
        insertion_and_merge_sort_combine(a,mid + 1,end,k);
        merge(a,start,end,mid);
    }
}
  
void test_sorting_runtimes(int size,int num_of_times)
{
    // Measuring the runtime of the sorting algorithms
    int number_of_times = num_of_times;
    int t = number_of_times;
    int n = size;
    double insertion_sort_time = 0, merge_sort_time = 0;
    double merge_sort_and_insertion_sort_mix_time = 0, qsort_time = 0;
    while(t--)
    {
        clock_t start, end;
          
        int *a = generate_random_array(n);
        int *b = copy_array(a,n);
        start = clock();
        insertion_sort_asc(b,0,n-1);
        end = clock();
        insertion_sort_time += ((double) (end - start)) / CLOCKS_PER_SEC;
        free(b);
        int *c = copy_array(a,n);
        start = clock();
        merge_sort(c,n);
        end = clock();
        merge_sort_time += ((double) (end - start)) / CLOCKS_PER_SEC;
        free(c);
        int *d = copy_array(a,n);
        start = clock();
        insertion_and_merge_sort_combine(d,0,n-1,40);
        end = clock();
        merge_sort_and_insertion_sort_mix_time+=((double) (end - start))/CLOCKS_PER_SEC;
        free(d);
        start = clock();
        qsort(a,n,sizeof(int),cmpfunc);
        end = clock();
        qsort_time += ((double) (end - start)) / CLOCKS_PER_SEC;
        free(a);
    }
      
    insertion_sort_time /= number_of_times;
    merge_sort_time /= number_of_times;
    merge_sort_and_insertion_sort_mix_time /= number_of_times;
    qsort_time /= number_of_times;
    printf("\nTime taken to sort:\n"
            "%-35s %f\n"
            "%-35s %f\n"
            "%-35s %f\n"
            "%-35s %f\n\n",
            "(i)Insertion sort: ",
            insertion_sort_time,
            "(ii)Merge sort: ",
            merge_sort_time,
            "(iii)Insertion-mergesort-hybrid: ",
            merge_sort_and_insertion_sort_mix_time,
            "(iv)Qsort library function: ",
            qsort_time);
}
  
int main(int argc, char const *argv[])
{
    int t;
    scanf("%d", &t);
    while(t--)
    {
        int size, num_of_times;
        scanf("%d %d", &size, &num_of_times);
        test_sorting_runtimes(size,num_of_times);
    }
    return 0;
}

我比较了以下算法的运行时间:

  • 插入排序:没有修改/优化的传统算法。对于较小的输入尺寸,它表现得非常好。是的,它确实击败了合并排序
  • 归并排序:遵循分而治之的方法。对于 10^5 数量级的输入大小,此算法是正确的选择。对于如此大的输入大小,它使插入排序变得不切实际。
  • 插入排序和归并排序的组合版本:我稍微调整了归并排序的逻辑,以在较小的输入大小下获得更好的运行时间。正如我们所知,归并排序将它的输入分成两半,直到它足够琐碎来对元素进行排序。但是在这里,当输入大小低于阈值(例如 ‘n’ < 40)时,这种混合算法就会调用传统的插入排序过程。由于插入排序在较小的输入上运行得更快,而合并排序在较大的输入上运行得更快,因此该算法可以最好地利用这两个世界。
  • 快速排序:我还没有实现这个程序。这是库函数qsort() 可在.我考虑过这个算法是为了了解实现的重要性。它需要大量的编程专业知识来最小化步骤数量并最多使用底层语言原语以尽可能最好的方式实现算法。这是推荐使用库函数的主要原因。它们被编写来处理任何事情。他们尽可能地优化。在我忘记之前,根据我的分析,qsort() 在几乎任何输入大小上都运行得非常快!

分析:

  • 输入:用户必须提供他/她想要测试与测试用例数量相对应的算法的次数。对于每个测试用例,用户必须输入两个空格分隔的整数,表示输入大小“n”和“num_of_times”,表示他/她想要运行分析并取平均值的次数。 (说明:如果 ‘num_of_times’ 为 10,则上面指定的每个算法运行 10 次并取平均值。这样做是因为输入数组是根据您指定的输入大小随机生成的。输入数组可以是所有排序。我们的它可以对应最坏的情况。即降序。为了避免这种输入数组的运行时间。算法运行’num_of_times’并取平均值。)
    clock() 例程和 CLOCKS_PER_SEC 宏来自用于测量所花费的时间。
    编译:我已经在Linux环境(Ubuntu 16.04 LTS)中编写了上述代码。复制上面的代码片段。使用 gcc 编译它,键入指定的输入并欣赏排序算法的力量!
  • 结果:正如您所看到的,对于小输入大小,插入排序比合并排序快 2 * 10^-6 秒。但这种时间上的差异并不那么显着。另一方面,混合算法和 qsort() 库函数都与插入排序一样好。
    Algos_0 的渐近分析
    输入大小现在增加了大约 100 倍,从 n = 30 增加到 n = 1000。现在差异是有形的。合并排序比插入排序快 10 倍。混合算法的性能与 qsort() 例程之间再次存在联系。这表明 qsort() 的实现方式与我们的混合算法或多或少相似,即在不同算法之间切换以充分利用它们。
    Algos_1 的渐近分析
    最后,输入大小增加到 10^5(10 万!),这很可能是实际场景中使用的理想大小。与之前的输入 n = 1000 相比,合并排序比插入排序快 10 倍,这里的差异更加显着。合并排序比插入排序高 100 倍!
    实际上,我们编写的混合算法通过快 0.01 秒的运行速度来执行传统的归并排序。最后,库函数qsort() 最终向我们证明了在通过快 3 毫秒的速度精心测量运行时间的同时,实现也起着至关重要的作用! 😀

Algos_2 的渐近分析
注意:不要在 n >= 10^6 的情况下运行上述程序,因为它会占用大量的计算能力。谢谢你,快乐编码! 🙂