大话数据结构 -- 排序

基本概念

排序:
假设含有n个记录的序列为{r1,r2,......,rn},其相应的关键字分别为{k1,k2,......,kn},需确定1,2,......,n的一种排列p1,p2,......,pn,使其相应的关键字满足kp1≤kp2≤......≤kpn(非递减或非递增)关系,即使得序列成为一个按关键字有序的序列{rp1,rp2,......,rpn},这样的操作就称为排序

在排序问题中,通常将数据元素称为记录。显然我们输入的是一个记录集合,输出的也是一个记录集合。所以说,可以将排序看成是线性表的一种操作

排序的依据是关键字之间的大小关系,那么,对同一个记录集合,针对不同的关键字进行排序,可以得到不同序列

这里关键字ki可以是记录r的主关键字,也可以是次关键字,甚至是若干数据项的组合。

对于组合排序的问题,当然可以先排序总分,若总分相等的情况下,再排序语数外总分,但这是比较土的办法。我们还可以应用一个技巧来实现一次排序即完成组合排序问题。例如,把总分与语数外都当成字符串首尾连接在一起(注意如果语数外总分不够三位,需要在前面补0)。

从此也可看出,多个关键字的排序最终都可以转化为单个关键字的排序。因此,我们这里主要讨论的是单个关键字的排序。

对于次关键字,因为待排序的记录序列中可能存在两个或两个以上的关键字相等的记录,排序结果可能会存在不唯一的情况,我们给出了稳定与不稳定排序的定义。

假设ki=kj(1≤i≤n,1≤j≤n,i≠j),且在排序前的序列中ri领先于rj(即i<j)。如果排序后ri仍领先于rj,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中rj领先ri,则称所用的排序方法是不稳定的。如图9-2-2所示,经过对总分的降序排序后,总分高的排在前列。此时对于令狐冲和张无忌而言,未排序时是令狐冲在前,那么它们总分排序后,分数相等的令狐冲依然应该在前,这样才算是稳定的排序,如果他们二者颠倒了,则此排序是不稳定的了。只要有一组关键字实例发生类似情况,就可认为此排序方法是不稳定的。排序算法是否稳定的,要通过分析后才能得出。

根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为内排序和外排序。

内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。

对于内排序来说,排序算法的性能主要是受3个方面影响:

1、时间性能。在内排序中,主要进行两种操作:比较和移动。比较指关键字之间的比较,这是要做排序最起码的操作。移动指记录从一个位置移动到另一个位置。事实上,移动可以通过改变记录的存储方式来予以避免。总之,高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数

2、辅助空间。即执行算法所需要的辅助存储空间,它是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间

3、算法的复杂性。这里指的是算法本身的复杂度,而不是指算法的时间复杂度。

根据排序过程中借助的主要操作,我们把内排序分为:插入排序、交换排序、选择排序和归并排序

按照算法的复杂度又可分为两大类:冒泡排序、简单选择排序和直接插入排序属于简单算法;而希尔排序、堆排序、归并排序、快速排序属于改进算法

冒泡排序

冒泡排序是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止

先看一段比较容易理解的代码:

// 对顺序表L作交换排序(冒泡排序初级版)
void BubbleSort0(SqList *L){
    int i,k;
    for(i=1;i<L->length;i++){
        for(j=i+1;j<=L->length;j++){
            if(L->r[i]>L->r[j]){
                // 交换L->r[i]与L->r[j]的值
                swap(L,i,j);
            }
        }
    }
}

这段代码严格意义上说不算是标准的冒泡排序算法,因为它不满足“两两比较相邻记录”的冒泡排序思想,它更应该是最最简单的交换排序而已。它的思路就是让每一个关键字都和它后面的每一个关键字比较,如果大则交换,这样第一位置的关键字在一次循环后一定变成最小值。

这个简单易懂的代码是有缺陷的。观察后发现,在排序好1和2的位置后,对其余关键字的排序没有什么帮助(数字3反而还被换到了最后一位)。也就是说,这个算法的效率是非常低的

来看看正宗的冒泡算法,并考虑它能否优化?

// 对顺序表L作冒泡排序
void BubbleSort(SqList *L){
    int i,j;
    for(i=1;i<L->length;i++){
        for(j=L->length;j>=i;j--){
            // 若前者大于后者(注意这里与上一算法差异)
            if(L->r[j]>L->r[j+1]){
                // 交换L->r[j]与L->r[j+1]的值
                swap(L,j,j+1);
            }
        }
    }
}

依然假设我们待排序的关键字序列是{9,1,5,8,3,7,4,6,2},当i=1时,变量j由8反向循环到1,逐个比较,将较小值交换到前面,直到最后找到最小值放置在了第1的位置。如图9-3-3所示,当i=1、j=8时,我们发现6>2,因此交换了它们的位置,j=7时,4>2,所以交换……直到j=2时,因为1<2,所以不交换。j=1时,9>1,交换,最终得到最小值1放置第一的位置。事实上,在不断循环的过程中,除了将关键字1放到第一的位置,我们还将关键字2从第九位置提到了第三的位置,显然这一算法比前面的要有进步,在上十万条数据的排序过程中,这种差异会体现出来。图中较小的数字如同气泡般慢慢浮到上面,因此就将此算法命名为冒泡算法。

当i=2时,变量j由8反向循环到2,逐个比较,在将关键字2交换到第二位置的同时,也将关键字4和3有所提升。

这样的冒泡程序是否还可以优化呢?答案是肯定的。试想一下,如果我们待排序的序列是{2,1,3,4,5,6,7,8,9},也就是说,除了第一和第二的关键字需要交换外,别的都已经是正常的顺序。当i=1时,交换了2和1,此时序列已经有序,但是算法仍然不依不饶地将i=2到9以及每个循环中的j循环都执行了一遍,尽管并没有交换数据,但是之后的大量比较还是大大地多余了,如图9-3-5所示。

当i=2时,我们已经对9与8,8与7,……,3与2作了比较,没有任何数据交换,这就说明此序列已经有序,不需要再继续后面的循环判断工作了。为了实现这个想法,我们需要改进一下代码,增加一个标记变量flag来实现这一算法的改进。

// 对顺序表L作改进冒泡算法
void BubbleSort2(SqList *L){
    int i,j;
    // flag用来作为标记
    Status flag=TRUE;
    // 若flag为TRUE说明有过数据交换,否则停止循环
    for(i=1;i<L->length && flag ;i++){
        // 初始为flase
        flag=FALSE;
        for(j=L->length;j>=i;j--){
            if(L->r[j]>L->r[j+1]){
                // 交换L->r[j]与r[j+1]的值
                swap(L,j,j+1);
                // 如果有数据交换,则flag为true
                flag=TRUE;
            }
        }
    }
}

代码改动的关键就是在i变量的for循环中,增加了对flag是否为true的判断。经过这样的改进,冒泡排序在性能上就有了一些提升,可以避免因已经有序的情况下的无意义循环判断

复杂度分析

当最好的情况,也就是要排序的表本身就是有序的,那么我们比较次数,根据最后改进的代码,可以推断出就是n-1次的比较,没有数据交换,时间复杂度为O(n)。当最坏的情况,即待排序表是逆序的情况,此时需要比较sigma(i=2, n, i-1)=1+2+3+...+(n-1)=n(n-1)/2次,并作等数量级的记录移动。因此,总的时间复杂度为O(n^2)

简单选择排序

听名字就知道,这是一种选择排序。选择排序的基本思想是每一趟在n-i+1(i=1,2,...,n-1)个记录中选取关键字最小的记录作为有序序列的第i个记录

简单选择排序法(Simple Selection Sort)就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1≤i≤n)个记录交换之

来看代码:

// 对顺序表L作简单选择排序
void SelectSort(SqList *L){
    int i,j,min;
    for(i=1;i<L->length;i++){
        min =i;
        for(j=i+1;j<=L->length;j++){
            // 如果有小于当前最小值的关键字
            if(L->r[min]>L->r[j])
            // 将此关键字的下标赋值给min
            min=j;
        }
        // 若min不等于i,说明找到最小值,交换
        if(i!=min){
            // 交换L->r[i]与L->r[min]的值
            swap(L,i,min);
        }
    }
}

代码应该说不难理解,针对待排序的关键字序列是{9,1,5,8,3,7,4,6,2},对i从1循环到8。当i=1时,L.r[i]=9,min开始是1,然后与j=2到9比较L.r[min]与L.r[j]的大小,因为j=2时最小,所以min=2。最终交换了L.r[2]与L.r[1]的值。如图9-4-1所示,注意,这里比较了8次,却只交换数据操作一次。

复杂度分析

分析它的时间复杂度发现,无论最好最差的情况,其比较次数都是一样的多,第i趟排序需要进行n-i次关键字的比较,此时需要比较sigma(i=1, n-1, n-i)=(n-1)+(n-2)+...+1=n(n-1)/2次。而对于交换次数而言,当最好的时候,交换为0次,最差的时候,也就初始降序时,交换次数为n-1次,基于最终的排序时间是比较与交换的次数总和,因此,总的时间复杂度依然为O(n^2)

应该说,尽管与冒泡排序同为O(n^2),但简单选择排序的性能上还是要略优于冒泡排序

直接插入排序

听名字就吉道,这是一种插入排序

直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表

来看代码:

// 对顺序表L作直接插入排序
void InsertSort(SqList *L){
    int i,j;
    for(i=2;i<L->length;i++){
        // 需将L->r[i]插入有序子表
        if(L->r[i]<L->r[i-1]{
            // 设置哨兵
            L->r[0]=L->r[i];
            for(j=i-1;L->r[j]>L->r[0];j--){
                // 记录后移    
                L->r[j+1]=L->r[j];
            }
            // 插入到正确位置
            L->r[j+1]=L->r[0];
        }
    }
}

程序开始运行,此时我们传入的SqList参数的值为length=6,r[6]={0,5,3,4,6,2},其中r[0]=0将用于后面起到哨兵的作用。

i从2开始的意思是我们假设r[1]=5已经放好位置,后面的牌其实就是插入到它的左侧还是右侧的问题

第6行,此时i=2,L.r[i]=3比L.r[i-1]=5要小,因此执行第8~11行的操作。第8行,我们将L.r[0]赋值为L.r[i]=3的目的是为了起到第9~10行的循环终止的判断依据。如图9-5-2所示。图中下方的虚线箭头,就是第10行,L.r[j+1]=L.r[j]的过程,将5右移一位。

此时,第10行就是在移动完成后,空出了空位,然后第11行L.r[j+1]=L.r[0],将哨兵的3赋值给j=0时的L.r[j+1],也就是说,将扑克牌3放置到L.r[1]的位置,如图9-5-3所示。

继续循环,第6行,因为此时i=3,L.r[i]=4比L.r[i-1]=5要小,因此执行第8~11行的操作,将5再右移一位,将4放置到当前5所在位置,如图9-5-4所示。

再次循环,此时i=4。因为L.r[i]=6比L.r[i-1]=5要大,于是第8~11行代码不执行,此时前三张牌的位置没有变化,如图9-5-5所示。

再次循环,此时i=5,因为L.r[i]=2比L.r[i-1]=6要小,因此执行第8~11行的操作。由于6、5、4、3都比2小,它们都将右移一位,将2放置到当前3所在位置。如图9-5-6所示。此时我们的排序也就完成了。

复杂度分析

从空间上看,它只需要一个记录的辅助空间(哨兵)。

对于时间复杂度,当最好的情况,也就是要排序的表本身就是有序的,

比如纸牌拿到后就是{2,3,4,5,6},那么我们比较次数,其实就是代码第6行每个L.r[i]与L.r[i-1]的比较,共比较了(n-1)sigma(i=2, n, 1)次,由于每次都是L.r[i]>L.r[i-1],因此没有移动的记录,时间复杂度为O(n)。

当最坏的情况,即待排序表是逆序的情况,比如{6,5,4,3,2},此时需要比较sigma(i=2, n, i)=2+3+...+n=(n+2)(n-1)/2次,而记录的移动次数也达到最大值sigma(i=2, n, i+1)=3+4+...+n=(n+4)(n-1)/2次。

如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次数约为n^2/4次。因此,我们得出直接插入排序法的时间复杂度为O(n^2)。从这里也看出,同样的O(n^2)时间复杂度,直接插入排序法比冒泡和简单选择排序的

前面我们提到的三种不同的排序算法,时间复杂度都是O(n^2),似乎没法超越了,一时,计算机学术界充斥着“排序算法不可能突破O(n^2)”的声音。

直到新的算法出现打破常规。

 

希尔排序

我们之前讲的直接插入排序,应该说它的效率在某些时候是很高的。比如,我们的记录本身就是基本有序的,我们只需要少量的插入操作,就可以完成整个记录集的排序工作,此时直接插入很高效。还有就是记录数比较少时,直接插入的优势也比较明显。

可问题在于,两个条件本身就过于苛刻,现实中记录少或者基本有序属于特殊情况。

于是科学家希尔研究出了一种排序方法,对直接插入排序改进后可以增加效率,这就是希尔排序算法

如何让待排序的记录个数较少呢?很容易想到的就是将原本有大量记录树的记录进行分组。分割成若干个子序列,此时每个子序列待排序的记录个数就比较少了,然后在这些子序列内分别进行直接插入排序。当整个序列都基本有序时(注意只是基本有序),再对全体记录进行一次直接插入排序。

问题在于,我们分隔待排序记录的目的是减少待排序记录的个数,并使整个序列向基本有序发展,但是如果只是简单的顺序分组后就各自排序,很有可能大的依旧在前面,小的依旧在后面,只能在每个组里做到局部有序,做不到整体的“基本有序”。

因此,我们需要采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序

代码如下:

// 对顺序表L作希尔排序
void ShellSort(SqList *L){
    int i,j;
    int increment=L->length;
    do{
        // 增量序列
        increment=increment/3+1;
        for(i=icrement+1;i<=L->length;i++){
            if(L->r[i]<L->r[i-increment]){
                // 需将L->r[i]插入有序增量子表
                // 暂存在L->r[0]
                L->r[0]=L->r[i];
                for(j=i-increment;L->r[j]>L->r[0] &&j>0;j-=increment)
                    // 记录后移,查找插入位置
                    L->r[[j+increment]=L->r[j];
                // 插入
                L->r[j+increment]=L->r[0];
            }
        }
    }while(increment>1)
}

会发现其实就是把原来直接插入排序的增量1改为increment,再加上一层循环改变increment就行了。(增量为1时停止循环)

复杂度分析

希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高

这里“增量”的选取就非常关键了。我们在代码中是用increment=increment/3+1的方式选取增量的,可究竟应该选取什么样的增量才是最好,目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列。不过大量的研究表明,当增量序列为dlta[k]=2t-k+1-1(0≤k≤t≤)时,可以获得不错的效率,其时间复杂度为O(n^3/2),要好于直接排序的O(n^2)。需要注意的是,增量序列的最后一个增量值必须等于1才行。另外由于记录是跳跃式的移动,希尔排序并不是一种稳定的排序算法

堆排序

我们前面讲到简单选择排序,它在待排序的n个记录中选择一个最小的记录需要比较n-1次。本来这也可以理解,查找第一个数据需要比较这么多次是正常的,否则如何知道它是最小的记录。

可惜的是,这样的操作并没有把每一趟的比较结果保存下来,在后一趟的比较中,有很多在前一趟已经做过了,但由于前一趟排序时未保存这些比较结果,所以后一趟排序时又重复执行了这些比较操作,因而记录的比较次数较多

如果可以做到每次在选择到最小记录的同时,并根据比较结果对其他记录做出相应的调整,那样排序的总体效率就会非常高了。而堆排序(HeapSort),就是对简单选择排序的一种改进。

“堆”结构相当于把数字符号堆成一个塔型的结构。

很明显,我们可以发现它们都是二叉树,如果观察仔细些,还能看出它们都是完全二叉树。左图中根结点是所有元素中最大的,右图的根结点是所有元素中最小的。再细看看,发现左图每个结点都比它的左右孩子要大,右图每个结点都比它的左右孩子要小。这就是我们要讲的堆结构。

是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆(例如图9-7-2左图所示);或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆(例如图9-7-2右图所示)。

这里需要注意从堆的定义可知,根结点一定是堆中所有结点最大(小)者。较大(小)的结点靠近根结点(但也不绝对,比如右图小顶堆中60、40均小于70,但它们并没有70靠近根结点)。

如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:

这里要结合之前“树”的讲解中二叉树的性质5,如果遗忘了可去https://blog.csdn.net/qq_36770641/article/details/82086142查看。

可以说,这个性质就是在为堆准备的。

如果将图9-7-2的大顶堆和小顶堆用层序遍历存入数组,则一定满足上面的关系表达,如图9-7-3所示。

堆排序(Heap Sort)就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列了。

来看代码:

// 对顺序表L进行堆排序
void HeapSort(SqList *L){
    int i;
    // 把L中的r构建成一个大顶堆
    for(i=L->length/2;i>0;i--)
        HeapAdjust(L,i,L->length);
    for(i=L->length;i>1;i--){
        // 将堆顶记录和当前未经排序子序列的最后一个记录交换
        swap(L,1,i);
        // 将L->r[1...i-1]重新调整为大顶堆
        HeapAdjust(L,1,i-1)
    }
}

从代码中也可以看出,整个排序过程分为两个for循环。第一个循环要完成的就是将现在的待排序序列构建成一个大顶堆。第二个循环要完成的就是逐步将每个最大值的根结点与末尾元素交换,并且再调整其成为大顶堆

我们所谓的将待排序的序列构建成为一个大顶堆,其实就是从下往上、从右到左将每个非终端结点(非叶结点)当作根结点,将其和其子树调整成大顶堆。

现在来看关键的HeapAdjust(堆调整)函数是如何实现的:

// 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义
// 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆
void HeapAdjust(SqList *L, int s, int m){
    int temp,j;
    temp=L->r[s];
    // 宴关键字较大的孩子结点向下筛选
    for(j=2*s;j<=m;j*=2){
        if(j<m&&L->r[j]<L->r[j+1])
            // j为关键字中较大的记录的下标
            ++j;
        if(temp>=L->r[j])
            break;
        L->r[s]=L->r[j];
        s=j;
    }
    L->r[s]=temp;
}

复杂度分析

堆排序的运行时间主要是消耗在初始构建堆和在重建堆时的反复筛选上。

在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)

在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为| log2(n) |+1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)

所以总体来说,堆排序的时间复杂度为O(nlogn)。由于堆排序堆原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)

空间复杂度上,它只有一个用来交换的暂存单元。不过由于记录的比较与交换是跳跃式进行(父子结点之间),因此堆排序也是一种不稳定的排序方法

另外,由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况

归并排序

前面我们讲了堆排序,因为它用到了完全二叉树,充分利用了完全二叉树的深度是|log2n|+1的特性,所以效率比较高。不过堆结构的设计本身是比较复杂的,老实说,能想出这样的结构就挺不容易,有没有更直接简单的办法利用完全二叉树来排序呢?当然有。

为了更清晰地说清楚这里的思想,大家来看图9-8-1所示,我们将本是无序的数组序列{16,7,13,10,9,15,3,2,5,8,12,1,11,4,6,14},通过两两合并排序后再合并,最终获得了一个有序的数组。注意仔细观察它的形状,你会发现,它像极了一棵倒置的完全二叉树通常涉及到完全二叉树结构的排序算法,效率一般都不低的——这就是我们要讲的归并排序法。

“归并”在数据结构中的定义是将两个或两个以上的有序表组合成一个新的有序表。

归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 |n/2| ( |x| 表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序

来看代码:

// 对顺序表L作归并排序
void MergeSort(SqList *L){
    Msort(L->r,L->r,1,L->length);
}

一句代码,别奇怪,它只是调用了另一个函数而已。为了与前面的排序算法统一,我们用了同样的参数定义SqList *L,由于我们要讲解的归并排序实现需要用到递归调用,因此我们外封装了一个函数。假设现在要对数组{50,10,90,30,70,40,80,60,20}进行排序,L.length=9,我现来看看MSort的实现。

// 将SR[s..t]归并排序为TR1[s..t]
void Msort(int SR[],int TR1[],int s, int t){
    int m;
    int TR2[MAXSiZE+1];
    if(s==t)
        TR1[s]=SR[s];
    else{
        // 将SR[s..t]平分为SR[s..m]和SR[m+1..t]
        m=(s+t)/2;
        // 递归将SR[s..m]归并为有序的TR2[s..m]
        Msort(SR,TR2,s,m);
        // 递归将SR[m+1..t]归并为有序TR2[m+1..t]
        Msort(SR,TR2,m+1,t);
        // 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t]
        Merge(TR2,TR1,s,m,t);
    }
}

 

接下来再看Merge函数的代码是如何实现的:

// 将有序的SR[i..m]和SR[m+1..n]归并为有序的TR[i..n]
void Merge(int SR[], int TR[], int i, int m, int n){
    int j,k,l;
    // 将SR中记录由小到大归并入TR
    for(j=m+1,k=i;i<=m&&j<=n;k++){
        if(SR[i]<SR[j])
            TR[k]=SR[i++];
        else
            TR[k]=SR[j++];
    }
    if(i<=m){
        for(l=0;l<=m-i;l++)
            // 将剩余的SR[i..m]复制到TR
            TR[k+l]=SR[i+l];
    }
    if(j<=n){
        for(l=0;l<=n-j;l++)
           // 将剩余的SR[j..n]复制到TR
            TR[k+l]=SR[j+l];     
    }
}

1.假设我们此时调用的Merge就是将{10,30,50,70,90}与{20,40,60,80}归并为最终有序的序列,因此数组SR为{10,30,50,70,90,20,40,60,80},i=1,m=5,n=9。

2.第4行,for循环,j由m+1=6开始到9,i由1开始到5,k由1开始每次加1,k值用于目标数组TR的下标。

3.第6行,SR[i]=SR[1]=10,SR[j]=SR[6]=20,SR[i]<SR[j],执行第7行,TR[k]=TR[1]=10,并且i++。如图9-8-7所示。

4.再次循环,k++得到k=2,SR[i]=SR[2]=30,SR[j]=SR[6]=20,SR[i]>SR[j],执行第9行,TR[k]=TR[2]=20,并且j++,如图9-8-8所示。

5.再次循环,k++得到k=3,SR[i]=SR[2]=30,SR[j]=SR[7]=40,SR[i]<SR[j],执行第7行,TR[k]=TR[3]=30,并且i++,如图9-8-9所示。

6.接下来完全相同的操作,一直到j++后,j=10,大于9退出循环,如图9-8-10所示。

7.第11~20行的代码,其实就将归并剩下的数组数据,移动到TR的后面。当前k=9,i=m=5,执行第13~20行代码,for循环l=0,TR[k+l]=SR[i+l]=90,大功告成。

复杂度分析

一趟归并需要将SR[1]~SR[n]中相邻的长度为h的有序序列进行两两归并。并将结果放到TR1[1]~TR1[n]中,这需要将待排序序列中的所有记录扫描一遍,因此耗费O(n)时间,而由完全二叉树的深度可知,整个归并排序需要进行 |log2(n)| +1次,因此,总的时间复杂度为O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能

由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为log2(n)的栈空间,因此空间复杂度为O(n+logn)

另外,对代码进行仔细研究,发现Merge函数中有if(SR[i]<SR[j])语句,这就说明它需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法

也就是说,归并排序是一种比较占用内存,但却效率高且稳定的算法

猜你喜欢

转载自blog.csdn.net/qq_36770641/article/details/82563314