第 9 章 排序

来源:互联网 发布:英特尔人工智能大会 编辑:程序博客网 时间:2024/05/28 15:08

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

9.1 开场白

9.2 排序的概念与分类

9.2.1 排序的稳定性

假设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. 算法的复杂性
    根据排序过程中借助的主要操作,把内排序分为:插入排序、交换排序、选择排序和归并排序。
    按照算法的复杂度分为两类,冒泡排序、简单选择排序和直接插入排序属于简单算法,而希尔排序、堆排序、归并排序、快速排序属于改进算法。

9.2.3 排序用到的结构与函数

先提供一个用于排序用的的顺序表结构。

#define MAXSIZE 10  typedef struct{    int r[MAXSIZE];    int length;}SqList;

交换函数

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

9.3 冒泡排序

9.3.1 最简单排序实现

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

//对顺序表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);        }    }}

9.3.2 冒泡排序算法

//对顺序表L作冒泡排序void BubbleSort1(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]的值        }    }}

9.3.3 冒泡排序优化

//对顺序表L作改进冒泡算法void BubbleSort2(SqList *L){    int i, j;    Status flag = TRUE;    for (i = 1; 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])            {                swap(L, j, j+1);        //交换L->r[j]与L->r[j+1]的值                flag = TRUE;        //如果数据交换,则flag为TRUE            }        }    }}

9.3.4 冒泡排序算法复杂度分析

总的时间复杂度为O(n2

9.4 简单选择排序

选择排序的基本思想是一趟在n-i+1个记录中选取关键字最小的记录作为有序序列的第i个记录。

9.4.1 简单选择排序算法

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

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

与冒泡排序相比较,只需交换8次就可完成,性能略有于冒泡排序。

9.4.2 简单选择排序复杂度分析

时间复杂度为O(n2).

9.5 直接插入排序

9.5.1 直接插入排序算法

直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增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--0)                L-r[j+1] = L->r[j];     //记录后移            L->r[j+1] = L->r[0];        //插入到正确位置        }    }}

9.5.2 直接插入排序复杂度分析

复杂度为O(n2).同样的,直接插入排序算法比冒泡和简单排序的性能要好一些。

9.6 希尔排序

9.6.1 希尔排序原理

  所谓的基本有序,就是小的关键字基本在前面,大的基本在后面,不大不小的基本在中间。
  采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
  

9.6.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++)        {            if (L->r[i] < L->r[i-increment])            {//需将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);}

9.6.3 希尔排序复杂度分析

  通过这段代码的剖析,希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式的移动,使得排序的效率提高。
  增量的选取非常关键了,目前还是一个数学难题,没有一个人找打一种最好的增量序列。不过大量的研究表明,当增量序列为dlta[k]=2tk+11(0<=k<=t<=log2(n+1)时,可以获得不错的效率,其时间复杂度为O(n3/2,要好与直接排序的O(n2)。需要注意的是,增量序列的最后一个增量值必须等于1才行。另外,由于记录跳跃式的移动,希尔排序并不是一种稳定的算法
  

9.7 堆排序

堆排序就是对简单选择排序的一种改进。
  堆具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆;
  根结点一定是最大或小值。
  

9.7.1 堆排序算法

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

  1. 如何由一个无序序列构建成一个堆
  2. 如果在输出堆顶元素后,调整剩余元素成为一个新的堆。
//对顺序表L进行堆排序void HeapSort(SqList *L){    int i;    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]重新调整成为大顶堆。    }}

  所谓的将待排序的序列构建成为一个大顶堆,其实就是从下往上、从右到左,将每个非终端结点(非叶节点)当做根结点,将其和其子树调整成大顶堆。下面是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;      //rc应插入在位置s上        L->r[s] = L->r[j];        s = j;    }    L->r[s] = temp;     //插入}

9.7.2 堆排序复杂度分析

总的时间复杂度为O(nlgn)。它无论是最好、最坏还是平均时间复杂度都为O(nlgn)。不过由于记录的比较鱼交换是跳跃式进行,因此堆排序也是一种不稳定的排序算法。另外,由于初始构建堆所需的比较次数较多,因此,他并不适合待排序序列个数较少的情况。

9.8 归并排序

9.8.1 归并排序算法

  归并排序的原理是假设初始序列有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,让后两两归并,得到⌈n/2⌉个长度为2或者1的有序子序列;再两两归并,如此重复,直到的得到一个长度为n的有序序列为止。
  

//对顺序表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(TR2, TR1, 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, 1;    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 - i; l++)            TR[k+1] = SR[i+1];      //将剩余的SR[i..m]复制到TR    }    if (j <= n)    {        for (l=1; l <= n-j; l++)        TR[k+1] = SR[j+1];      //将剩余的SR[j..n]复制到TR    }}

9.8.2 归并排序复杂度分析

总的时间复杂度为O(nlgn),而且这是归并排序算法中最好最坏、平均时间时间性能。空间复杂度为O(n+logn)。但是归并排序是稳定的排序算法。
  总之,归并排序是一种比较占用内存,但效率高且稳定的算法。
  

9.8.3 非递归实现归并排序

//对顺序表L作归并非递归排序void MergeSort2(SqList *L){    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=i; j <= n; j++)            TR[j] = SR[j];}

使用归并排序时,尽量考虑使用非递归方法

9.9 快速排序

  希尔排序相当于直接插入排序的升级,堆排序相当于简单选择排序的升级,而快速排序相当于冒泡排序的升级。
  

9.9.1 快速排序算法

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

//对顺序表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函数实现

//交换顺序表L中子表的记录,使枢轴记录到位,并返回其所在位置//此时在它之前(后)的记录均不大于它int 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);                   while(low < high && L->r[low] <= pivotkey)            low++;        swap(L,low,high);           }    return low;     //返回枢轴所在位置}

9.9.2 快速排序复杂度分析

空间复杂度为O(logn),时间复杂度为O(nlgn)。**快速排序是一种不稳定的排序方法。
**

9.9.3 快速排序优化

  • 优化选取枢轴

三数取中,即取三个关键字先进行排序,将中间数作为枢轴,一般是左端、中间、右端。
在Partition函数代码的第3行和第4行之间增加这样一端代码:

//优化选取枢轴int pivotkey;int m = low + (high - low)if (L->r[low] > L->r[high])    swap(L, low, high);         //交换左端与右端数据,保证左端较小if (L->r[m] > L->r[high])    swap(L, high, m);           //交换中间与右端数据,保证中间较小if (L->r[m] > L->r[low])    swap(L, low, high);         //交换中间与左端数据,保证左端较小//此时L.r[low]已经为整个序列左中右三个关键的中间值pivotkey = L->r[low];           //用子表的第一个记录作枢轴记录
  • 优化必要的交换
//优化不必要的交换int Partition1(SqList *L, int low, int high){    int pivotkey;    //这里省略三数取中代码    pivotkey = L->r[low];       //用子表的第一个记录作枢轴记录    L->r[0] = pivotkey;         //将枢轴关键字备份到L->r[0]    while(low < high)           //从表的两端交替向中间扫描    {        while(low < hight && L->r[high] >= pivotkey)            high--;        L->r[low] = L->r[high];     //采用替换而不是交换的方式进行操作        while(low < high && L->r[low] <= pivotkey)            low++;        L->r[high] = L->r[low];     //采用替换而不是交换的方式进行操作    }    L->r[low] = L->r[0];        //将枢轴数值替换会L.r[low]    return low;     //返回枢轴所在位置}
  • 优化小数组时的排序方案
//优化小数组时的排序方案#define MAX_LENGTH_INSERT_SORT  7void QSort(SqList &L, int low, int high){    int pivot;    if ((high-low) > MAX_LENGTH_INSERT_SORT)    {//当high-low大于常数时用快速排序        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);}
  • 优化递归操作
//优化递归操作//对顺序表L中的子序列L->r[low..high]作快速排序void QSort1(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            QSort1(L, low, pivot - 1);      //对低子表递归排序            low = pivot + 1;            //尾递归        }    }    else         InsertSort(L);}
  • 了不起的排序算法
    依然时最好的排序算法

9.10 总结回顾

这里写图片描述

9.11 结尾语