1、7.5 归并排序,基本原理,通过对两个有序结点序列的合并来实现排序。 所谓归并是指将若干个已排好序的部分合并成一个有序的部分。 对象序列dataList中有两个有序表vl,vm和vm+1,vn。它们可归并成一个有序表,存于另一对象序列 mergedList 的vl,vn中。 这种归并方法称为两路归并 (2-way merging)。 归并排序经常用于外部排序,设有两个有序表A和B,对象个数分别为al和bl,变量i和j分别是两表的当前指针。设表C是归并后的新有序表,变量k是它的当前指针。i和j对A和B遍历时,依次将关键字小的对象放到C中,当A或B遍历结束时,将另一个表的剩余部分照抄到新表中。,0
2、8 21 25 25* 49 62 72 93,16 37 54,l m m+1 n,initList,i j,08 16 21 25 25* 37 49 54 62 72 93,l n,k,mergeList,迭代的归并排序算法,迭代的归并排序算法就是利用两路归并过程进行排序的,其基本思想是: 假设初始对象序列有n个对象,首先把它看成是n个长度为1的有序子序列(归并项),先做两两归并,得到 n/2 个长度为2的归并项(如果n为奇数,则最后一个有序子序列的长度为1);再做两两归并,如此重复,最后得到一个长度为n的有序序列。,迭代的归并排序算法,21,25,25*,25*,93,62,72,08
3、,37,16,54,49,21,25,49,62,93,08,72,16,37,54,21,25,25*,49,08,62,72,93,16,37,54,08,08,21,16,25,21,25*,25,49,25*,62,37,72,49,93,54,16,37,54,62,72,93,len=1,len=2,len=4,len=8,len=16,template void merge(dataList /若第二个表未检测完,复制 ,迭代的两路归并算法,一趟归并排序的情形,设数组initList.Vector1到initList.Vectorn中的n个对象已经分为一些长度为len的归并项,将
4、这些归并项两两归并,归并成一些长度为2len的归并项,结果放到mergedList.Vector中。 如果n不是2len的整数倍,则一趟归并到最后,可能遇到两种情形: 剩下一个长度为len的归并项和另一个长度不足len的归并项,可用一次merge算法,将它们归并成一个长度小于2len的归并项。 只剩下一个归并项,其长度小于或等于len,可将它直接抄到数组MergedList.Vector中。,template void MergePass ( datalist /只有一个子表 ,template void MergeSort ( datalist ,(两路)归并排序的主算法,算法分析,在迭代的
5、归并排序算法中,函数MergePass( )做一趟两路归并排序,要调用merge( )函数n/(2*len) O(n/len)次,函数MergeSort( )调用MergePass( )正好log2n 次,而每次merge( )要执行比较O(len)次,所以算法总的时间复杂度为:log2nlenn/len = O(nlog2n) 归并排序占用附加存储较多,需要另外一个与原待排序对象数组同样大小的辅助数组。这是这个算法的缺点。 归并排序是一个稳定的排序方法。,递归的归并排序,与快速排序类似,归并排序也可以利用划分为子序列的方法递归实现。 在递归的归并排序方法中,首先要把整个待排序序列划分为两个长
6、度大致相等的部分,分别称之为左子表和右子表。对这些子表分别递归地进行排序,然后再把排好序的两个子表进行归并。 图示:待排序对象序列的关键码为21,25,49,25*,16, 08,先是进行子表划分,待到子表中只有一个对象时递归到底。再是实施归并,逐步退出递归调用的过程。,21 25 49 25* 16 08,21 25 49,25* 16 08,21,25,49,25 49,21,25*,16 08,25*,16,08,21 25 49 25* 16 08,25* 16 08,21 25 49,16 08,25*,25 49,21,21,25*,16,08,49,25,递 归,回 推,data
7、List mergeSort(dataList ,递归实现归并算法的基本思路,tempalatevoid mergeSort(dataList /合并 ,通过算法演示观看归并排序的算法执行过程,递归实现(两路)归并排序的主算法,算法分析,两路归并排序所需时间主要包括划分两个子序列的时间、两个子序列分别排序的时间和归并的时间。划分子序列的时间是一个常数,可以不考虑,最后归并所需时间与元素个数成线形关系,因此,总的时间代价应当为:,T(n)推导过程与快速排序类似,结果为:O(nlog2n)。归并排序不依赖于原待排序元素序列的初始输入排列,所以,归并排序的最好、最差和平均时间复杂度都为O(nlog2
8、n)。,算法改进,R.Sedgewick提出了一个改进的两路归并算法。在把元素序列复制到辅助数组的过程中,把第二个有序表的元素顺序逆转。这样,两个待归并的表从两端开始处理,向中间归并,两个表的尾端互成监视哨,省去程序中间是否越界的判断,提高了算法效率(当然,算法的时间复杂度并没有改变)。,改进的两路归并排序算法,template void improvedMerge(dataList ,template void doSort(dataList /对排序结果在座插入排序 ,7.6 基于链表的排序,前面讨论的排序方法都是基于数组实现的,排序过程中需要大量的移动数据。在排序元素体积较大时,移动数据
9、花费的时间较多。 基于链表来实现排序,在排序过程中通过修改链接指针有序地将各个元素链接起来,不需要移动数据,可有效提高排序的效率。,const int DefaultSize=10; /默认静态链表最大容量 template struct Element /静态链表元素类的定义T key; /排序码,其他信息略int link; /节点的链表指针Element( ):link(0) /构造函数Element(T x,int next=0):key(x),link(next) /构造函数 ,静态链表的定义,template class staticLinkedList /静态链表的类定义 pub
10、lic:staticLinkedList(int sz=Default):maxSize(sz),n(0)Vector=new Elementsz; /构造函数Element /最大元素个数和当前元素个数 ,静态链表的定义,链表插入排序,链表插入排序的基本思想是:在每个对象的结点中增加一个链接指针数据成员link。 对于存放于数组中的一组对象v1,v2,vn,若v1,v2,vi-1已经通过指针link,按其关键码的大小,从小到大链接起来,现在要插入vi, i=2, 3,n,则必须在前面i-1个链接起来的对象当中,循链顺序检测比较,找到vi应插入(或链入)的位置,把vi插入,并修改相应的链接指针
11、。这样就可得到v1,v2,vi的一个通过链接指针排列好的链表。 如此重复执行,直到把vn也插入到链表中排好序为止。,25,49,25*,16,08,0 1 2 3 4 5 6,i = 2,i = 3,21,初始,current pre i,current pre i,pre current i,i = 4,25,49,25*,16,08,0 1 2 3 4 5 6,i = 5,i = 6,21,pre current i,结果,pre current i,链表插入排序示例,template int insertSort(staticLinkedList /结点i链入pre和p之间 ,链表插入排
12、序的算法,算法分析,使用链表插入排序,每插入一个对象,最大关键码比较次数等于链表中已排好序的对象个数,最小关键码比较次数为1。故总的关键码比较次数最小为 n-1,最大为:,用链表插入排序时,对象移动次数为0。但为了实现链表插入,在每个对象中增加了一个链域link,并使用了vector0作为链表的表头结点,总共用了n个附加域和一个附加对象。 算法从i=2开始,从前向后插入。并且在vector current.Key = vectori.Key时,current还要向前走一步,pre跟到current原来的位置,此时, vectorpre.Key = vectori.Key。将vectori插在v
13、ectorpre的后面,所以,链表插入排序方法是稳定的。,归并时采用静态链表的存储表示,可以得到一种有效的归并排序算法。,设待排序的元素存放在类型为staticlinkedlist的静态链表中。初始时置所有的Vectori.link=0,即每个链表结点都是一个独立的单链表,L.Vector0.link用于存放结果链表的头指针。 通过函数ListMerge( )将两个有序的单链表归并成一个有序的单链表。函数返回结果链表的头指针。,链表归并排序,template int ListMerge(staticLinkedList /返回结果链头指针 ,静态链表的两路归并算法,template int r
14、MergeSort(staticLinkedlist ,基于链表的归并排序算法,算法分析,链表的归并排序方法的递归深度为O(log2n),对象关键码的比较次数为O(nlog2n)。 链表的归并排序方法是一种稳定的排序方法。,排序算法性能分析,以上的算法都是通过比较确定元素之间的先后关系,因此,这些算法均为基于比较的排序算法。 什么是基于比较的排序算法的最好性能呢? 我们考虑用树的形式来描述排序过程,树的每一个结点表示一次关键字的比较,树的分支表示比较的结果。这棵树被称之为决策树。决策树中的一条路径表示排序算法中产生的一个比较序列。 假定对含有3个元素的表采用插入排序算法进行排序,相应的决策树如
15、下一张幻灯片所示。,树的叶结点表示算法的终止。因此,对于上图给定的表和排序算法,得到六个可能的比较序列。,对一个长度为n的任意元素序列进行排序,至少需要作O(nlog2n)次比较。,证明: 对n个元素进行排序,排在第一位有n种可能,排在第二位有n-1种可能,因此,总的有n!个不同的排序结果,每种排序结果对应于一种比较序列,因此,任何一棵排序的决策树有n!个叶子结点。 一棵决策树也是一棵二叉树,要想使得树的高度最小,则该树应是一棵完全二叉树,高度为k的完全二叉树最多有 2k-1 个叶子结点。 n!=2k-1 ,所以,树的高度k至少是log2n!+1。,根据斯特林公式,有n!(n/2)n/2,于是
16、,基于比较的排序算法的最坏情况复杂性下界为:这就是借助于“比较”进行的排序能达到的最好性能。,另外,在排序过程中每次进行元素的比较产生两种路径,如果排序过程至少需要k次比较,则有2k条路径。由于n个元素共有n!种不同的排列(即决策树中的叶子结点最多为n!),因而必然有n!种不同的比较路径。于是:,排序算法是否还能够改进呢?从前面的分析我们知道,如果要改进排序算法的效率,就不能只利用“比较”来确定元素间相对位置。因为,光知道元素的大小信息是不够的,还需要知道元素的其他附加信息。基数(多关键字)排序和计数排序是具有线性时间复杂性的排序算法,这些算法无一例外地对输入数据作了某些附加限制,从而增加已知
17、的信息,因此可以不通过比较来确定元素间的相对位置。,7.7 分配排序,分配排序是采用“分配”与“收集”的办法,用对多关键码进行排序的思想实现对单关键码进行排序的方法。,桶式排序,假定待排序元素序列在某个值域范围内是均匀分布的; 桶式排序的思路是将相应值域划分为若干个等长的子区间,这些区间称为桶或箱; 然后将各个元素按照自己所属的区间放入相应的桶中。因为待排序元素是均匀分布的,所以,各个桶中的元素个数大致相当; 只需对每个桶中的元素排好序,然后依次输出各个桶中的元素,就得到了有序的元素序列。,桶式排序,将元素分配到每个桶中的时间复杂度为O(n),如果对桶中元素采用直接插入排序,时间复杂度为O(n
18、2),所以整个桶排序的时间复杂度为:,可以证明,桶排序的平均时间复杂度为O(n)。,如对扑克牌的排序 每张扑克牌有两个“关键字”:花色和面值,它们之间有次序的优先。 对以上排序,可以先对花色排序,或先对面值排序。 基本原理,采用“分配”和“收集”的办法,是一种用对多关键字进行排序的思想实现对单关键字进行排序的方法。 下面介绍多关键字排序,多关键字排序方法示例,基数排序,例 扑克牌排序,扑克牌有两个属性(花色与面值),设定次序如下:花色: 面值:234510JQKA 扑克牌排序如下(假定花色优先于面值): 2, A, 2, A, 2, A, 2, 2 方法有两种:(1)先按花色分配,再按面值排序
19、。或(2)先按面值分配,再按花色排序。,多关键字有序的概念,考虑对象序列V0,V1,., Vn-1,每个对象Vi含d个关键字(Ki1,Ki2,., Kid)。若对序列中的任意两个对象Vi和Vj都有(Ki1,Ki2,., Kid) (Kj1,Kj2,., Kjd) 则称序列对关键字(Ki1,Ki2,., Kid)有序,且K1称为最高位关键字,Kd称为最低位关键字。,多关键字排序,原理:根据组成各个关键字的的值进行排序 实现基数排序的两种方法: 1 最高位优先(MSD)排序:从关键字的高位到低位; 2 最低位优先(LSD)排序:从关键字的低位到高位。 利用多关键字排序实现对单个关键字排序的算法就称
20、 为基数排序。,MSD方法通常是一个递归的过程:首先根据最高位关键码K1进行排序,得到若干个对象组(对象组的数量取决于K1的取值 ,如果K1取值有两种可能,则划分为两个对象组,如果K1取值有10种可能,则划分为10个对象组)。我们称每位关键码可能的取值数为基数(radix)。对象组中的每个对象都具有相同的关键码K1;然后分别对每组中的对象根据关键码K2进行排序,按K2值的不同,再分成若干个更小的子组,每个子组中的对象具有相同的K1、K2值。依次重复,直到对关键码Kd完成排序为止。最后,把所有子组中的对象依次连接起来,就得到一个有序的对象序列。(比如扑克牌排序),MSD方法在每个关键码取值范围(
21、基数)较小时,效果较好。 如对二进制数的排序,每次根据关键字Ki分为两组,前一组为Ki为0,后一组Ki为1:011,110,101,111,010,001 第一次排序分组: 011,010,001, 110,101,111 第二次排序分组: 001, 011,010, 101, 110,111 第三次排序分组: 001, 010, 011, 101, 110, 111 最后,将所有第三次分组的结果连接起来。,MSD算法框架,MSD(S,i) /S为初始序列,假设有d个关键字,从第i个关键字开始分组排序,初始i=1;while (i1) MSD(Sj, i+1);return S1+Sm; ,c
22、onst int radix=10; /基数 templatevoid RadixSort(dataList,最高位优先的基数排序算法,for (j=0; jradix; j+) countj=0;for (i=left; iright; i+) /统计各桶元素的存放位置countgetDigit(Li,d)+;for (j=1); jradix; j+) /安排各桶元素的存放位置countj=countj+countj-1;for (i=left; iright; i+) /将待排序序列中的元素按位置分配到各个桶中,存于辅助数组auxArray中j=getDigit(Li,d); /取元素L
23、i第d位的值auxArraycountj-1=Li; /按预先计算位置存放countj-; /计数器减1for (i=left, j=0; i=right; i+, j+) Li=auxArrayj; /从辅助数组顺序写入原数组,for (j=0; jradix; j+) /按桶递归对d-1位处理p1=countj; /取桶始端p2=countj+1-1; /取桶尾端RadixSort(L, p1, p2, d-1); /对桶内元素进行基数排序 ,LSD方法是首先依据最低位关键码Kd对所有对象进行一趟排序,然后依据次低位关键码对上一趟排序的结果再排序,依次重复,直到对关键码K1完成排序为止。这
24、样就得到了一个有序的对象序列。使用这种方法进行排序时,不需要再分组,而是在每趟排序时,所有对象都参加排序。,如对二进制数的排序:011,110,101,111,010,001 第一次排序分组: 110, 010, 011, 101, 111, 001 第二次排序分组: 101, 001, 110, 010, 011, 111 第三次排序分组: 001, 010, 011, 101, 110, 111 最后,得到最终的排序结果。,多关键字排序(续),LSD和MSD方法也可应用于对一个关键字进行的排序。此时可将单关键字Ki看成是一个子关键字组:(Ki1, Ki2, ., Kid)对关键字取值范围为
25、0到999的一组对象,可看成是(K1,K2,K3)的组合。MSD方法按K1, K2, K3的顺序对所有对象排序,排序过程中需要不断的分组,大组分好后,在大组的基础上继续分小组;LSD方法按K3, K2, K1的顺序对所有对象排序,排序过程中不分组。,链式的基数排序,基数排序是一种典型的LSD排序方法,它利用“分配”和“收集”两种运算对单关键字进行排序。 此时可将单关键字K看成是一个子关键字组:(Ki1,Ki2,., Kid) 排序过程:设关键字共有d位,让j=d, d-1, ., 1, 依次执行d次“分配”与“收集”。 各队列采用链式队列结构,分配到同一队列的关键码用链接指针链接起来。每一队列
26、设置两个队列指针:int frontradix指示队头,int rearradix 指向队尾。 为了有效地存储和重排n个待排序对象,以静态链表作为它们的存储结构。在对象重排时不必移动对象,只需修改各对象的链接指针即可。,例 对序列614,738,921,485,637,101,215,530,790,306 进行链式的基数排序。排序过程:关键码共有3位,让j=3, 2, 1, 依次执行3次“分配”与“收集”。每位关键码的取值范围为0到9,设10个箱子(用链将箱子里的元素串接起来)。,基数排序的“分配”与“收集”过程 第一趟(个位),614,921,485,637,738,101,215,530
27、,790,306,第一趟分配(按最低位 i = 3 ),re0 re1 re2 re3 re4 re5 re6 re7 re8 re9,614,738,921,485,637,101,215,530,790,306,fr0 fr1 fr2 fr3 fr4 fr5 fr6 fr7 fr8 fr9,第一趟收集,530,790,921,101,614,485,215,306,637,738,基数排序的“分配”与“收集”过程 第二趟(十位),614,921,485,637,738,101,215,530,790,306,第二趟分配(按次低位 i = 2 ),re0 re1 re2 re3 re4 re
28、5 re6 re7 re8 re9,614,738,921,485,637,101,215,530,790,306,fr0 fr1 fr2 fr3 fr4 fr5 fr6 fr7 fr8 fr9,第二趟收集,530,790,921,101,614,485,215,306,637,738,基数排序的“分配”与“收集”过程 第三趟(百位),614,921,485,637,738,101,215,530,790,306,第三趟分配(按最高位 i = 1 ),re0 re1 re2 re3 re4 re5 re6 re7 re8 re9,614,738,921,485,637,101,215,530,
29、790,306,fr0 fr1 fr2 fr3 fr4 fr5 fr6 fr7 fr8 fr9,第三趟收集,530,790,921,101,614,485,215,306,637,738,for (序列中的每一个元素) /建立初始链表将其连接到初始链表中;for (从低位到高位,针对每一个子关键码) for (基数的每一个可能值) /对每一个基数对应的队列初始化初始化队列;for (序列中的每一个元素)按照基数将其放置在相应的队列中;找到第一个非空的队列q;for (从q开始的每一个队列) /按序将队列连接成新的链表将队列连接到链表中; ,算法框架:,通过算法演示观看基数排序的算法执行过程,L
30、SD链式基数排序算法,const int radix=10; /基数 templatevoid Sort(staticlinkedList,while (current!=0) /将n个元素分配到各队列中去k=getDigit(Lcurrent, i); /取当前检测元素的第i个排序码if (frontk=0) frontk=current; /第k个队列空,该元素为队头else Lreark.link=current; /不空,尾链接reark=current; /该元素成为新的队尾current=Lcurrent.link; /检测下一个元素j=0; /依次从各队列收集并拉链while(f
31、rontj=0) j+; /跳过空队列L0.link=current=frontj; /第j个队列的队头尾新链表的链头,last=rearj; /第j个队列的链尾for (k=j+1); kradix; k+) /连接其余的队列if (frontk!=0) /队列非空Llast.link=frontk;last=reark; /尾链接Llast.link=0; /新链表表尾 ,算法分析,在链表基数排序中,没有关键字的比较和移动,而只是顺链扫描链表和修改指针。设关键字基数为r,长度为d,将待排序序列初始化为一个静态链表的时间是O(n)。在每一趟排序中,清箱时间为 O(r),分配时需将n个记录放入
32、箱子,其时间是O(n)。因为要进行d趟排序,所以总的时间复杂度为O(d(n+r)。 链表基数排序特别适合于dn以及记录的数据量较大时的情况。 显然,基数排序是稳定的排序方法。,各位关键码的基数不一定相等,例如:卡片排序关键码是日期(年月日),方法1: 先按年分成若干堆,再按月份分成12堆,每个月再按天数分成小堆,然后按顺序年、月、日收集。这样的思路与手工排序的习惯方法相似,但用计算机实现要使用递归算法。,方法2: 先按日分类,再按月分类,最后按年分类,按日分类:,8日,26日,21日,19931026,1993726,1992526,1988126,按日从小到大收集,日数小的卡片放在最上面,1
33、996108,1988521,199518,19991021,1991721,19981021,1997521,199758,198918,1991121,10月,7月,5月,1月,1996108,199758,199518,198918,19931026,19991021,19981021,1991721,1993726,1992526,1988521,1997521,1991121,1988126,按月分类:注意这时卡片已经按日收集起来,因此分到每月的盒子中的最先装入的一定是日数小的卡片,按月从小到大收集。月数小的放在上面,1996108,199518,199758,198918,1988
34、521,19991021,1991721,19981021,1997521,1991121,19931026,1993726,1992526,1988126,1996108,199758,199518,198918,19931026,19991021,19981021,1991721,1993726,1992526,1988521,1997521,1991121,1988126,1988,1989,1991,1992,1993,1995,1996,1997,1998,1999,按年分类,按年从小到大收集,199518,198918,1991121,1988126,199758,1988521,
35、1997521,1992526,1991721,1993726,1996108,19991021,19981021,19931026,例 已知序列503,87,512,61,908,170,897,275,653,462。请给出采用基数排序法对该序列作升序排序时的每一躺扫描的结果。,初始: 503, 87,512, 61,908,170,897,275,653,462 1、 170, 61,512,462,503,653,275, 87,897,908 2、 503,908,512,653, 61,462,170,275, 87,897 3、 61, 87,170,275,462,503,51
36、2,653,897,908,计数排序 Counting Sort,计数排序是一个非基于比较的线性时间排序算法。它对输入的数据有附加的限制条件:,1、输入的线性表的元素属于有限偏序集S 2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。(即线性表的长度与集合中元素的取值范围成线性关系)当满足这两个条件时,计数排序的复杂性为O(n)。,计数排序算法的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放
37、在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。 假设输入的线性表L的长度为n,L=L1,L2,Ln;线性表的元素属于有限偏序集S,|S|=k且k=O(n),S=S1,S2,Sk;则计数排序算法可以描述如下:,1、扫描整个集合S,对每一个SiS,找到在线性表L中小于等于Si的元素的个数T(Si); 2、逆向扫描整个线性表L,对L中的每一个元素Li,将Li放在输出线性表的第T(Li)个位置上,并将T(Li)减1。,具体的实现过程如下: 在下面的计数排序算法中,我们假设L为输入的长度为n的线性表,输出的排
38、序结果存放在线性表R中。算法中还用到一个辅助表tmp用于对输入元素进行计数。,1 2 3 4 5 6 7 8,1 2 3 4 5 6,(a),(b),(c),(d),L,temp,(e),(f),(f),L,temp,1 2 3 4 5 6 7 8,(g),(h),1 2 3 4 5 6,(i),(j),i, j是整型数; tmp 是一个线性表; for (i=1;i=k;i+) tmpi= 0; for (j=1;j=n;j+) tmpLj+; /执行完上面的循环后,/tmpi的值是L中等于i的元素的个数for (i=2;i=k;i+)tmpi=tmpi+tmpi-1; /执行完上面的循环后
39、,/tmpi的值是L中小于等于i的元素的个数,for(j=n;j=1;j-) /这里是逆向遍历,/保证了排序的稳定性 RtmpLj = Lj; /Lj存放在输出数组R的第tmpLj个位置上tmpLj-; /tmpLj表示L中剩余的元素中小于/等于Lj的元素的个数 ,算法的第l个for循环是对数组tmp初始化。 第2个for循环检查每个输入元素。如果输入元素的键值为i,则tmpi增1。因此,在第2个for循环结束后,tmpi中存放着值等于i的输入元素个数,i=1,2,k。 算法的第3个for循环对每个i=1,2, 统计值小于或等于i的输入元素个数。 最后在第4个for循环中,将每个元素Lj存放到
40、输出数组R中相应的最终位置上。如果所有n个元素的值都不相同,则共有tmpLj个元素的键值小于或等于Lj,而小于Lj的元素有tmpLj-1个,因此tmpLj就是Lj在输出数组R中的正确位置。,算法分析,当输入元素有相同的值时,每将一个Lj存放到数组R时,tmpLj就减1,使下个值等于Lj的元素存放在输出数组R中存放元素Lj的前一个位置上。 计数排序算法的计算时间复杂性很容易分析。其中,第1个for循环需要O(k)时间;第2个for循环需要O(n)时间,第3个for循环需要O(k)时间;第4个for循环需要O(n)时间。这样,整个算法所需的计算时间为O(n+k)。当k=O(n)时,算法的计算时间复
41、杂性为O(n)。 我们看到,计数排序算法没有用到元素间的比较,它利用元素的实际值来确定它们在输出数组中的位置。因此,计数排序算法不是一个基于比较的排序算法,从而它的计算时间下界不再是O(nlog2n)。,另一方面,计数排序算法之所以能取得线性计算时间的上界是因为对元素的取值范围作了一定限制,即k=O(n)。如果k=n2,n3,,就得不到线性时间的上界。此外,我们还看到,由于算法第4个循环使用了循环控制变量递减的办法,经计数排序,输出序列中值相同的元素之间的相对次序与他们在输入序列中的相对次序相同,换句话说,计数排序算法是一个稳定的排序算法。,例 试利用计数排序的方法实现基数排序,在计数排序方法
42、中,要求线性表的长度与集合中元素的取值范围成线性关系。当线性表长度远远小于取值范围且待排序元素由多关键字组成时(如对500个数字进行排序,每个数字的取值范围在10万以内),则可按基数排序的原理,反复进行计数排序。 排序的过程为:首先对个位进行计数排序,然后对十位、百位、千位、。,/设待排序元素有n个,关键字基数为r,长度为d for (关键字中的每一位k) /从最低位开始初始化临时数组for (所有待排序元素)统计关键字k位各个可能取值的元素个数for (所有关键字k位的可能取值)计算每一个元素的存放位置 /从右向左的存放位置for (所有待排序元素)按计算好的位置从右向左重新排列 /算法总的
43、时间复杂度为:O(d(n+r),O(d),O(n),O(r),O(n),O(r),通过观看演示理解算法,7.8 内部排序算法的总结,排序问题的输入是一个线性表,该线性表的元素属于一个偏序集;要求对该线性表的元素做某种重排,使得线性表中除表尾外的每个元素外都小于等于(或大于等于)它的后继。 设R为非空集合A上的二元关系,如果对于每一个xA, R满足自反性( (x,x)R ),反对称性(x,y)R(y,x) Rx=y )和传递性(x,y)R(y,x)R(x,z)R),则称R为A上的偏序关系,记作。如果(x,y)R,则记作xy,读作“x小于等于y”。存在偏序关系的集合A称为偏序集。,如果一个排序算法
44、在排序后并不改变表中相同的元素原来的相对位置,那么这种排序算法叫做稳定排序(stable sort)算法。 排序的稳定性是指,对于相同的关键字,排序完成后他们的次序是否发生了改变,维持原状是稳定,否则就是不稳定的。实际上就是说,对一个多关键字序列,多次排序结果是否能够累积多次排序能否最终达到预期的有序。打个比方,首先我们按各门功课的总成绩学生排序得到一个序列,然后按照数学成绩排序,对于数学成绩相同的学生,我们希望总成绩好的排在前边(预期的有序),如果排序是不稳定的,就会破坏前面按照总成绩排好的序列,从而导致得不到最终的预期序列。注意到是“预期有序”,在排成绩的例子中,如果没有上述要求,排序是否
45、稳定就无关紧要了。,如何判断排序算法是否稳定呢?首先看一下是什么导致了“不稳定”。排序肯定有元素的移动(或者指针的修改),移动方法有平移(插入排序)、交换(冒泡排序)、重排(归并)。仔细观察一下,就会发现,不相邻位置记录的交换是导致不稳定的因素。这样一来,凡是有这个隐患的排序算法都是不稳定的,对于原来稳定的算法,如果采用了这样的交换策略,也会导致不稳定。,排序问题一般分为内排序( internal sorting )和外排序( external sorting )两类: 内排序:待排序的表中记录个数较少,整个排序过程中所有的记录都可以保留在内存中; 外排序:待排序的记录个数足够多,以至于他们必须存储在磁带、磁盘上组成外部文件,排序过程中需要多次访问外存。,为了对有n个元素的线性表进行排序,至少必须扫描线性表一遍以获取这n个元素的信息,因此排序问题的计算复杂性的下界为O(n)。 如果我们对输入的数据不做任何要求,我们所能获得的唯一信息就是各个元素的具体的值,我们仅能通过比较来确定输入序列的元素间次序。即给定两个元素ai和aj,通过测试aiaj 中的哪一个成立来确定ai和aj间的相对次序。这样的排序算法称为基于比较的排序算法。 可以证明,基于比较的排序算法的时间复杂度的下界为:O(nlog2n),