《数据结构》第三章:排序基础

3.1 排序的概念与分类

含有多个数据项的数据元素称为记录。用作记录唯一标识的数据项称为关键字域,其值称为关键字。若关键字唯一标识一个记录,则称为主关键字,否则为次关键字。关键字类型与应用相关,既可以是简单类型,也可以是复合的结构类型。为了与ElemType区别,记录类型定义为RecordType。

typedef int KeyType;//关键字类型为整数类型

typedef struct{

KeyType key;//关键字项

...//其他数据项

}RecordType,RcdType;//记录类型,RcdType为RecordType的缩写

3.1.1 排序的概念

排序就是将无序的记录序列按关键字调整为有序记录序列的一种操作。

假设含有n个记录的序列R为

(r1,r2,…,rn)

它们相对应的关键字序列K为

(k1,k2,…,kn)

对记录序列R进行排序就是确定序列号1,2,…,n的一种排序P;

                                 (p1,p2,…,pn)

使其相应的关键字序列K满足非递减(或非递增)的关系:

                                 kp1≤kp2≤…≤kpn

也就是使记录序列R重新排列成一个关键字有序的序列R‘:

                                 {rp1,rp2,…,rpn}

这样的操作就称为排序。例如,将下列记录序列

(175,85,260,63,412,504,840,518,630,950)

调整为有序序列

(63,85,175,260,412,504,518,630,840,950)

在对排序算法的讨论中,若无特殊说明,均是对存储记录的顺序表排序。顺序表的0号单元留作他用,记录顺序表的类型定义如下:

typedef struct{

    RcdType *rcd;//存储空间基址

    int length;//当前长度

    int size;//存储容量

}RcdSqList;//记录的顺序表

3.1.2 排序的分裂

排序的方法分为两大类:内部排序和外部排序。

内部排序是待排序序列完全存放在内存中所进行的排序过程。

外部排序是对大文件的排序,待排序的文件无法一次装入内存,在排序过程中需要在内存和外部存储器之间进行多次数据交换。

根据排序过程所用策略的不同,可将内部排序方法分为五类:交换排序、选择排序、插入排序、归并排序和基数排序。

其中交换排序、选择排序和插入排序是一个逐渐扩大记录的有序序列长度的过程。

从序列的当前无序区取出一个元素,按照算法策略加入到有序区,使得有序去长度加1的过程,这个过程称为一趟排序。

交换排序是对无序区中记录的关键字两两进行比较,若逆序则交换,直到关键字之间之间不存在逆序为止。冒泡排序快速排序是交换排序中最典型的两个办法。

选择排序是在无序区中选出关键字最小的记录,置于有序区的最后,直到全部记录有序。简单选择排序堆排序是选择排序中最典型的两个方法。

插入排序是将无序区中的一个记录插入至有序区,使得有序区的长度加1,直到全部记录有序。直接插入排序希尔排序是插入排序中最具有代表性的两个方法。

归并排序是不断将两个或两个以上有序区合并成一个有序区,直到全部记录有序。

基数排序是和前面所述各类排序方法完全不同的一种排序方法,不需要进行记录关键字的比较。

内部排序方法可按照时间复杂度分为以下3类。

  1. 简单的排序方法,时间复杂度为O(n2)。
  2. 先进的排序方法,时间复杂度为O(nlog n)。
  3. 基数排序,时间复杂度为O(n)。

希尔排序的算法时间复杂度与增量序列有关,还涉及一些数学上没解决的难题,其算法的时间复杂度介于(1)和(2)之间。

3.2 直接插入排序

直接插入排序的算法思路是,每次将无序区的第一个记录按关键字插入到有序区的合适位置,并将有序区的长度加1。

加入记录个数为8,输入关键字序列(58,68,25,45,90,38,10,72)

第i趟插入排序若需将记录了L.rcd[i+1]插入到有序区L.rcd[1...i]中,则应先执行以下两步:

(1)查找插入位置,有序区的位置1到i,依次判断是否为记录L.rcd[i+1]的插入位置

代码:

j=0;

do

{

    j++;

}while(L.rcd[j].key<了.rcd[i+1].key);

(2)移动记录空出插入位置。将有序区中从位置i到j的记录依次向后移动一个位置,空出插入位置j。

代码:

L.rcd[0]=L.rcd[i+1];//先将记录L.rcd[i+1]保存在空闲的0号单元

k=i+1;

do

{

    k--;

    L.rcd[k+1]=L.rcd[k];

}while(k>j);

若将判断插入位置的次序改为从i到1,可将两步的代码合并:

L.rcd[0]=L.rcd[i+1];

j=i+1;

do

{

    j--;

    L.rcd[j+1]=L.rcd[j];

}while(j>1&&L.rcd[0].key<L.rcd[j-1].key);

而且,由于0号单元存放了记录L.rcd[i+1],所有判断j是否越界的操作“j>1”,可以略去。记录L.rcd[0]起到了边界监视哨的作用,称为哨兵

算法:直接插入排序

void InsertSort(RcdSqList &L){//对顺序表L作直接排序

    int i,j;

    for(i=1;i<L.length;++i)

    {

        if(L.rcd[i+1].key<L.rcd[i].key){//需将L.rcd[i+1]插入有序序列

            L.rcd[0]=L.rcd[i+1];//先将记录L.rcd[i+1]保存到空闲的0号单元

            j=i+1;

            do{

                j--;

                L.rcd[j+1]=L.rcd[j];//记录后移

            }while(L.rcd[0].key<L.rcd[j-1].key);//判断是否需要继续移动

            L.rcd[j]=L.rcd[0];//插入

        }

    }

}

排序算法的时间耗费主要与关键字比较和记录移动的次数相关。在外层循环中,若L.rcd[i+1].key≥L.rcd[i].key,即代插入记录不小于前一记录,则只进行这一次比较,内层循环不执行,不需要移动记录。反之,L.rcd[i+1].key<L.rcd[i].key,则执行内层循环。

最好情况,待排记录关键字有序,关键字的比较次数为n-1,即1+2+...+n-1,移动记录次数为0,均达到最小。

最坏情况,待排记录按关键字逆序,每一趟待插入记录需要和有序区所有记录及哨兵进行比较,除哨兵外这些记录均需移动(待插入记录在每一趟开始前移动到哨兵位置)。先看比较次数,在第i(i≤i<n)趟,内层循环比较到哨兵需i次比较,加上if语句中的第一次,空共需i+1;再看移动次数,第i趟需要移动i个记录,加上设置哨兵和插入到位各移动记录1次,共需i+2次。因此,关键字的比较次数为i+1(i从1到n-1),即(n+2)(n-1)/2,移动次数均为i+2(i从1到n-1),即(n+4)(n-1)/2,达到最大。直接插入排序的最坏情况下时间复杂度为O(n2)。

直接插入排序只需要一个记录的辅助空间,空间复杂度为O(1)。

3.3 希尔排序

希尔排序是将整个待排记录序列

(R1,R2,…,Rn)

按增量d划分成d个子序列,其中第i(1≤i≤d)个子序列为

(Ri,Ri+d,Ri+2d,…,Ri+kd)

并分别对各子序列进行直接插入排序;不断减小增量d,重复这一过程;直到d减少到1,对整个序列进行一次直接插入排序。增量d的取值序列称为增量序列。基于增量序列的降序特点,希尔排序也被称为缩小增量排序

希尔排序与直接插入排序不同之处在于,直接插入排序每次对相邻记录进行比较,记录最多只移动一个位置,而希尔排序每次对相隔较远举例(即增量)的记录进行比较,使记录移动时能跨过多个记录,实现宏观上的调整。当增量减小到1时,序列已基本有序,希尔排序的最后一趟就是接近最好情况的直接插入排序。一般情况下,可将前面各趟的调整看成是最后一趟的预处理,比只做一次直接插入排序效率更高。

假设待排序记录为10个,其对应关键字序列为

(49,38,65,97,76,13,27,49,55,04)

其中有两个49,后一个49加下划线以示区别。若增量序列为

(5,3,1)

第一趟的增量d1为5,将10个待排记录分为5个子序列,分别进行直接插入排序,结果为(13,27,49,55,04,49,38,65,97,76)。

第二趟的增量为d2为3,将上一趟的结果分为3个子序列,分别进行直接插入排序,结果为(13,04,49,38,27,49,55,65,97,76)

第三趟的增量d3为1,对整个序列进行直接排序,最后结果为(04,13,27,38,49,49,55,65,76,97)

算法:一趟希尔排序

void ShellInsert(RcdSqList &L;int dk){//对顺序表L做一趟希尔排序,增量为dk

    int i,j;
    
    for(i=1;i<=length-dk;++i)

    {

        if(L.rcd[i+dk].key<L.rcd[i].key)//需将L.rcd[i+dk]插入有序序列

        {

        L.rcd[0]=L.rcd[i+dk];//暂存在L.rcd[0]

        j=i+dk;

        do{

            j-=dk;

            L.rcd[j+dk]=L.rcd[j];//记录后移

        }while(j-dk>0&&L.rcd[0].key<L.rcd[j-dk].key)//是否移动

        L.rcd[j]=L.rcd[0];//插入

        }

    }

}

按增量序列d[0..t-1]对顺序表L作t趟希尔排序

算法:希尔排序

void ShellSort(RcdSqList &L,int d[],int t){//按增量序列d[0...t-1]对顺序表L作希尔排序

    int k;

    for(k=0;k<t;++k)

    {

        ShellInsert(L,d[k]);//一趟增量为d[k]的插入排序

    }

}

希尔排序的时间复杂度是所取增量序列的函数,尚难准确分析。当增量序列为d[k]=2t-k+1-1时,希尔排序的时间复杂度为O(n1.5),其中t为排序趟数,1≤k≤t≤[log2(n+1)]。

如果待排序记录中的关键字ki(i=1,2,…,n)都不相同,则任何一个记录的无序序列经排序后得到的结果是唯一的;繁殖,若待排序列中存在两个或两个以上关键字相等的记录,则排序所得的结果不唯一。假设ki=kj(1≤i≤n,1≤j≤n,i≠j),且在排序前的序列中ki领先于kj(即i<j)。若在排序后的序列中ki仍领先于kj,则称该排序方法是稳定的;反之,若可能使排序后的序列中kj领先于ki,则称高排序方法是不稳定的

例如,对于含有两个47的关键字序列

(56,34,47,23,66,18,82,47)

若排序的结果为

(18,23,34,47,47,56,66,82)

这种排序方法就为稳定的

若排序结果为

(18,23,34,47,47,56,66,82)

这种排序方法是不稳定的

3.4 基数排序

3.4.1 多关键字排序

一般情况下,多关键字排序的定义为,假设含有n个记录的序列为

(r1,r2,...,rn)

每个记录ri中含有m个关键字(ki0,ki1,...,kim-1),如果对于序列中任意两个记录ri和rj(1≤i<j≤n)都满足下列有序关系:

(ki0,ki1,...,kim-1)≤(kj0,kj1,...,kjm-1)

则称记录序列对这m个关键字有序。其中k0被称为最主位关键字,km-1被称为最次位关键字;(a0,a1,...,am-1)≤(b0,b1,...,bm-1)是指必定存在l,使得:当s=0,...,l-1时,as=bs,而al<bl。

实现关键字排序有两种策略:高位优先排序(MSD法)和低位优先排序(LSD法)

高位优先排序先按最主位关键字k0进行排序,得到若干子序列,其中每个子序列中的记录都含相同的k0值,之后分别对每个子序列按关键字k1进行排序,致使k1值相同的记录构成长度更短的子序列,依次重复,直到对每个子序列按km-1从小到大排序,最后由这些子序列依次相连所得序列便是排序的最后结果。

低位优先排序先按先按最低位关键字进行排序,接着按次低位关键字实施排序,最后按最主位关键字进行排序。与高位优先排序不同,其排序过程中不产生子序列,每趟都是对整个序列进行排序。

3.4.2 基数排序

基数排序的基本思路是,先将所有关键字统一为相同位数,位数较少的数前面补零。然后从最低位开始依次排序,知道按最高位排序完成,关键字序列就成为有序序列了。

一般情况下,将记录的关键字看成由m个关键字复合而成,每个关键字可能取r个值,则只要从最低位关键字起,先按关键字的不同值将记录“分配”到r个子序列,再按从小到大的顺序将各子序列依次首尾相接“收集”在一起,如此重复m趟,最终完成整个记录序列的排序。按这种方法实现的排序称为基数排序,其中基数r指的是“关键字”的取值范围。

常用的基数排序的实现方法有两种:链式基数排序和计数基数排序。

链式基数排序在链式存储结构中实现,在每一趟,分配是按相应关键字的取值将记录加入到r个不同的队列;收集是从小到大依次将r个队列首尾相接成一个链表。

技术基数排序在顺序存储结构中实现。在每一趟,分配是相对应关键字的每种取值计数(即统计r个子序列的长度),确定每个子序列的起始位置;收集是根据各个子序列的起始位置将记录复制到合适的位置。

将关键字看作多关键字,涉及取每一位操作。

技术基数排序的数据类型定义如下:

typedef struct{

KeysType *keys;//关键字

    ...//其他数据项

}KeysRcdType;//基数排序中的记录类型

typedef struct{

    KeysRcd *rcd;//0号位置做哨兵

    int length;//顺序表长度

    int size;//顺序表容量

    int digitNum;//关键字位数

    int radix;//关键字基数

}KeysSqList;//顺序表类型

实现计数基数排序时,需要引入三个数组,其中数组count和pos分别用于统计关键字的r种取值的个数和确定各个子序列的初始位置。count[i]是对值i的计数,pos[i]是值为i的子序列的起始位置。另一个数组rcd1与rcd类型相同。在各趟收集中,第一趟从数组rcd收集到rcd1,第二趟从rcd1收集到rcd,如此交替进行,若趟数为奇数,最后还需将排序的结果从数组rcd1复制回rcd。

例如,一组关键字(337,332,132,267,262,164,260,167)的8个记录序列进行低位优先的计数基数排序,需要三趟“分配”和“收集”。

第一趟对个位数排序,首先用数组count对个位数的每种取值计数,共有1个0、3个2、1个4和3个7,其余均为0个。

利用count数组统计第i个关键字各种取值的个数的代码如下:

for(j=0;j<L.radix;++j)

{

    count[j]=0;//初始化

}

for(k=1;k<=n;k++)

{

    count[rcd[k].keys[i]]++;//对各种值计数

}

基于count数组,依次计算关键字从0到9的记录的起始位置,存入数组pos。值为0的关键字的起始位置为1,值为j(1≤j≤9)的关键字的起始位置是值j-1的关键字的起始位置加上j-1的关键字的个数,其代码如下:

pos[0]=1;

for(j=1;j<L.radix;++j)

{

    pos[j]=pos[j-1]+count[j-1];

}

第一趟收集时,337的个位数7取得其起始位置pos[7]的值为6,将337放入rcd1[6]中,并将pos[7]+1,令pos[7]指向下一个个位数为7的记录应放入的位置。接着,由332的个位数2取得其起始位置pos[2]得值为2,将332放入rcd1[2]中,并将pos[2]加1.

收集的代码如下:

for(k=1;k<=n;++k)

{

    j=rcd[k].keys[i];

    rcd1[pos[j]++]=rcd[k];//复制记录,对应的起始位置加1

}

第二趟对rcd1的十位数进行分配,结果存入pos数组。

第二趟的收集将记录从数组rcd1收集到rcd。

第三趟对百位进行分配和收集,将记录从数组rcd收集到数组rcd1。第三趟收集就是最后的排序结果。由于趟数为奇数,须将数组rcd1复制回rcd。

算法:计数基数排序

Status RadixSort(KeysSqList &L){//对顺序表L进行计数基数排序

    KeysRcdType *rcd1;//开设同等大小的辅助空间用于复制数据

    int i=0,j;

    int *count,*pos;

    count=(int *)malloc(L.radix*sizeof(int));

    pos=(int *)malloc(L.radix*sizeof(int));

    rcd1=(KeysRcdType*)malloc((L.length+1)*sizeof(KeysRcdType));

    if(NULL==count||NULL==pos||NULL==rcd1)

    {

        return OVERFLOW;
    }

    while(i<L.digitNum)

    {

        for(j=0;j<L.radix;++j)

        {

            count[j]=0;//初始化

        }

        if(0==i%2)//对L.rcd进行一趟基数排序,结果存入rcd1

        {

            RadixPass(L.rcd,rcd1,L.length,i++,count,pos,L.radix);

        }

        else

        {
    
            RadixPass(rcd1,L.rcd,L.length,i++,count,pos,L.radix);

        }

    }

    if(1==L.digitNum%2)//排序后的结果在rcd1中,复制到L.rcd中

    {

        for(j=1;j<=L.length;j++)

        {

            L.rcd[j]=rcd1[j];

        }

    }

    free(count);

    free(pos);

    free(rcd1);

    return OK;

}

算法:一趟计数基数排序

void RadixPass(KeysRcdType rcd[],KeysRcdType rcd1[],int n,int i,int count[],int pos[],int radix)

{

    //对数组rcd中记录的第i位计数,计算得到位置数组pos[]

    //并按照pos数组将数组rcd中记录复制到数组rcd1中

    int k,j;

    for(k=1;k<=n;k++)

    {

        count[rcd[k].keys[i]]++;//对各种取值计数

    }
    
    pos[0]=1;

    for(j=1;j<radix;++j)

    {

        pos[j]=count[j-1]+pos[j-1];//求初始位置

    }

    for(k=1;k<=n;++k)//收集

    {

        j=rcd[k].keys[i];

        rcd1[pos[j]++]=rcd[k];//复制记录,对应起始位置加1

    }

}

计数基数排序算法无需比较关键字,主要操作时分配和收集。对于n个记录,每个记录包含m个关键字,基数为r,分配过程统计r个值对应的记录数,收集过程根据统计的结果将记录复制到合适位置。因此一趟分配与收集的时间复杂度为O(n),记录由m个关键字构成,则需要进行m趟分配与收集,因此计数基数排序算法的时间复杂度O(mn),且是一种稳定的排序算法。通常m远小于n,时间复杂度可视为O(n)。由于采用了长度为n的赋值数组,rcd1,算法的空间复杂度为O(n)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sɪʟᴇɴᴛ໊ོ5329

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值