1 / 88

基本概念 插入排序 交换排序 选择排序 归并排序 基数排序 内部排序比较 外排序

第 9 章 排序. 基本概念 插入排序 交换排序 选择排序 归并排序 基数排序 内部排序比较 外排序. 9.1 排序的基本概念. 关键字 假设被排序的对象是由一组记录组成的文件,记录由若干个数据项 ( 或域 ) 组成,其中有一项可用来标识一个记录,称为 关键字项 。该数据项的值称为 关键字 (Key) 。 在不易产生混淆时,本章中将关键字项简称为关键字。 排序 所谓排序( Sort ),就是要整理文件中的记录,使它们按关键字递增 ( 或递减 ) 次序重新排列。

Télécharger la présentation

基本概念 插入排序 交换排序 选择排序 归并排序 基数排序 内部排序比较 外排序

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 第9章 排序 • 基本概念 • 插入排序 • 交换排序 • 选择排序 • 归并排序 • 基数排序 • 内部排序比较 • 外排序

  2. 9.1 排序的基本概念 • 关键字 假设被排序的对象是由一组记录组成的文件,记录由若干个数据项(或域)组成,其中有一项可用来标识一个记录,称为关键字项。该数据项的值称为关键字(Key)。在不易产生混淆时,本章中将关键字项简称为关键字。 • 排序 所谓排序(Sort),就是要整理文件中的记录,使它们按关键字递增(或递减)次序重新排列。 • 排序方法的稳定性在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,则称该排序方法是稳定的;否则,若具有相同关键字的记录之间的相对次序发生变化,则称该排序方法是不稳定的。

  3. 排序方法的分类 • 按是否涉及数据的内、外存交换分类: 外排序、内排序。 • 按策略划分内部排序方法: 插入排序、选择排序、交换排序、归并排序和基数排序等。 • 排序算法性能评价 • 评价排序算法好坏的标准主要有两条: (1)执行算法所需的时间; (2)执行算法所需的辅助空间。

  4. 不同存储方式的排序过程 • 以顺序表作为存储结构: 对记录本身进行物理重排。 • 以链表作为存储结构: 无须移动记录,仅需修改指针。 • 用顺序的方式存储待排序的记录,但同 时建立一个辅助表。 只需对辅助表的表目进行物理重排,适 用于难以在链表上实现,仍需避免排序 过程中移动记录的排序方法。

  5. 若无特别说明,则所讨论排序均为升序(即按递增排序),并以记录数组作为文件的存储结构。同时假定关键字是整数。记录数组的类型说明如下:若无特别说明,则所讨论排序均为升序(即按递增排序),并以记录数组作为文件的存储结构。同时假定关键字是整数。记录数组的类型说明如下: typedef int KeyType; typedef struct { KeyType key; InfoType otherinfo; }RecType; typedef RecType SeqList[n+1];

  6. 9.2 插入排序 基本原理:每步将一个待排序的记录,按其关键字大小,插入到前面已经排好序的一组记录适当位置上,直到记录全部插入为止。 • 直接插入排序 • 希尔排序

  7. 9.2.1 直接插入排序 基本思想:假设待排序的记录存放在数组R[1..n]中( R[1..n]表示数组元素的范围是从R[1]到R[n])。初始时,i=1,R[1]自成一个有序区,无序区为R[2..n]。然后,从i=2起直至i=n,依次将R[i]插入当前的有序区R[1..i-1]中,最后,生成含n个记录的有序区。

  8. 例9.1 直接插入排序举例 [初始关键字] [49] 38 65 97 76 13 27 49 j=2 (38) [38 49] 65 97 7613 27 49 j=3 (65) [38 49 65] 97 76 13 27 49 j=4 (97) [38 49 65 97] 76 13 27 49 j=5 (76) [38 49 65 76 97] 13 27 49 j=6 (13) [13 38 49 65 76 97] 27 49 j=7 (27) [13 27 38 49 65 76 97] 49 j=8 (49) [13 27 38 49 49 65 76 97] 监视哨R[0] 图9.1 直接插入排序过程示例

  9. 直接插入排序算法 void lnsertSort(SeqList R) { int i,j; for (i=2;i<n;i++) {R[0]=R[i]; j=i-1; while (R[0].key<R[j].key) {R[j+1]=R[j]; j- -; } R[j+1]=R[0]; } }

  10. 算法中引入附加记录R[0]有两个作用:  ①进入查找循环之前,它保存了R[i]的副本,使得不至于因记录的后移而丢失R[i]中的内容;算法中引入附加记录R[0]有两个作用:  ①进入查找循环之前,它保存了R[i]的副本,使得不至于因记录的后移而丢失R[i]中的内容;   ②在while循环“监视”下标变量j是否越界,一旦越界(即j=0),能自动控制while循环的结束,从而避免了在while循环内的每一次都要检测j是否越界(即省略了循环条件j>=1)。 因此,我们把R[0]称为“监视哨”。

  11. 直接插入排序的算法分析 • 直接插入排序算法由两重循环组成,对于有n个记录的排序,内循环表明完成一趟排序所需进行的记录关键字间的比较和记录的后移。 • 若初始时关键字递增有序,这是最好情况。每一趟排序中仅需进行一次关键字的比较,所以总的比较次数为n-1。在while循环之前和之中,至少要移动记录两次,所以总的比较次数为2(n-1)。 • 若初始时关键字递减有序,这是最坏情况。这时的记录比较和移动次数分别为: • 直接插入排序是一种稳定的排序方法。

  12. 9.2.2 希尔排序 • 1959年由D.L. Shell提出,又称缩小增量排序(Diminishing-increment sort) 。 • 基本思想:在直接插入排序中,只比较相邻的结点,一次比较最多把结点移动一个位置。如果对位置间隔较大距离的结点进行比较,使得结点在比较以后能够一次跨过较大的距离,这样就可以提高排序的速度。

  13. 希尔排序的基本过程 设待排序的记录序列有n个记录,首先取一个整数d1 <n作为间隔,将全部记录分为d1个子序列,所有距离为d1的记录放在同一个序列中,在每一个子序列中分别施行直接插入排序,然后缩小间隔d2,如取d2 = d1 /2,重复上述的子序列划分和排序工作,直到最后取dt= 1为止。

  14. 希尔排序示例 [初始关键字] 49 38 65 97 76 13 27 49 55 04 (增量为5) 一趟排序结果: 13 27 49 55 04 49 38 65 97 76 (增量为3) 二趟排序结果: 13 04 49 38 27 49 55 65 97 76 (增量为1) 三趟排序结果: 04 13 27 38 49 49 55 65 76 97 图9.2 希尔排序过程示例

  15. 希尔排序算法 void ShellPass(SeqList R,int d) { for(i=d+1;i<=n;i++) if(R[i].key<R[i-d].key) { R[0]=R[i];j=i-d; do { R[j+d]=R[j]; j=j-d; }while(j>0&&R[0].key<R[j].key); R[j+d]=R[0]; } } void ShellSort(SeqList R) { int increment=n; do { increment=increment/3+1; ShellPass(R,increment); }while(increment>1); }

  16. 为什么shell排序的时间性能优于直接插入排序呢?为什么shell排序的时间性能优于直接插入排序呢? 因为直接插入排序在初态为正序时所需时间最少,实际上,初态为基本有序时直接插入排序所需的比较和移动次数均较少。另一方面,当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度O(n2)差别不大。在shell排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但组内元素已经过多次排序,数组已经比较接近有序状态,所以新的一趟排序过程也较块。

  17. 希尔排序中增量di的取法 • Shell最初的方案是d1 = n/2, di+1 = di /2,直到dt =1。 • Knuth的方案是di+1 = di /3+1。 • 其它方案有:都取奇数为好;或di互质为好等等。

  18. 希尔排序的算法分析 • 对希尔排序的复杂度的分析很困难,在特定情况下可以准确地估算关键字的比较和记录移动次数,但是考虑到与增量之间的依赖关系,并要给出完整的数学分析,目前还做不到。 • Knuth的统计结论是,平均比较次数和记录平均移动次数在n1.25与1.6n1.25之间。 • 希尔排序的稳定性 希尔排序是一种不稳定的排序方法。见前例,未排序前,25在25* 之前,希尔排序完成后, 25在25* 之后,这就说明希尔排序是不稳定的。

  19. 9.3 交换排序 基本原理:两两比较待排序的记录的关键字,如果发生逆序,则交换之,直到全部记录都排好序为止。 两种常见的交换排序 • 冒泡排序 • 快速排序

  20. 9.3.1 冒泡排序 冒泡排序的基本思想 • 将被排序的记录数组R[1..n]垂直排列,每个记录R[i]看作是重量为R[i].key的气泡。根据轻气泡不能在重气泡之下的原则,从下往上(也可以从上往下)扫描数组R,凡扫描到违反此原则的轻气泡,就使其向上“飘浮”。如此反复进行,直到最后任何两个气泡都是轻者在上、重者在下为止。 • 初始时R[1..n]为无序区。第一趟扫描从无序区底部向上依次比较相邻的两个气泡的重量,若发现轻者在下、重者在上,则交换二者的位置。即依次比较(R[n],R[n-1]),(R[n-1],R[n-2]),…,(R[2],R[1]);对于每对气泡(R[j+1],R[j]),若R[j+1].key<R[j].key,则交换R[j+1]和R[j]的内容。第一趟扫描完毕时,“最轻”的气泡就飘浮到该区间的顶部,即关键字最小的记录被放在最高位置R[1]上。第二趟扫描扫描R[2..n]。扫描完毕时,“次轻”的气泡飘浮到R[2]的位置上。最后,经过n-1趟扫描可得到有序区R[1..n]。

  21. 例9.3 冒泡排序示例 49 13 13 13 13 13 13 13 38 49 27 27 27 27 27 27 65 38 49 38 38 38 38 38 97 65 38 49 49 49 49 49 76 97 65 4949494949 13 76 97 65 65 65 65 65 27 27 76 97 76 76 76 76 494949 76 97 97 97 97 初始 第一趟 第二趟 第三趟 第四趟 第五趟 第六趟最终排序 关键字 排序后 排序后 排序后 排序后 排序后 排序后,结果 已无记录交换, 冒泡排序终止! 图9.3 冒泡排序过程示例(由下向上比较)

  22. 冒泡排序算法 void BubbleSort(SeqList R) {int i,j,exchange; for(i=1;i<n;i++) { exchange=0; for(j=n-1;j>=i;j--) if(R[j+1].key<R[j].key) { R[0]=R[j+1]; R[j+1]=R[j]; R[j]=R[0]; exchange=1; } if(!exchange) break; } }

  23. 冒泡排序的算法分析 • 考虑关键字的比较次数和记录移动次数 • 在最好情况下,初始状态是递增有序的,一趟扫描就可完成排序,关键字的比较次数为n-1,没有记录移动。 • 若初始状态是反序的,则需要进行n-1趟扫描,每趟扫描要进行n-i次关键字的比较,且每次需要移动记录三次,因此,最大比较次数和移动次数分别为: • 冒泡排序方法是稳定的。

  24. 9.3.2 快速排序 快速排序的基本思想 • 快速排序方法是一种所需比较次数较少、在内部排序中速度比较快的排序方法。 • 其思想是在待排序的记录序列中任取某个记录(作为基准)的值作为控制值,采用某种方法把这个记录放到适当的位置,使得这个位置的左边的所有记录的值都小于或等于这个控制值,而这个位置的右边的所有记录的值都大于或等于这个控制值。

  25. 例9.4 (a) 一趟快速排序示例(一次划分过程) 49 38 65 97 76 13 27 49’ i j 38 65 97 76 13 27 49’ i j pivot 27 38 65 97 76 13 49’ 49 i j 27 38 97 76 13 65 49’ j i 27 38 13 97 76 65 49’ i j 27 38 13 76 97 65 49’ i j 27 38 13 49 76 49 65 49’ i j

  26. 例9.4 (b) 各趟排序后的状态 初始关键字 49 38 65 97 76 13 27 49’ 27 38 13 49 76 97 65 49’ 一趟排序之后 13 27 38 49 49’ 65 76 97 二趟排序之后 13 27 38 49 49’ 65 76 97 三趟排序之后 13 27 38 49 49’ 65 76 97 排序结果 图中,粉红色关键字表示基准关键字pivot.key。 方括号 表示划分后的区间。

  27. 快速排序算法 void QuickSort(SeqList R,int low,int high) { int pivotpos; if(low<high) { pivotpos=Partition(R,low,high); QuickSort(R,low,pivotpos-1); QuickSort(R,pivotpos+1,high); } }

  28. 划分算法 int Partition(SeqList R,int i,int j) {ReceType pivot=R[i]; while(i<j) {while(i<j&&R[j].key>=pivot.key) j--; if(i<j) R[i++]=R[j]; while(i<j&&R[i].key<=pivot.key) i++; if(i<j) R[j--]=R[i]; } //endwhile R[i]=pivot; return i; }

  29. 快速排序的算法分析 • 考虑关键字的比较次数和记录移动次数 • 最坏情况是每次划分选取的基准都是当前无序区 中关键字最小(或最大)的记录,划分的结果是基准的左边(或右边)为空,划分前后无序区的元素个数减少一个,因此,排序必须做n-1趟,每一趟中需做n-i次比较,所以最大比较次数为 • 最好情况是每次所取的基准都是当前无序区的“中值”记录,划分的结果是基准的左右两个无序子区的长度大致相等。

  30. 快速排序的记录移动次数不会大于比较次数,所以,快速排序的最坏时间复杂度为O(n2);最好时间复杂度为O(nlog2n)。快速排序的记录移动次数不会大于比较次数,所以,快速排序的最坏时间复杂度为O(n2);最好时间复杂度为O(nlog2n)。 快速排序的平均时间复杂度也是(nlog2n),空间复杂度为O(log2n)。 快速排序是不稳定的排序方法。例如: 初始关键字 22* 1 一趟排序后 1 2* 2 二趟排序后 1 2* 2 排序结果 1 2* 2

  31. 9.4 选择排序 基本思想:将待排序的记录分为已排序(初始为空)和未排序两组,依次将未排序的记录中关键字值最小的记录放入已排序的记录组的最后,直到记录直到排序完毕为止。 两种常见的选择排序 • 直接选择排序 • 堆排序

  32. 9.4.1 直接选择排序 直接选择排序的基本思想 • (1)在一组记录R[i]到R[n]中选择具有最小关键字的记录。 • (2)若它不是这组记录中的第一个记录,则将它与这组记录中的第一个记录对调。 • (3) 除去这个最小关键字的记录,在剩下的记录中重复(1)、(2)步,直到剩余记录只有一个为止。

  33. 例9.5 直接选择排序过程示例 初始关键字 49 38 65 97 76 13 27 49’ 第一趟排序后 13 38 65 97 76 49 27 49’ 13 27 65 97 76 49 38 49’ 第二趟排序后 13 27 38 97 76 49 65 49’ 第三趟排序后 13 27 38 49 76 97 65 49’ 第四趟排序后 13 27 38 49 49’ 97 65 76 第五趟排序后 13 27 38 49 49’ 65 97 76 第六趟排序后 第七趟排序后 得最后排序结果 13 27 38 49 49’ 65 76 97

  34. 直接选择排序算法 void SelectSort(SeqList R) { int i,j,k; for(i=1;i<n;i++) { k=i; for(j=i+1;j<=n;j++) if(R[j].key<R[k].key) k=j; if(k!=i) { R[0]=R[i];R[i]=R[k];R[k]=R[0];} } }

  35. 直接选择排序的算法分析 • 无论初始状态如何,在第i 趟排序中选择最小关键字的记录,需做n-i次比较,因此总的比较次数为: • 当文件为正序时,移动次数为0;当文件初态为反序时,每趟排序均要执行交换操作,总的移动次数取最大值3(n-1)。 • 直接选择排序的时间复杂度为O(n2)。 • 直接选择排序的空间复杂度为O(1)。 • 直接选择排序是不稳定的排序方法,见例 9.5。

  36. 9.4.2 堆排序 堆的定义: n个关键字序列称为堆,当且仅当该序列满足如下关系: 或 从堆的定义可以看出,堆实质上是满足如下性质的二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在的话)结点的关键字。

  37. 小根堆:根结点(即堆顶)的关键字是堆里所有结点关键字中最小者的堆,称为小根堆。小根堆:根结点(即堆顶)的关键字是堆里所有结点关键字中最小者的堆,称为小根堆。 10 15 56 25 30 70 10 15 56 25 30 70 小根堆示例

  38. 大根堆:根结点(即堆顶)的关键字是堆里所有结点关键字中最大者的堆,称为大根堆。大根堆:根结点(即堆顶)的关键字是堆里所有结点关键字中最大者的堆,称为大根堆。 70 56 30 25 15 10 70 56 30 25 15 10 大根堆示例

  39. 堆排序及其特点 • 堆排序是一种树型选择排序。 • 在排序过程中,将R[1]到R[n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)的记录。 • 堆排序分为两个步骤: 1、根据初始输入,形成初始堆。 2、通过一系列的记录交换和重新调整 进行排序。

  40. 堆排序算法 • 堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。 • 用大根堆排序的基本思想 • ①先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区; • ②再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key; • ③由于交换后新的根R[1]可能违反堆的定义,故应将当前无序区R[1..n-1]重新调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]重新调整为堆。…,直到无序区只有一个元素为止。

  41. 注意: ①只需做n-1趟排序,选出较大的n-1个关键字即可以使得文件递增有序。 ②用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻,堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止。

  42. 算法9.7 堆排序算法 void HeapSort(SeqList R) {/*对R[1..n]进行堆排序,不妨用R[0]做暂存单元*/ int i; BuildHeap(R);/*建堆函数,将R[1..n]建成初始堆*/ for(i=n;i>1;i--) {R[0]=R[1];R[1]=R[i];R[i]=R[0]; /*将堆顶和堆中最后一个记录交换*/ Heapify(R,1,i-1); /*调整堆函数,将R[1..i-1]重新调整为堆*/ } }

  43. ①调整堆函数Heapify( )的实现 每趟排序开始前R[l..i]是以R[1]为根的堆,在R[1]与R[i]交换后,新的无序区R[1..i-1]中只有R[1]的值发生了变化,故除R[1]可能违反堆性质外,其余任何结点为根的子树均是堆。因此,当被调整区间是R[low..high]时,只须调整以R[low]为根的树即可。 R[low]的左、右子树(若存在)均已是堆,这两棵子树的根R[2low]和R[2low+1]分别是各自子树中关键字最大的结点。若R[low].key不小于这两个孩子结点的关键字,则R[low]未违反堆性质,以R[low]为根的树已是堆,无须调整;否则必须将R[low]和它的两个孩子结点中关键字较大者进行交换,即:交换R[low]与R[large](R[large].key=max(R[2low].key,R[2low+1].key)) 。交换后又可能使结点R[large]违反堆性质,同样由于该结点的两棵子树(若存在)仍然是堆,故可重复上述的调整过程,对以R[large]为根的树进行调整。此过程直至当前被调整的结点已满足堆性质,或者该结点已是叶子为止。上述过程就象过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。因此,有人将此方法称为“筛选法”。

  44. 算法9.8 调整堆函数(用筛选法调整堆)。 void Heapify(SeqList R,int k,int m) {/*假设R[k..m]是以R[k]为根的完全二叉树,且分别以R[2k]和R[2k+1]为根的左、右子树*/ /*为大根堆,调整R[k],使整个序列R[k..m]满足堆的性质*/ RecType t=r[k];/*暂存“根”结点R[k]*/ x=r[k].key; i=k; j=2*i; while(j<=m) /*j≤m,R[j]是R[i]的左孩子 */ { if((j<m) && (R[j].key< R[j+1].key)) j=j+1; /*若存在右子树,且右子树根的关键字大,则沿右分支“筛选”*/ if(x<R[j].key) /*孩子结点的关键字较大*/ {R[i]=R[j]; /*将R[j]换到双亲位置上*/ i=j; /*修改当前被调整结点,并递推向下调整*/ j=2*i; } else break; /*调整完毕,退出循环*/ } R[i]=t;/*将最初的被调整结点R[k]填入到恰当的位置*/ } (输出堆顶元素并调整建新堆的过程请参见后面的堆排序的全过程示例)

  45. ②建堆函数BuildHeap()的实现 要将初始文件R[l..n]调整为一个大根堆,就必须将它所对应的完全二叉树中以每一结点为根的子树都调整为堆。显然只有一个结点的树是堆,而在完全二叉树中,所有序号 的结点都是叶子,因此以这些结点为根的子树均已是堆。这样,我们只需依次将以序号为 , -1,…,1的结点作为根的子树都调整为堆即可。

  46. 算法9.9 建立大根堆算法(建堆函数) Void BuildHeap(SeqList R) {/*利用筛选法将初始文件R[1..n]调整为一个大根堆*/ int i; for(i=n/2;i>=1;i--) Heapify(R,i,n); /*调整堆函数*/ }

  47. 例9.8 用筛选法建新堆示例。 已知关键字序列为 42,13,91,23, 24, 16,05,88,要求建立大根堆。因n=8,故从第4个结点开始调整。 42 13 91 23 24 16 05 88 42 13 91 23 24 16 05 88 (a) i=4,23筛下一层

  48. 42 13 91 88 24 16 05 23 42 13 91 88 24 16 05 23 (b) i=3,不调整

  49. 42 13 91 88 24 16 05 23 42 13 91 88 24 16 05 23 (c) i=2,13筛下两层

  50. 42 88 91 23 24 16 05 13 42 88 91 23 24 16 05 13 (d) i=1,42筛下一层

More Related