为啥 C 快速排序函数(磁带比较、磁带交换)比冒泡排序函数慢得多?

Posted

技术标签:

【中文标题】为啥 C 快速排序函数(磁带比较、磁带交换)比冒泡排序函数慢得多?【英文标题】:Why is C quicksort function much slower (tape comparisons, tape swapping) than bubble sort function?为什么 C 快速排序函数(磁带比较、磁带交换)比冒泡排序函数慢得多? 【发布时间】:2011-06-22 13:48:02 【问题描述】:

我将为学生实现一个玩具磁带“大型机”,展示“快速排序”类函数的快速性(递归与否,并不重要,因为硬件速度慢,以及众所周知的堆栈反转技术) 与“冒泡排序”函数类相比。所以,虽然我很清楚硬件实现和控制器,但我猜快速排序功能在序列、顺序和比较距离方面比其他功能快得多(从中间倒带比从非常最后,由于不同的倒带速度)。

不幸的是,这不是真的;与“快速排序”函数相比,这个简单的“冒泡”代码在比较距离、方向以及比较和写入的数量方面有了很大的改进。

所以我有3个问题:

    我在实现快速排序功能时是否有错误? 我在实现 bubblesoft 函数时是否有错误? 如果不是,为什么“冒泡排序”函数(比较和写入操作)比“快速排序”函数快得多?

我已经有了“快速排序”功能:

void quicksort(float *a, long l, long r, const compare_function& compare)

    long i=l, j=r, temp, m=(l+r)/2;
    if (l == r) return;
    if (l == r-1)
    
        if (compare(a, l, r))
        
            swap(a, l, r);
        
        return;
    
    if (l < r-1)
    
        while (1)
        
            i = l;
            j = r;
            while (i < m && !compare(a, i, m)) i++;
            while (m < j && !compare(a, m, j)) j--;
            if (i >= j)
            
                break;
            
            swap(a, i, j);
        
        if (l < m) quicksort(a, l, m, compare);
        if (m < r) quicksort(a, m, r, compare);
        return;
    

我有自己的“冒泡排序”功能实现:

void bubblesort(float *a, long l, long r, const compare_function& compare)

    long i, j, k;
    if (l == r)
    
        return;
    
    if (l == r-1)
    
        if (compare(a, l, r))
        
            swap(a, l, r);
        
        return;
    
    if (l < r-1)
    
        while(l < r)
        
            i = l;
            j = l;
            while (i < r)
            
                i++;
                if (!compare(a, j, i))
                
                    continue;
                
                j = i;
            
            if (l < j)
            
                swap(a, l, j);
            
            l++;
            i = r;
            k = r;
            while(l < i)
            
                i--;
                if (!compare(a, i, k))
                
                    continue;
                
                k = i;
            
            if (k < r)
            
                swap(a, k, r);
            
            r--;
        
        return;
    

我在测试示例代码中使用了这些排序函数,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <conio.h>

long swap_count;
long compare_count;

typedef long (*compare_function)(float *, long, long );
typedef void (*sort_function)(float *, long , long , const compare_function& );

void init(float *, long );
void print(float *, long );

void sort(float *, long, const sort_function& );
void swap(float *a, long l, long r);

long less(float *a, long l, long r);
long greater(float *a, long l, long r);

void bubblesort(float *, long , long , const compare_function& );
void quicksort(float *, long , long , const compare_function& );

void main()

    int n;
    printf("n=");

    scanf("%d",&n);
    printf("\r\n");

    long i;
    float *a = (float *)malloc(n*n*sizeof(float));

    sort(a, n, &bubblesort);
    print(a, n);

    sort(a, n, &quicksort);
    print(a, n);

    free(a);


long less(float *a, long l, long r)

    compare_count++;
    return *(a+l) < *(a+r) ? 1 : 0;


long greater(float *a, long l, long r)

    compare_count++;
    return *(a+l) > *(a+r) ? 1 : 0;


void swap(float *a, long l, long r)

    swap_count++;

    float temp;

    temp = *(a+l);
    *(a+l) = *(a+r);
    *(a+r) = temp;


float tg(float x)

    return tan(x);


float ctg(float x)

    return 1.0/tan(x);


void init(float *m,long n)

    long i,j;
    for (i = 0; i < n; i++)
    
        for (j=0; j< n; j++)
        
            m[i + j*n] = tg(0.2*(i+1)) + ctg(0.3*(j+1));
        
    


void print(float *m, long n)

    long i, j;
    for(i = 0; i < n; i++)
    
        for(j = 0; j < n; j++)
        
            printf("  %5.1f", m[i + j*n]);
        
        printf("\r\n");
    
    printf("\r\n");


void sort(float *a, long n, const sort_function& sort)

    long i, sort_compare = 0, sort_swap = 0;

    init(a,n);

    for(i = 0; i < n*n; i+=n)
    
        if (fmod (i / n, 2) == 0)
        
            compare_count = 0;

            swap_count = 0;
            sort(a, i, i+n-1, &less);

            if (swap_count == 0)
            
                compare_count = 0;
                sort(a, i, i+n-1, &greater);
            

            sort_compare += compare_count;
            sort_swap += swap_count;
        
    

    printf("compare=%ld\r\n", sort_compare);
    printf("swap=%ld\r\n", sort_swap);

    printf("\r\n");

【问题讨论】:

注意:所有的复杂性都被赋予了一些“成本”函数。快速排序平均为 O(n log n),其中n 确定比较次数。这不是任意选择的,在“常规”计算的情况下,这是一个很好的成本指标。但是,在处理非常规设备(此处为磁带)时,根据磁带的“移动”来计算复杂性会更准确。我认为这是让学生思考什么是“复杂性”的好方法。 我发现实际上我的算法与交换平均的选择排序算法非常相似。它位于这里:sorting-algorithms.com/selection-sort,如果你有兴趣,你可以找到一个非常有趣的解释,在哪里使用特定的算法以及它们的特定优缺点。 【参考方案1】:

我认为问题在于大多数快速排序实现依赖于一个分区步骤,该步骤在要排序的区域的两端交替读取和写入。在随机访问模型中,这非常好(所有读取基本上都是 O(1)),但在磁带上这可能非常昂贵,因为在要排序的范围的不同端之间来回交换可能需要 O( n) 磁带卷盘前后滚动的时间。这将通常是 O(n) 的分区步骤变成了可能是 O(n2) 的东西,从而支配了函数的运行时间。此外,由于执行磁带查找所需的时间可能比处理器的频率慢数千或数百万倍,因此这个 O(n2) 工作具有巨大的常数因子。

另一方面,冒泡排序没有这个问题,因为它总是比较数组中的相邻单元格。它最多在阵列上进行 O(n) 次遍历,因此只需要将磁带倒带 n 次。冒泡排序的处理逻辑肯定更昂贵 - 几乎比任何其他 O(n2) 排序都要昂贵 - 但与不来回寻找磁带所节省的时间相比,这算不了什么。

简而言之,快速排序在磁带上的运行速度可能比冒泡排序慢得多,因为它需要磁带在执行期间移动得更多。由于磁带查找的成本很高,因此快速排序的自然运行时间优势将在这一步中被消耗掉,而冒泡排序应该看起来更快。

【讨论】:

+1 没想到这么快就看到这个完整完整的回复 ;) Phanx ;) 我会考虑的!【参考方案2】:

templatetypedef's answer 是正确的。冒泡排序的访问不仅分散到最低限度,而且就地运行。我怀疑它实际上是具有单个任意慢磁带且只有 O(1) RAM 的机器的 最佳 排序算法。 [编辑:事实上cocktail sort(bubblesort 的双向版本)应该更好,因为它可以避免浪费的倒带——感谢 Steve Jessop。]

如果您有 4 个磁带驱动器可用,那么 mergesort rules the roost。只需 3 个磁带,即可使用 a fancier version of mergesort。

【讨论】:

我认为冒泡排序的最佳条件比这更具体,关于 2 磁头记忆鼓。 @Steve:听起来很有趣,虽然我不知道“2 头记忆鼓”是什么 :) 现在我想知道对于我描述的模型,哪种排序算法比冒泡排序更好( 1 个慢速磁带 + O(1) RAM)——我能想到的一切都需要更多内存或快速随机访问。想法? en.wikipedia.org/wiki/Drum_memory,请注意“一些鼓店,如 Univac FASTRAND 有一个或多个摇头”。使用一个慢速磁带 IIRC,您可以比冒泡排序做得更好,方法是在一个方向通过一个气泡,然后另一个气泡再次返回,这样就不必昂贵地倒带磁带回到起点将采用纯冒泡排序。可能被称为“鸡尾酒排序”? 谢谢@Steve,我以为冒泡排序是双向的,但我现在看到“鸡尾酒排序”是正确的术语。是的,最好避免倒带。【参考方案3】:

快速排序比冒泡排序更快的原因之一是它可以瞬间将元素移动很远的距离。如果 QuickSort 将一个元素向上移动 50 个元素,然后向下移动 20、向上 10、向上 5 和向下 2 直到它最终到达正确的位置,该元素将从它开始的位置结束 43 个插槽,而只移动了 5 次。冒泡排序会移动元素 43 次。如果将元素移动一个槽的成本与将其移动 50 的成本相同,那将是一个重大胜利。但是,如果移动元素的成本与距离成正比,则 QuickSort 将元素移动了 87 个槽的总距离——是冒泡排序的两倍。

如果一个人在处理磁带驱动器时遇到了困难,那么最佳算法将在很大程度上取决于驱动器的物理工作方式。例如,在某些驱动器上,唯一的操作是倒带和准备写入(在此过程中有效地擦除磁带)、倒带和准备读取以及处理下一个字节(读取或写入,取决于倒带模式)。其他驱动器允许在磁带上的任何位置随机访问和替换单个块。某些驱动器仅限于单向读取。其他(例如 QIC 磁带)有一些在一个方向读取的磁道和一些在另一个方向读取的磁道。我不知道是否有任何驱动器允许在两个方向上读取或写入相同的数据块,但这样的事情至少在理论上是可能的。

【讨论】:

以上是关于为啥 C 快速排序函数(磁带比较、磁带交换)比冒泡排序函数慢得多?的主要内容,如果未能解决你的问题,请参考以下文章

二级C语言排序技术2

面试官:写一个冒泡排序和快速排序吧

数组中元素的排序(常用的冒泡排序选择排序快速排序)

快速拍粗和冒泡排序

快速排序

排序算法5--交换排序--快速排序