大话数据结构——第九章排序笔记

一、排序的基本概念与分类

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

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

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

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

多关键字的排序最终都可以转化为单个关键字的排序。

1.排序的稳定性

因为排序结果可能存在不唯一的情况,下面给出了稳定不稳定排序的定义。
稳定:假设ki=kj(1 ⩽ \leqslant i ⩽ \leqslant n, 1 ⩽ \leqslant j ⩽ \leqslant n, i ≠ \not= =j),且在排序前的系列中ri领先于rj(即i<j)。如果排序后ri仍领先于rj,则称所用的排序方法是稳定的。
不稳定:如果排序后rj领先于ri,则称所用的排序方法是不稳定的。

2.内排序与外排序

根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序。
内排序:是在排序整个过程中,待排序的所有记录全部被放置在内存中。
外排序:是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
我们这里主要介绍内排序的多种方法。
对于内排序来说,排序算法的性能主要是受3个方面影响:

  1. 时间性能
    排序是数据处理中经常执行的一种操作,往往属于系统的核心部分,因此排序算法的时间开销是衡量其好坏的最重要的标识。
    在内排序中,主要进行两种操作:比较和移动。 比较指关键字之间的比较,这是要做排序最起码的操作。移动指记录从一个位置移动到另一个位置,实际上,移动可以通过改为记录的存储方式来予以避免。
    总之,高效的内排序算法应该具有尽可能少的关键字比较次数和尽可能少的记录移动次数。

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

  3. 算法的复杂度
    注意这里指的是算法本身的复杂度,而不是指算法的时间复杂度。显然算法过于复杂也会影响排序的性能。

根据排序过程中借助的主要操作,我们把内排序分为:插入排序、交换排序、选择排序和归并排序
本章一共要讲解七种排序的算法,按照算法复杂度分为两大类,冒泡排序、简单选择排序和直接插入排序属于简单算法,而希尔排序、堆排序、归并排序、快速排序属于改进算法。

3.排序用到的结构与函数

为了讲清楚排序算法的代码,我先提供一个用于有排序的顺序表结构,此结构也将用于之后我们要讲到所有排序算法。

# define MAXSIZE 10       /*用于要排序数组个数最大值,可根据需要修改*/
typedef struct
{
    int r(MAXSIZE+1);     /*用于存储要排序数组, r[0]用作哨兵或临时变量*/
    int length;           /*用于记录顺序表的长度
}SqList

另外,用于排序最最常用的操作是数组两元素的交换,我们将它写成函数,在之后的讲解中会大量的用到。

/*交换L中数组r的下标为i和j的值*/
void swap(SqList *L, int j)
{
    int temp=L->r[i];
    L->r[i]=L->r[j];
    L->r[j]=temp;
}

二、冒泡排序

1.最简单排序实现

冒泡排序(Bubble Sort)一种交换排序,它的基本思想是:两两比较相邻记的关键字,如果反序则交换,直到没有反序记录为止。
3种不同的冒泡实现代码:

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

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

2.冒泡排序算法

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

较小的如同气泡般慢慢浮到上面,因此就将此算法命名为冒泡排序。

3.冒泡排序优化

增加一个标记变量flag来实现这一算法的改进,避免因已经有序的情况下的无意义循环判断。

/*对顺序表L作改进冒泡排序*/
void BubbleSort2(SqList *L)
{
    int i,j;
    Status flag=TURE;        /*flag用来标记*/
    for(i=j;i<L->length&& flag;i++)     /*若flag为true则退出循环*/
    {
        flag=FALSE;      /*初始为false*/
        for(j=L->length-1;j>=i;j--)
        {
            if(L-r[j]>L->r[j+1])
            {
                swag(L,j,j+1);    /*交换L->r[j]与L->r[j+1]*/
                flag=TURE;        /*如果有数据交换,则flag为true*/
            }
        }
    } 
}

4.冒泡排序复杂度分析

分析一下它的时间复杂度。
当最好的情况,就是要排序的表本身有序的,根据最后改进的代码,比较的次数为n-1,没有数据交换,时间复杂度为O(n)。
当最坏的情况,就是待排序表是逆序的情况,需要比较n(n-1)/2次,并作等数量级的移动记录,时间复杂度为O(n2)。

三、简单选择排序

冒泡排序的思想就是不断地在交换,通过交换完成最终的排序,这和做股票短线频繁操作的人类似。
选择排序的初步思想是在排序时找到合适的关键字再做交换,并且只移动一次就完成相应关键字的排序定位工作。
选择排序的基本思想是每一趟在n-i+1(i=1,2,…,n-1)个记录中选取关键字最小的记录作为有序序列的第i个记录。

1.简单选择排序

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

/*对顺序表L作简单选择排序*/
void SelectSort(SqList *L)
{
    int i,j;
    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=j;                             /*将此关键字的下标赋值给min*/
            }
        }
        if(i!=min)                                /*若min不等于i,说明找到最小值,交换*/
        {
            swap(L,i,min);                       /*交换L->r[i]>L->r[min]的值*/
        }
    }
}

2.简单选择排序复杂度分析

需要进行的比较次数为n(n-1)/2次,交换次数为n-1次。
当最好的时候交换次数是0次,最坏的是n-1次,基于最终的排序时间是比较与交换的次数总和,因此,时间复杂度为O(n2)。

四、直接插入排序

例如扑克要理理顺序才能看出是否为同花顺,理牌的方法就是直接插入排序法。

1.直接插入排序算法

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

/*对顺序表L作直接插入排序*/
void InsertSort(SqList *L)
{
    int i,j;
    for(i=2;i<L->length;i++)
    {
        if(L->r[i]<L->r[i-1])                               /*需将L->r[i]插入有序子表*/
        {
            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];                              /*插入到正确的位置*/
        }
    }
}

在这里插入图片描述
在这里插入图片描述

2.直接插入排序复杂度分析

从空间上看,它只需要一个记录的辅助空间,因此关键是看它的时间复杂度。
当最好的情况,共比较了n-1次,没有移动的记录,时间复杂度为O(n)。
当最坏的情况,比较(n+2)(n-1)/2次,移动(n+4)(n-1)/2次。
如果排序记录是随机的,那么根据概率相同的原则,平均比较次数和移动次数约为n2/4次。时间复杂度为O(n2)。
可以看出,同样是时间复杂度为O(n2),直接插入排序比冒泡排序和简单选择排序的性能要好一些。

五、希尔排序

优秀排序的首要条件就是速度。科学家们将内排序算法的时间复杂度提升到了O(nlogn)。

1.希尔排序原理

希尔排序是D.L.Shell与1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是O(n2)的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
前一节讲的直接插入排序的效率在某些时候是很高的,比如记录本身就是基本有序的,还有就是记录数较少时。可问题是这两个条件本身就过于苛刻,现实中记录少或者基本有序都属于特殊情况。于是希尔研究出了一种排序方法,对直接插入排序改进后可以增加效率。
基本有序就是小的关键字基本在前面,大的关键字基本在后面,不大不小的基本在中间,像{2, 1, 3, 6, 4, 7, 5, 8, 9}这样可以称为基本有序了。但像{1, 5, 9, 3, 7, 8, 2, 4, 6}这样的9在第三位,2在倒数第三位就谈不上基本有序。
如何让待排序的记录个数较少?很容易想到的就是将其进行分组,分割成若干个子序列,然后再每个子序列内分别进行直接插入排序,当整个序列都基本有序时,再对整体记录进行一次插入排序。但会出现子序列排序好了,但是合并后还是杂乱无序的情况。
因此采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序二不是局部有序。

2.希尔排序算法

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.希尔排序复杂度分析

希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式移动,使得排序的效率提高。
这里“增量”的选取十分关键。迄今为止还没有人找到一种最好的增量序列,不过大量的研究表明,当增量系列为dlta[k]=2t-k+1-1(0KaTeX parse error: Undefined control sequence: \leqlant at position 1: \̲l̲e̲q̲l̲a̲n̲t̲kKaTeX parse error: Undefined control sequence: \leqlant at position 1: \̲l̲e̲q̲l̲a̲n̲t̲tKaTeX parse error: Undefined control sequence: \leqlant at position 1: \̲l̲e̲q̲l̲a̲n̲t̲[log2(n+1)])时,可以获得不错的效率,且时间复杂度为O(n3/2),要好于直接插入排序的O(n2)。
需要注意的是,增量系列的最后一个增量值必须等于1才行。
另外,由于记录是跳跃式移动,希尔排序是一种不稳定的排序算法。

六、堆排序

简单选择排序在待排序的n个记录中选择一个最小的记录需要比较n-1次。可惜的是,这样的操作并没有把每一趟的比较结果保留下来,会导致重复前面已经做过的比较操作。
堆排序就是对简单选择排序的改进,是Floyd和Williams在1964年共同发明的,同时,他们发明了“堆”这样的数据结构。
是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

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

这里i ≤ \leq [n/2]的原因是二叉树的性质5:如果对一棵有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第一层到[log2n]+1层,每层从左到右),对任一结点i(1 ≤ \leq i ≤ \leq n)有:

  1. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]。
  2. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
  3. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
    将大顶堆和小顶堆用层序遍历存入数组:

1.堆排序算法

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

/*对顺序表L进行堆排序*/
void HeapSort(SqList *L)
{
    int i,j;
    for(i=L->length/2;i>0;i--)      /*把L中的r构建成一个大顶堆*/
    {
        HeapAdjust(L,i,L->length);
    }
    for(i=L->length;i>1;i--)
    {
        swap(L,1,i);             /*将堆顶记录和当前未经排序子序列的最后一个记录交换*/
        HeapAdjust(L,1,i-1);     /*将L->r[1..i-1]重新调整为大顶堆*/
    }
}
/*已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义*/
/*本函数调整L->r[s]的关键字,是L->r[s..m]成为一个大顶堆*/
void HeapAdjust(SqList *L,int s, int m)
{
    int temp;
    temp=L->r[s];
    for(j=2*s;j<=m;j*=2)     /*沿关键字较大dd 孩子结点向下筛选*/
    {
        if(j<m && L->r[j]<L->r[j+1])
        {
             ++j;              /*j为关键字中较大的记录的下标*/
        }
        if(temp>=L->r[j])
        { 
             break;         /*rc应该插入在位置s上*/
        }
        L->r[s]=L->r[j];
        s=j;
    }
    L->r[s]=temp;                /*插入*/
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来是HeapSort函数的6-11行就是正式的排序过程:

在这里插入图片描述在这里插入图片描述

2.堆排序的复杂度分析

它的运行时间主要是消耗在初始构建堆和重建堆的反复筛选上。
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为[log2i]+1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
所以总体来说,堆排序的时间复杂度为O(nlogn)
由于堆排序对初始状态并不敏感,因此它无论最好、最坏和平均时间复杂度均为O(nlogn)。这在性能上显然要远远高于冒泡、简单选择、直接插入的O(n2)的时间复杂度。
空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错。
不过由于记录的比较和交换是跳跃式进行,因此堆排序是一种不稳定的排序方法。
另外,由于初始构建堆所需的比较次数较多,因此,它不适合到待排序序列个数较少的情况

七、归并排序

堆排序用到了完全二叉树,充分利用了完全二叉树的深度是[log2n]+1十五特性,所以效率比较高。但设计本身比较复杂。
另一种更直接简单的利用完全二叉树的排序方法是归并排序。

1.归并排序算法

“归并”一词的中文含义就是合并、并入的意思,而在数据结构中的定义是将两个或两个以上的有序表组合成一个新的有序表,
归并排序(Merging Sort)就是利用归并的思想实现的排序方法。
原理:假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]([x]表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,…,如此重复,直到得到一个长度为n的有序序列为止,这种排序的方法称为2路归并排序这里只介绍2路归并排序

/*对顺序表L进行归并排序*/
void MergeSort(SqList *L)
{
    MSort(L->r,L->r,1,L->length); 
}
/*将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
     {
         m=(s+t)/2;      /*将SR[s..t]平均分为SR[s..m]和SR[m+1..t]*/
         MSort(SR,TR2,s,m);  /*递归将SR[s..m]归并为有序的TR2[s..m]*/
         MSort(SR,TR2,m+1,t);  /*递归将SR[m+1..t]归并为有序的TR2[m+1..t]*/
         Merge(TR1,TR2,s,m,t);  /*将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t]*/

     }
     
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

/*将有序的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;
    for(j=m+1,k=i;i<m && j<=n;k++)        /*将SR中的记录由小到大归并入TR*/
    { 
        if(SR[i]<SR[j])
             TR[k]=SR[i++];
        else
             TR[k]=SR[j++];
    }
    if(i<=m)
    {
        for(l=0;l<=m-1;l++)
            TR[k+1]=SR[i+1];     /*将剩余的SR[i..m]复制到TR*/
            
    }
    if(j<=n)
    {
        for(l=0;l<=n-j;l++)
            TR[k+1]=SR[j+1];       /*将剩余的SR[j..m]复制到TR*/   
    }
}

在这里插入图片描述
在这里插入图片描述

2.归并排序的复杂度分析

一趟归并需要将SR[1]—SR[n]中相邻的长度为h的有序序列进行两两归并。并将结果放到TR1[1]—TR[n]中,这需要将待排序序列中的所有记录扫描一遍,因此耗时O(n)时间,而由完全二叉树的深度可知,这个归并排序需要进行[log2n]次,因此总的时间复杂度为O(nlogn),并且这是归并排序算法中最好、最坏和平均的时间性能。
由于归并排序再归并过程需要与原始记录同样数量的存储空间存放归并结果以及递归时深度为log2n的栈空间,因此归并排序空间复杂度为O(n+logn)。
归并排序时一种稳定排序

3.非递归实现归并排序

大量引用递归会造成时间和空间上的性能的损耗,可以将递归转化成迭代来提升性能。

/*对顺序表L作归并非递归排序*/
void MergeSort2()
{
    int *TR=(int *)malloc(L->length*sizeof(int));   /*申请额外空间*/
    int k=1;
    while(k<L->length)
    {
        Mergepass(L->r,TR,k,L->length);
        k=2*k;                            /*子序列长度加倍*/
        MergePass(TR,L->r,k,L->length);
        k=2*k;                          /*子序列长度加倍*/
    }
}

在这里插入图片描述

/*将SR[]中相邻长度为s的子序列两两归并的TR[]中*/
void MergePass(int SR[],int TR[],int s,int n)
{
    int i=1;
    int j;
    while(i<=n-2*s+1)
    {
        Merge(SR,TR,i,i+s-1,i+2*s-1);       /*两两归并*/
        i=i+2*s;
    }
    if(i<n-s+1)                            /*归并最后两个序列*/
        Merge(SR,TR,i,i+s-1,n);
    else                               /*若最后只剩下单个子序列*/
        for(j=1;j<=n;j++)
            TR[j]=SR[j];
}

在这里插入图片描述
在这里插入图片描述

4.非递归归并排序复杂度分析

非递归的迭代方法避免了递归时深度为log2n的栈空间,空间只是用到申请归并临时用的TR数组,因此空间复杂度为O(n)。并且避免递归也在时间性能上有一定的提升。在使用归并排序时,尽量考虑用非递归方法。

八、快速排序

快速排序最早由图灵奖获得者Tony Hoare设计出来的。快速排序被列为20世纪十大算法之一。
希尔排序相当于直接插入排序的升级,它们同属于插入排序类,堆排序相当于简单选择排序的升级,它们同属于选择排序类。而快速排序是冒泡排序的升级,它们同属于交换排序类。 即它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动交次数。

1.快速排序算法

快速排序(Quick Sort)的基本思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,已到达整个序列有序的目的。

/*对顺序表L作快速排序*/
void QuickSort(SqList *L)
{
    QSort(L,1,L->length);
}
/*对顺序表L中的子序列L->r[low..high]作快速排序*/
void QSort(SqList *L,int low,int high)
{
    int pivot;
    if(low<high)
    {
        pivot=Partition(L,low,high);    /*将L->r[low..high]一分为二,算出枢轴值pivot*/
        Qsort(L,low,pivot-1);        /*对低子表递归排序*/
        Qsort(L,pivot+1,high);        /*对高子表递归排序*/
    }
}

Partition函数要做的就是先选取当中的一个关键字,然后想尽办法将它放到第一位置,使得它左边的值都比它小,右边的值比它大,我们将这样的关键字称为枢轴(pivot)。

/*交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置*/
void Partition(SqList *L,int low,int high)
{
    int pivotkey;
    pivotkey=L->r[low];         /*用子表的第一个记录作枢轴记录*/
    while(low<high)             /*从表的两端交替向中间扫描*/
    {
        while(low<high && L->r[high]>=pivotkey)
            high--;
        swap(L,low,high);      /*将比枢轴记录小的记录交换到低端*/
    }
    return low;               /*返回枢轴所在位置*/
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.快速排序复杂度分析

快速排序的时间性能取决于快速排序递归的深度,可以用递归树来描述递归算法的执行情况。
最优情况下,Partition每次都划分得很均匀,如果排序n个关键字,其递归树得深度为[log2n]+1([x]表示不大于x得最大整数),即仅需递归log2n次,需要时间为T(n)得话,第一次Partition需要对整个数组扫描一遍,做n次比较,然后,获得得枢轴将数组一分为二,那么各自还需要T(n/2)的时间。于是不断划分下去。再最优情况下,时间复杂度为O(nlogn)。
再最坏情况下,待排序的序列为正序或逆序,每次划分只得到一个比上一个划分少一个记录的子序列,注意另一个为空。如果递归树画出来,它就是一颗斜树。此时需要执行n-1次递归调用,且第i次划分需要经过n-i次关键字的比较才能找到第i个记录,也就是枢轴的位置,因此比较次数为n(n-1)/2,最终时间复杂度为O(n2)。
平均情况,时间复杂度为O(nlogn)。
空间复杂度,最好情况为O(logn);最坏情况为O(n);平均情况为O(logn)。

3.快速排序优化

一些优化方案:

  1. 优化选取枢轴
  2. 优化不必要的交换
  3. 优化小数组时排序方案
  4. 优化递归操作

3.1 优化选取枢轴

三数取中法(median-of-three):取三个关键字先进行排序,将中间数作为枢轴,一般取左端、右端和中间三个数,也可以随机选取。
在Partition函数代码的第三行与第四行之间增加这样一段代码。

int pivotkey;

int m=low+(high-low)/2;     /*计算数组中间的元素下标*/
if(L->r[low]>L->r[high])
    sawp(L,low,high);       /*交换左端与右端数据,保证左端较小*/
if(L->r[m]>L->r[high])
    sawp(L,high,m);        /*交换中间与右端数据,保证中间较小*/
if(L->r[m]>L->r[low])
    sawp(L,m,low);        /*交换中间与左端数据,保证左端较小。此时L.r[low]已经为整个序列左中右三个关键字的中间值。*/

pivotkey=L->r[low];        /*用子表的第一个记录作枢轴记录*/

九数取中(median-of-nine):先从数组中分三次取样,每次取三个数,三个样品各取出中数,然后从这三个中数当中再取出一个中数作为枢轴。

3.2 优化不必要的交换

/*快速排序优化算法*/
int Partition1(SqList *L, int low,int high)
{
    int pivotkey;
    //这里省略三数取中代码
    pivotkey=L->r[low];        /*用子表的第一个记录作枢轴记录*/
    L->r[0]=pivotkey;          /*从表的两端交替向中间扫描*/
    while(low<high)
    {
        while(low<high && L->r[high]>=pivotkey)
            high--;
        L->r[low]=L->r[high];      /*采用替换而不是交换的方式进行操作*/
        while(low<high && L->r[low]>=pivotkey)
            low++;
        L->r[low]=L->r[low];      /*采用替换而不是交换的方式进行操作*/
    }
    L->r[low]=L->r[0];      /*将枢轴数值替换回L.r[low]*/
    return low;              /*返回枢轴所在位置*/
}

在这里插入图片描述

3.3 优化小数组时排序方案

如果数组非常小,快速排序不如直接插入排序(直接插入排序是简单排序中性能最好的)。其原因在于快速排序用到了递归操作,大量数据排序时,可以忽略不计。因此需要改进QSort。

#define MAX_LENGTH_INSERT_SORT 7   /*数组长度阀值,对顺序表L中的子序列L.r[low..high]作快速排序*/
void QSoet(SqList &L, int low, int high)
{
    int pivot;
    if((high-low)>MAX_LENGTH_INSERT_SORT)
    { /*当high-llow大于常数时用快速排序*/
        pivot=Partition(L,low,high);    /*将L.r[low..high]一分为二,并计算出枢轴值pivot*/
        QSort(L,low,pivot-1);         /*对低子表递归排序*/
        QSort(L,pivot+1,high);       /*对高子表递归排序*/
    }
    else      /*当high-low小于等于常数时用直接插入排序*/
         InsertSort(L);
}

3.4 优化递归操作

递归对性能有一定的影响。QSort函数在其尾部有两次递归操作。
于是我们对QSort实施尾递归。

/*对顺序表L中的子序列L.r[low..high]作快速排序*/
void QSort(SqList *L,int low,int high)
{
    int pivot;
    if((high-low)>MAX_LENGTH_INSERT_SORT)
    {
       while(low<high)
       {
           pivot=Partition(L,low,high);     /*L.r[low..high]一分为二,算出枢轴值pivot*/
           QSort(L,low,pivot-1);         /*对低子表递归排序*/
           low=pivot+1;            /*尾递归*/
       }
    }
    else
        InsertSort(L);
}

7种算法的对比:

排序方法平均价格最好情况最坏情况辅助空间稳定性
冒泡排序O(n2)O(n)O(n2)O(1)稳定
简单选择排序O(n2)O(n2)O(n2)O(1)稳定
直接插入排序O(n2)O(n2)O(n2)O(1)稳定
希尔排序O(nlogn)~O(n2)O(n1.3)O(n2)O(1)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
快速排序O(nlogn)O(nlogn)O(n2)O(logn)~O(n)不稳定

7种算法分类:
简单算法:冒泡、简单选择、直接插入。
改进算法:希尔、堆、归并、快速。

从平均情况看,显然最后3种改进算法要胜过希尔排序,并远远胜过前3种简单算法。
从最好情况看,冒泡和直接插入排序要更胜一筹。
从最坏情况看,堆排序与归并排序又强过快速排序以及其他简单排序。

从这三组时间复杂度的数据对比中,我们可以得出这样的一个认识。堆排序和归并排序就像是两个参加奥数考试的优等生,心理素质强,发挥稳定。而快速排序像是很情绪化的天才,心情好时表现极佳,碰到较糟糕环境会变得差强人意。但是他们如果都来比个赛计算个位数的加减法,它们反而不如成绩及普通的冒泡和直接插入。

从空间复杂度来说,归并排序强调要马跑得快,就得给马吃个饱。快速排序也有相对应的空间要求,反而堆排序等却都是少量索取,大量付出,对空间要求是O(1)。如果执行算法的软件所处的环境非常在乎内存使用量的多少时,选择归并排序和快速排序就不是一个较好的决策了。

从稳定性来看,归并排序独占鳌头,对于非常在乎排序稳定性的应用中,归并排序是个好算法。

从待排记录的个数上来说,待排序的个数n越小,采用简单排序方法方法较合适,反之,n越大,采用改进排序方法越合适。

简单选择排序的优点:
如果记录的关键字本身信息量比较大(例如,关键字都是数十位的数字),此时表明其占用存储空间很大,这样移动记录所花费的时间就越多,我们给出三种简单排序算法的移动次数比较:

排序方法平均情况最好情况最坏情况
冒泡排序O(n2)0O(n2)
简单选择排序O(n2)0O(n)
直接插入排序O(n2)O(n)O(n2)

可以看出简单选择排序变得很有优势,原因在于,它是通过大量比较后选择明确记录进行移动,有的放矢。因此对于数据量不是很大而记录的关键字信息量较大的排序要求,简单排序算法是占优的。另外,记录的关键字信息量大小对那四个改进算法影响不大。

总之,从综合各项指标来说,经过优化的快速排序是性能最好的排序算法,但是不同的场合我们应该考虑使用不同的算法来应对它。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值