1、内容提要:本章重点讨论四类内排序方法,即插入排序、交换排序、选择排序和归并排序。每一类排序方法都基于同一种思想,在前三类排序方法中,首先介绍一种简单的排序方法,再讨论一种改进的排序方法。,学习重点: 1各种排序方法的基本思想;2各种排序算法的执行过程;3各种排序算法的时间复杂度及比较。,学习重点: 1快速排序、堆排序、归并排序等算法的设计; 2改进算法的时间复杂度的分析。,排 序 技 术,概 述,排序: 给定一组数据元素(或记录)的集合r1,r2,rn,其相应的关键码分别为k1,k2,kn,排序是将这些元素排列成顺序为rs1,rs2,rsn的一个序列,使得相应的关键码满足ks1ks2ksn(升
2、序)或ks1ks2ksn(降序)。 趟:在排序的过程中,基本动作执行一轮,即对待排序集合中的元素遍扫描一遍。,排 序 技 术,8.1.1 排序的基本概念,排序算法的稳定性:假定在待排序的记录集中,存在多个具有相同键值的记录,若经过排序,这些记录的相对次序仍然保持不变,即在原序列中,ki=kj,ij,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。,概 述,排 序 技 术,排序方法分为内排序和外排序两大类。内排序是指在排序的整个过程中,待排序的所有记录全部被放置在内存中;外排序是指由于待排序的记录个数太多,不能同时放置在内存,而需要将一部分记录放置在内存,另一部
3、分记录放置在外存上,整个排序过程需要在内外存之间多次交换数据才能得到排序的结果。,概 述,排 序 技 术,将排序方法分为基于比较的排序和不基于比较的排序,其中,基于比较的排序方法的实现主要是通过关键码的比较和记录的移动这两种操作,并且其最差时间下限已经被证明为(nlog2n) 内排序大致可分为插入排序、交换排序、选择排序、归并排序等四类;内排序还可分为两类:简单的排序方法,其时间复杂度为O(n2);先进的排序方法,其时间复杂度为O(nlog2n)。,概 述,排 序 技 术,概 述,排 序 技 术,对于内排序,在排序过程中通常需进行下列两种基本操作:比较:关键码之间的比较;移动:记录从一个位置移
4、动到另一个位置。 评价排序方法的另一个主要标准是执行算法所需要的辅助存储空间。辅助存储空间是指在数据规模一定的条件下,除了存放待排序记录占用的存储空间之外,执行算法所需要的其他存储空间。,8.1.2 排序算法的性能,插入排序,插入排序是一类藉助“插入”进行排序的方法,其主要思想是:每次将一个待排序的记录按其关键码的大小插入到一个已经排好序的有序序列中,直到全部记录排好序为止。,排 序 技 术,无序序列i+1n,有序序列1i,直接插入排序的基本思想是:依次将待排序序列中的每一个记录插入到一个已排好序的序列中,直到全部记录都排好序。,需解决的关键问题:,(1)如何构造初始的有序序列? (2)如何查
5、找待插入记录的插入位置,8.2.1 直接插入排序,直接插入排序过程示例,25,49,25*,思考r0的作用?,16,08,r 0 1 2 3 4 5 6,直接插入排序过程示例,思考r0的作用?, 将整个待排序的记录序列划分成有序区和无序区,初始时有序区为待排序记录序列中的第一个记录,无序区包括所有剩余待排序的记录; 将无序区的第一个记录插入到有序区的合适位置中,从而使无序区减少一个记录,有序区增加一个记录; 重复执行,直到无序区中没有记录为止。,关键问题(1)的解决方法,将第一个记录看成是初始有序表,然后从第二个记录起依次插入到这个有序表中,直到将第n个记录插入完毕。算法描述为: for (i
6、=2; i=n; i+) 插入第i个记录,即第i趟直接插入排序;,关键问题(2)的解决方法,一般情况下,在i-1个记录的有序区r1 ri-1中插入一个记录ri时,首先要查找ri的正确插入位置,然后将ri插入到相应位置。算法描述为:r0=ri; for (j=i-1; r0rj; j-) rj+1=rj;,r0有两个作用:一是进入查找插入位置的循环之前,它暂存了ri的值,使得不致于因记录的后移而丢失ri的内容;二是在查找插入位置的循环中充当“哨兵”。,void insertSort (int r , int n) for (i=2; i=n; i+) r0=ri; j=i-1;for (j=i-
7、1;r0rj;j-) rj+1=rj; j=j-1; rj+1=r0; ,直接插入排序算法,if riri-1,直接插入排序算法性能分析,最好情况下(正序):时间复杂度为O(n)。 最坏情况下(逆序或反序):时间复杂度为O(n2)。 平均情况下(随机排列):时间复杂度为O(n2) 。 需要一个记录的辅助空间。 直接插入排序算法是一种稳定的排序算法。算法简单、容易实现。适用于待排序记录基本有序或待排序记录较小时。,8.2.2 希尔排序,基本思想:先将整个待排序记录分割成若干个子序列,在子序列内分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插入排序。,改进的着眼点是:
8、 (1)若待排序记录按关键码基本有序时,直接插入排序的效率可以大大提高; (2)由于直接插入排序算法简单,则在待排序记录数量n较小时效率也很高。,需解决的关键问题,(1)应如何分割待排序记录,才能保证整个序列逐步向基本有序发展? (2)子序列内如何进行直接插入排序?,希尔插入排序过程示例,子序列的构成不能是简单地“逐段分割”,而是将相隔某个“增量”的记录组成一个子序列。 接下来的问题是增量应如何取?希尔最早提出的方法是d1=n/2,di+1=di/2。 算法描述为: for(d=n/2;d=1;d=d/2) 以d为增量,进行组内直接插入排序;,关键问题(1)的解决方法,关键问题(2)的解决方法
9、,在每个子序列中,待插入记录和同一子序列中的前一个记录比较,在插入记录ri时,自ri-d起往前跳跃式(跳跃幅度为d)搜索待插入位置,并且r0只是暂存单元,不是哨兵。当搜索位置0,表示插入位置已找到,在搜索过程中,记录后移也是跳跃d个位置。在整个序列中,前d个记录分别是d个子序列中的第一个记录,所以从第d+1个记录开始进行插入。,解决问题(2)的算法,for(i=d+1; i0 ,时间性能,对希尔排序算法的时间性能的分析是一个复杂的问题,因为它是所取“增量”的函数,而到目前为止尚未有人求得一种最好的增量序列。有人在大量实验的基础上指出,希尔排序的时间性能在O(n2)和O(nlog2n)之间,当n
10、在某个特定范围内,希尔排序所需的比较次数和记录的移动次数约为n1.3。,交换排序,主要思想:在待排序列中选两个记录,将它们的关键码相比较,如果反序(即排列顺序与排序后的次序正好相反),则交换它们的存储位置。,排 序 技 术,8.3.1 起泡排序,基本思想:两两比较相邻记录关键码,如果反序则交换,直到没有反序的记录为止。,65,65,55,起泡排序过程示例,65,55,50,具体的排序过程, 将整个待排序的记录序列划分成有序区和无序区,初始状态有序区为空,无序区包括所有待排序的记录。 对无序区从前向后依次将相邻记录的关键码进行比较,若反序则交换,从而使得关键码小的记录向前移,关键码大的记录向后移
11、(像水中的汽泡,体积大的先浮上来)。 重复执行,直到无序区中没有反序的记录。,需解决的关键问题, 在一趟起泡排序中,若有多个记录位于最终位置,应如何记载? 如何确定起泡排序的范围,使得已经位于最终位置的记录不参与下一趟排序? 如何判别起泡排序的结束?,问题的解决,设变量exchange记载记录交换的位置,则一趟排序后,exchange记载的一定是这一趟排序中记录的最后一次交换的位置,且从此位置以后的所有记录均已经有序。算法描述为:if (rjrj+1)rjrj+1;exchange=j; ,问题的解决,设bound位置的记录是无序区的最后一个记录,则每趟起泡排序的范围是r1 rbound。而在
12、一趟排序后,从exchange位置之后的记录一定是有序的,所以bound=exchange。算法描述为:bound=exchange; for (j=1; jrj+1)rjrj+1;exchange=j;,问题的解决,在每一趟起泡排序之前,令exchange的初值为0,在以后的排序过程中,只要有记录交换,exchange的值就会大于0。这样,在一趟比较完毕,就可以通过exchange的值是否为0来判别是否有记录交换,从而判别整个起泡排序的结束。,void BubbleSort(int r , int n) exchange=n; while (exchange) bound=exchange;
13、 exchange=0; for (j=1; jrj+1) rjrj+1;exchange=j; ,起泡排序算法,时间性能分析,最好情况:算法只执行一趟,进行了n-1次关键码的比较,不需要移动记录,时间复杂度为O(n); 最坏情况:每趟只浮出一个当前无序序列中最大的记录,故算法执行n-1趟,第i(1in)趟做了n-i次关键码的比较,执行了n-i次记录的交换,时间复杂度为O(n2)。 平均情况:时间复杂度为O(n2) 。,8.3.2 快速排序,改进的着眼点是:在起泡排序中,记录的比较和移动是在相邻单元中进行的,记录每次交换只能上移或下移一个单元,因而总的比较次数和移动次数较多。 在快速排序中,记
14、录的比较和移动是从两端向中间进行的,关键码较大的记录一次就能从前面移动到后面的单元,关键码较小的记录一次就能从后面移动到前面的单元,记录每次移动的距离较远,从而减少了总的比较次数和移动次数。,快速排序的基本思想,首先选一个轴值(即比较的基准),通过一趟排序将待排序记录分割成独立的两部分,前一部分记录的关键码均小于轴值,后一部分记录的关键码均大于或等于轴值,然后分别对这两部分重复上述方法,直到整个序列有序。,需解决的关键问题,如何选择轴值? 在每个子序列内如何实现快速排序(通常叫做一次划分)? 如何处理分区得到的两个待排序子序列? 如何判别快速排序的结束?,一次划分示例,问题的解决,选择轴值的方
15、法: (1)使用第一个记录的关键码;,(2)选取序列中间记录的关键码; (3)比较序列中第一个记录、最后一个记录和中间记录的关键码,取关键码居中的作为轴值并调换到第一个记录的位置; (4)随机选取轴值。,问题的解决,设待划分的序列是rs rt,设参数i,j分别指向子序列左、右两端的下标s和t,令rs为轴值, (1)j从后向前扫描,直到rjri,将rj移动到ri的位置,使关键码小(同轴值相比)的记录移动到前面去; (2)i从前向后扫描,直到rirj,将ri移动到rj的位置,使关键码大(同轴值比较)的记录移动到后面去; (3)重复上述过程,直到i=j,即指向同一位置。,整个快速排序的过程可递归进行
16、。若待排序列中只有一个记录,显然已有序,否则进行一趟快速排序后,再分别对分割所得的两个子序列进行快速排序(即递归处理)。,问题、的解决,13,27,50,38,49,55,j,i,i,j,13,65,27,50,38,49,55,快速排序的执行过程,65,void quickSort (int r , int first, int end ) /在序列 firstend中递归地进行快速排序if ( first end) pivotpos = partition (r, first, end ); quickSort (r, first, pivotpos-1);quickSort (r, pi
17、votpos+1, end ); ,快速排序算法,快速排序的时间性能分析,快速排序的趟数取决于递归的深度 。 最好情况:每一次划分对一个记录定位后,该记录的左侧子表与右侧子表的长度相同,为O(nlog2n)。 最坏情况(正序或逆序):每次划分只得到一个比上一次划分少一个记录的子序列(另一个子序列为空),为 O(n2)。 平均情况:为O(nlog2n)。,08 16 21 25 28 49,0 1 2 3 4 5,初始,16 21 25 28 49,08,21 25 28 49,08 16,25 28 49,08 16 21,28 49,08 16 21 25,49,08 16 21 25 28
18、,i = 1,i = 2,i = 3,i = 4,i = 5,快速排序退化的例子,选择排序,排 序 技 术,主要思想:每趟排序在当前待排序序列中选出关键码最小的记录,添加到有序序列中。,选出最小记录,有序序列,无序序列,一趟排序后,8.4.1 简单选择排序,基本思想: 第 i 趟在n-i+1(i=1,2,3,n-1)个记录中选取关键码最小的记录作为有序序列中的第i个记录。,需解决的关键问题,如何在待排序序列中选出关键码最小的记录? 如何确定待排序序列中关键码最小的记录在有序序列中的位置?,简单选择排序示例,最小者 08 交换21,08,最小者 16 交换25,16,最小者 21 交换49,21
19、,08,16,21,简单选择排序示例,最小者 25 交换25,28,25, 将整个记录序列划分为有序区和无序区,初始状态有序区为空,无序区含有待排序的所有记录。 在无序区中选取关键码最小的记录,将它与无序区中的第一个记录交换,使得有序区扩展了一个记录,而无序区减少了一个记录。 不断重复,直到无序区只剩下一个记录为止。此时所有的记录已经按关键码从小到大的顺序排列就位。,具体实现过程:,问题的解决,设置一个整型变量index,用于记录在一趟比较的过程中关键码最小的记录位置。,算法描述为:index=i; for (j=i+1; j=n; j+) if (rjrindex) index=j;,问题的
20、解决,第i趟简单选择排序的待排序区间是ri rn,则ri是无序区第一个记录,所以,将index所记载的关键码最小的记录与ri交换。,算法描述为:if (index!=i) ririndex;,void selectSort ( int r , int n) for ( i=1; irindex; ,简单选择排序算法,简单选择排序时间性能分析,移动次数: 最好情况(正序):0次; 最坏情况(反序):3(n-1)次。 比较次数: 无论记录的初始排列如何,所需进行的键值的比较次数相同,第i趟排序需进行n-i次键值的比较,而简单选择排序需进行n-1趟排序,则总的比较次数为:O(n2)。,需一个辅助空间
21、,是一种稳定的排序算法。,8.4.2 堆排序,改进的着眼点:如何减少关键码间的比较次数。若能利用每趟比较后的结果,也就是在找出键值最小记录的同时,也找出键值较小的记录,则可减少后面的选择中所用的比较次数,从而提高整个排序过程的效率。,堆的定义,堆是具有下列性质的完全二叉树:每个结点的值都小于或等于其左右孩子结点的值(称为小根堆)。(或每个结点的值都大于或等于其左右孩子结点的值,称为大根堆)。堆的根结点一定是所有结点的最小者; 将堆用顺序存储结构来存储,则堆对应一组序列。,堆的示例,18 29 19 40 32 36 22 60 50 38,堆排序的基本思想,首先将待排序的记录序列构造成一个堆,
22、此时,选出了堆中所有记录的最小者,然后将它从堆中移走,并将剩余的记录再调整成堆,这样又找出了次小的记录,以此类推,直到堆中只有一个记录为止,每个记录出堆的顺序就是一个有序序列。,需解决的关键问题,如何由一个无序序列建成一个堆(即初始建堆)? 如何处理堆顶记录? 如何调整剩余记录,成为一个新的堆(即重建堆)?,堆调整的问题,在一棵完全二叉树中,根结点的左右子树均是堆,如何调整根结点,使整个完全二叉树成为一个堆?,筛选法建堆,void sift ( int r , int k, int m ) /设当前要筛选的结点的编号为k,堆中最后一个结点的编号为m i=k; j=2*i; temp=ri; /
23、将筛选记录暂存while (jrj+1) j+;if (rirj) break; else ri=rj; i=j; j=2*i;ri=temp; /将筛选记录移到正确位置 ,初始建堆,21,25,28,49,16,08,1,2,3,4,5,6,i,21,25,28,16,08,49,i,21,16,28,08,25,49,i,08,16,28,25,21,49,08,16,28,25,21,49,49,16,28,25,21,08,16,25,28,49,21,08,输出堆顶 重建堆,堆排序算法,void heapSort ( int r, int n) for ( i=n/2 ; i=1;
24、i-) /初建堆sift ( r, i, n ) ; for ( i=1 ; in ; i+ ) /移走堆顶重建堆r1rn-i+1; sift ( r , 1 , n-i ); ,性能分析,初始建堆需要O(n)时间,第i次取堆顶记录重建堆需要用O(log2i)时间,并且需要取n-1次堆顶记录,因此整个时间复杂度为O(nlog2n),这是堆排序的最好、最坏和平均的时间代价。,8.5 归并排序,归并排序的主要思想是:将若干有序段逐步归并,最终归并为一个有序段。 二路归并排序的基本思想是:将一个具有n个待排序记录的序列看成是n个长度为1的有序序列,然后进行两两归并,得到n/2 个长度为2的有序序列,
25、再进行两两归并,得到n/4 个长度为4的有序序列,直至得到一个长度为n的有序序列为止。,需解决的关键问题,如何构造初始有序表? 如何将两个有序表合成一个有序表? 怎样完成一趟归并? 如何控制二路归并的结束?,二路归并排序示例,60 20 31 5 44 55 65,问题的解决,假设初始序列含有n个记录,则可将整个序列看成是长度为1个记录的n个有序表。,问题的解决,设两个相邻的有序表rs rm和rm+1 rt,将这两个有序表合成一个有序表r1s r1t。,5 20 31 60,44 55 65,5 20 31 44 55 60 65,i,j,k,有序表合成一个有序表算法,void merge (
26、int r , int r1 , int s, int m, int t ) i=s; j=m+1; k=s;while (i=m ,在一趟归并中,除最后一个有序表外,其它有序表中记录的个数相同,我们用长度h表示。现在的任务是把若干个相邻的长度为h的有序表和最后一个长度有可能小于h的有序表进行两两归并,把结果存放到r11 r1n中。设参数i指向待归并段的第一个记录,归并的步长是2h。,问题的解决,在归并过程中,有以下三种情况:,若in-2h+1,则表示相邻两个有序表的长度均为h,执行一次归并,完成后i加2h,准备进行下一次归并; 若in-h+1,则表示仍有两个相邻有序表,一个长度为h,另一个长
27、度小于h,则执行两个有序表的归并,完成后退出一趟归并。 若in-h+1,则表明只剩下一个有序表,直接将该有序表送到r1的相应位置,完成后退出一趟归并。,void mergePass (int r , int r1 , int n, int h)i=1;while (in-2h+1) merge (r, r1, i, i+h-1, i+2*h-1);i+=2*h;if (in-h+1) merge (r, r1, i, i+h-1, n);else for (k=i; k=n; k+) r1k=rk; ,一趟归并排序算法,问题的解决,开始时,有序表的长度h=1,结束时,有序表的长度为n,有序表的
28、长度来控制排序工作的结束。 void mergeSort (int r , int r1 , int n ) h=1;while (hn)mergePass (r, r1, h);h=2*h;mergePass (r1, r, h);h=2*h; ,性能分析,一趟归并排序的操作是将r1 rn中前后相邻的长度为h的有序表进行两两归并,并把结果存放到r11 r1n中。这需要耗费O(n)时间。整个归并排序需要进行log2n趟,因此,总的时间代价是O(nlog2n)。这也是归并排序算法的最好、最坏、平均的运行时间。 二路归并排序在算法执行时,需要占用与原始记录序列同样数量的存储空间。因此其空间复杂度为
29、O(n)。,8.6 各种排序方法的比较,从以下几个方面综合考虑: 时间复杂性; 空间复杂性; 稳定性; 算法简单性; 待排序记录个数n的大小; 记录本身信息量的大小; 关键码的分布情况。,各种排序算法时间复杂度的比较,空间复杂性,从空间复杂性看,所有排序方法归为三类: (1)归并排序单独属于一类,其空间复杂性为O(n); (2)快速排序单独属于一类,其空间复杂性为O(log2n)(但在最坏情况下为O(n); (3)其它排序方法归为一类,其空间复杂性为O(1)。,稳定性,所有排序方法可分为两类, (1)一类是稳定的,包括直接插入排序、起泡排序、直接选择排序和归并排序; (2)另一类是不稳定的,包
30、括希尔排序、快速排序和堆排序。,算法简单性,从算法简单性看, (1)一类是简单算法,包括直接插入排序、直接选择排序和起泡排序, (2)另一类是改进后的算法,包括希尔排序、堆排序、快速排序和归并排序,这些算法都很复杂。,待排序的记录个数n的大小,从待排序的记录个数n的大小看,n越小,采用简单排序方法越合适,n越大,采用改进的排序方法越合适。因为n越小,O(n2)同O(nlog2n)的差距越小,并且输入和调试简单算法比输入和调试改进算法要少用许多时间。,记录本身信息量的大小,记录本身信息量越大,移动记录所花费的时间就越多,所以对记录的移动次数较多的算法不利。,关键码的分布情况,当待排序记录按关键码有序时,插入排序和起泡排序能达到O(n)的时间复杂度;而对于快速排序而言,这是最坏的情况,此时的时间性能蜕化为O(n2),选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。,