1 / 119

第 9 章 排序

第 9 章 排序. 9.1 排序基本概念 9.2 插入排序 9.3 交换排序 9.4 选择排序 9.5 归并排序 9.6 基数排序 9.7 各种内排序算法的性能比较和选择 9.8 外排序. 9.1 排序基本概念. 1 .关键字项及关键字 记录中用来标识一个记录的数据项,称为关键字( Key )项,该数据项的值称为关键字。 2 .排序 排序( Sorting )又称分类,是指将一个数据元素的任意序列,重新排列成一个按关键字有序的序列。 3 .排序方法的稳定性

Download Presentation

第 9 章 排序

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章 排序 9.1 排序基本概念 9.2 插入排序 9.3 交换排序 9.4 选择排序 9.5 归并排序 9.6 基数排序 9.7各种内排序算法的性能比较和选择 9.8 外排序

  2. 9.1 排序基本概念 1.关键字项及关键字 记录中用来标识一个记录的数据项,称为关键字(Key)项,该数据项的值称为关键字。 2.排序 排序(Sorting)又称分类,是指将一个数据元素的任意序列,重新排列成一个按关键字有序的序列。 3.排序方法的稳定性 序列中的对象r[i]和r[j], 在排序之后, r[i]与r[j]在序列中的前后位置不变, 则称这个排序方法是稳定的, 否则称这个排序方法是不稳定的。

  3. 9.1 排序基本概念 4.排序过程的基本操作 (1)比较两个关键字的大小; (2)改变指向记录的指针或移动记录本身的位置。 5.排序方法的分类 (1)按是否涉及数据的内、外存交换分 内部排序(Internal Sorting)(简称内排序); 外部排序(External Sorting)(简称外排序)。 (2)按策略划分内部排序方法 插入排序、交换排序、选择排序、归并排序、分配排序。

  4. 9.1 排序基本概念 6.待排文件的常用存储方式 (1)顺序表结构 (2)链表结构 (3)索引表结构

  5. 9.1 排序基本概念 7.排序算法性能评价 (1)评价排序算法好坏的标准 ① 执行时间和所需的辅助空间;② 算法的复杂程度。 (2) 排序算法的空间复杂度 就地排序:排序算法所需辅助空间不依赖于问题规模n,辅助空间是O(1)。非就地排序一般要求的辅助空间为O(n)。 (3) 排序算法的时间开销 大多数排序算法的时间开销主要是关键字之间的比较和记录的移动。有的排序算法其执行时间不仅依赖于问题的规模,还取决于输入实例中数据的状态。

  6. 9.1 排序基本概念 8.文件的顺序存储结构表示 定义 9.1 #define n l00 /*假设的文件长度,即待排序的记录数目*/ typedef int KeyType; /*假设的关键字类型*/ typedef struct{ /*记录类型*/ KeyType key; /关键字项*/ InfoType otherinfo; /*其它数据项,类型InfoType依赖于具体应用而定义*/ }RecType; typedef RecType SeqList[n+1];/*SeqList为顺序表类型,表中第0单元用作哨兵*/

  7. 9.2 插入排序 • 插入排序(Insertion Sort)的基本思想: 每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中的适当位置,直到全部记录插入完成为止。 • 三种插入排序方法: • 直接插入排序 • 折半插入排序 • 希尔排序。

  8. 9.2.1 直接插入排序 1.基本思想 假设待排序的记录存放在数组R[1..n]中。初始时,R[1]自成1个有序区,无序区为R[2..n]。从i=2起直至i=n为止,依次将R[i]插入当前的有序区R[1..i-1]中,生成含n个记录的有序区。当插入第i (i  1) 个对象时, 前面的R[0], R[1], …, R[i-1]已经排好序。这时, 用R[i]的排序码与R[i-1], R[i-2], …的排序码顺序进行比较, 找到插入位置即将R[i]插入, 原来位置及其后的对象依次向后顺移。

  9. 9.2.1 直接插入排序 2.第i-1趟直接插入排序 • 通常将一个记录R[i](i=2,3,…,n)插入到当前的有序区,使得插入后仍保证该区间里的记录是按关键字有序的操作称第i-1趟直接插入排序。 • 排序过程的某一中间时刻,R被划分成两个子区间R[1,…,i-1](已排好序的有序区)和R[i,…,n](当前未排序的部分,可称无序区)。

  10. 9.2.1 直接插入排序 • 直接插入排序基本操作:是将当前无序区的第i个记录R[i]插入到有序区R[1..i-1]中适当的位置上,使R[1, …,i]变为新的有序区。 • 该方法每次使有序区增加1个记录,称增量法。

  11. 9.2.1 直接插入排序 3.直接插入排序过程示例 设有一组关键字{19,09,13,27,16,08},其直接插入排序过程如下图9.1所示:

  12. 9.2.1 直接插入排序 4.直接插入排序方法 (1)简单方法 在当前有序区R[1..i-1]中查找R[i]的正确插入位置k(1≤k≤i-1);然后将R[k..i-1]中的记录均后移一个位置,在k位置上插入R[i]。若R[i]的关键字大于等于R[1..i-1]中所有记录的关键字,则R[i]就插入原位置。 (2)改进的方法(反序的方法) 将待插入记录R[i]的关键字从右向左依次与有序区中记录R[j](j=i-1,i-2,…,1) 的关键字进行比较:

  13. 9.2.1 直接插入排序 ① 若R[j]的关键字大于R[i]的关键字,则将R[j]后移一个位置; ② 若R[j]的关键字小于或等于R[i]的关键字,则查找过程结束,j+1即为R[i]的插入位置。 (3)直接插入排序算法 ① 算法描述 void InsertSort(RecType R[],int n) { /*对顺序表R中的记录R[1..n]按递增序进行插入排序*/ int i,j; for(i=2;i<=n;i++) /*依次插入R[2],…,R[n]*/

  14. 9.2.1 直接插入排序 if(R[i].key<R[i-1].key) {/*若R[i].key大于等于有序区中所有的keys,则R[i]应在原有位置上*/ R[0]=R[i];j=i-1; /*R[0]是哨兵,且是R[i]的副本*/ do {//从右向左在有序区R[1..i-1]中查找R[i]的插入位置 R[j+1]=R[j]; /*将关键字大于R[i].key的记录后移*/ j-- ; }while(R[0].key<R[j].key); /*当R[i].key≥R[j].key时终止*/ R[j+1]=R[0]; /*R[i]插入到正确的位置上*/ } } /*endif*/

  15. 9.2.1 直接插入排序 ② 哨兵的作用 算法中引进的附加记录R[0]称监视哨或哨兵(Sentinel)。 哨兵有两个作用: 其一:进入查找(插入位置)循环之前,它保存了R[i]的副本;其二:在查找循环中“监视”下标变量j是否越界。 注意: (a) 一切为简化边界条件而引入的附加结点(元素)均可称为哨兵。 (b)引入哨兵使测试查找循环条件时间约减少一半,对于记录数较大的文件节约的时间相当可观。

  16. 9.2.1 直接插入排序 (4)算法分析 ① 算法的时间性能分析 :具有n个记录的文件进行n-1趟排序。各种状态下的时间复杂度:

  17. 9.2.1 直接插入排序 注意:初始文件按关键字递增有序,简称“正序”。初始文件按关键字递减有序,简称“反序”。 ② 算法的空间复杂度分析:辅助空间复杂度S(n)=O(1),是就地排序。 ③ 直接插入排序的稳定性:稳定的。

  18. 9.2.2 折半插入排序 1.基本思想 • 设在顺序表中有一个对象序列 R[0], R[1], …, R[n-1]。其中, R[0], R[1], …, R[i-1] 是已经排好序的对象。 • 在插入R[i] 时, 利用折半搜索法寻找R[i]的插入位置。 • 找到R[i]的插入位置后,直接插入即可。

  19. 9.2.2 折半插入排序 2.折半插入排序过程举例 设初始序列为空,用折半插入排序的方法,依次插入关键字序列{21,5,3,4,8,16},其折半插入排序过程如图9.2所示:

  20. 9.2.2 折半插入排序 3.折半插入排序算法(算法9.2 折半插入排序) typedef int SortData; void BinInsSort( SortData R[], int n ) { SortData temp; int Low, High, Mid; for(i = 1; i < n; i++) {Low = 0; High = i-1; temp = R[i]; while ( Low <= High ) { Mid = ( Low + High )/2; if( temp.data < R[Mid].data ) High = Mid - 1; else Low = Mid + 1; } for(k = i-1; k >= Low; k-- ) R[k+1] = R[k]; /*记录后移*/ R[Low] = temp; /*插入*/ } }

  21. 9.2.2 折半插入排序 4.折半插入排序算法分析 • 折半插入排序就平均性能来说比直接插入排序快。 • 它所需的排序码比较次数与待排序对象序列的初始排列无关, 仅依赖于对象个数。将 n 个对象(为推导方便, 设为 n=2k )用折半插入排序所进行的排序码比较次数为: • 当 n 较大时, 总排序码比较次数比直接插入排序的最坏情况要好得多, 但比其最好情况要差。 • 折半插入排序是一个稳定的排序方法。 • 折半插入排序的时间复杂度为O(n2)。

  22. 9.2.3 希尔排序 1.基本思想: • 设待排序对象序列有 n 个对象,首先取一个整数gap<n作为间隔,将全部对象分为gap 个子序列,所有距离为gap的对象放在同一个子序列中,在每一个子序列中分别施行直接插入排序。 • 然后缩小间隔 gap,例如取gap = gap/2,重复上述的子序列划分和排序工作。直到最后取gap = 1,将所有对象放在同一个序列中排序为止。 • 希尔排序方法又称为缩小增量排序。该方法实质上是一种分组插入方法。

  23. 9.2.3 希尔排序 2.给定实例的shell排序的排序过程 假设待排序文件有6个记录,其关键字分别是:21,25,49,25,16,08。增量序列的取值依次为:3,2,1,排序过程如图9.3所示:

  24. 9.2.3 希尔排序 3.Shell排序的算法实现 算法 9.3a 希尔排序算法 SeqList ShellPass(SeqList R,int d) {/*希尔排序中的一趟排序,d为当前增量*/ for(i=d+1;i<=n;i++) /*将R[d+1..n]分别插入各组当前的有序区*/ if(R[i].key<R[i-d].key) {R[0]=R[i]; /*R[0]只是暂存单元,不是哨兵*/ j=i-d; do /*查找R[i]的插入位置*/ { R[j+d]=R[j]; /*后移记录*/  j=j-d; /*查找前一记录*/ }while(j>0&&R[0].key<R[j].key); R[j+d]=R[0]; /*插入R[i]到正确的位置上*/ } Return R;   } 

  25. 9.2.3 希尔排序 3.Shell排序的算法实现 算法 9.3b 希尔排序算法 SeqList  ShellSort(SeqList R) {int increment=n; /*增量初值,不妨设n>0*/ do {increment=increment/3+1; /*求下一增量*/ ShellPass(R,increment);/*一趟增量为increment的Shell插入排序*/ }while(increment>1) Return R;     } 注意:当增量d=1时,ShellPass和InsertSort基本一致,只是由于没有哨兵而在内循环中增加了一个循环判定条件"j>0",以防下标越界。

  26. 9.3 交换排序 • 基本思想: 两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。 • 主要排序方法有: • 冒泡排序。 • 快速排序。

  27. 9.3.1 冒泡排序 • 冒泡排序(bubble sort)是一种人们熟知的、最简单的交换排序方法。 • 在排序过程中, 关键字较小的记录经过与其它记录的对比交换, 像水中的气泡向上冒出一样, 移到序列的首部, 故称此方法为冒泡排序法。 1.冒泡排序的算法思路 让j取n至2, 将r[j].key与r[j-1].key比较, 如果r[j].key<r[j-1].key, 则把记录r[j]与r[j-1]交换位置, 否则不进行交换。

  28. 9.3.1 冒泡排序 2.排序方法 基本过程为: • 初始, R[1..n]为无序区。 • 第一趟扫描。从无序区底部向上依次比较相邻的两个气泡的重量,若发现轻者在下、重者在上,则交换二者的位置。 • 第二趟扫描。扫描R[2..n]。扫描完毕时,"次轻"的气泡飘浮到R[2]的位置上…… • 最后,经过n-1 趟扫描可得到有序区R[1..n]。

  29. 9.3.1 冒泡排序 3.排序算法 (1)分析 • 因为每一趟排序都使有序区增加了一个气泡,在经过n-1趟排序之后,有序区中就有n-1个气泡,而无序区中气泡的重量总是大于等于有序区中气泡的重量,所以整个冒泡排序过程至多需要进行n-1趟排序。 • 若在某一趟排序中未发现气泡位置的交换,则说明待排序的无序区中所有气泡均满足轻者在上,重者在下的原则,因此,冒泡排序过程可在此趟排序后终止。

  30. 9.3.1 冒泡排序 (2)具体算法(算法9.4 冒泡排序算法) void BubbleSort(SeqList R) {/*R(l..n)是待排序的文件,采用自下向上扫描,对R做冒泡排序 bool exchange; /*交换标志*/ for(i=1;i<n;i++) /*最多做n-1趟排序*/      { exchange= false; /*本趟排序开始前,交换标志应为假*/ for(j=n-1;j>=i;j--) /*对当前无序区R[i..n]自后向前扫描*/ if(R[j+1].key<R[j].key) /*交换记录,R[0] 做暂存单元*/ {R[0]=R[j+1];R[j+1]=R[j];R[j]=R[0]; exchange=true;/*发生了交换,故将交换标志置为真*/ } if(!exchange) /*本趟排序未发生交换,提前终止算法*/ return; }     }

  31. 9.3.1 冒泡排序 例:设有一组关键字[41,52,23,34,15,66,87,78], 这里n=8, 对它们进行冒泡排序。排序过程如图9.4所示。第4趟处理中, 关键字进行两两比较, 并未发生记录交换,这表明关键字已经有序, 因此不必要进行第5趟至第7趟处理。

  32. 9.3.1 冒泡排序 4.算法分析 • 算法的最好时间复杂度:若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值: Cmin=n-1        Mmin=0。 • 冒泡排序最好的时间复杂度为O(n)。 • 在最坏情况下,即初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种

  33. 9.3.1 冒泡排序 情况下,比较和移动次数均达到最大值: Cmin =n(n-1)/2=O(n2)        Mmin =3n(n-1)/2=O(n2)冒泡排序的最坏时间复杂度为O(n2)。 • 算法的平均时间复杂度为O(n2)。 • 冒泡排序是就地排序,它是稳定的。 5.算法改进 (1)记住最后一次交换发生位置lastExchange的冒泡排序 在每趟扫描中,记住最后一次交换发生的位置lastExchange,(该位置之前的相邻记录均已有序)。

  34. 9.3.1 冒泡排序 下一趟排序开始时,R[1..lastExchange-1]是有序区,R[lastExchange..n]是无序区。 (2) 改变扫描方向的冒泡排序 ①冒泡排序的不对称性 ②造成不对称性的原因 ③改进不对称性的方法:在排序过程中交替改变扫描方向,可改进不对称性。

  35. 9.3.2 快速排序 就排序时间而言,快速排序被认为是一种最好的内部排序方法。 快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。 它采用了一种分治的策略,通常称其为分治法(Divide-and-Conquer Method)。 1. 算法思想 (1)分治法的基本思想 将原问题分解为若干个规模更小但结构与原 问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合

  36. 9.3.2 快速排序 为原问题的解。 (2)快速排序的基本思想 ①分解: 在R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)和R[pivotpos+1..high]。 ②求解: 通过递归调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。 ③组合:空操作。

  37. 9.3.2 快速排序 2.快速排序示例 例9.2 设有一组关键字{49,38, 65, 97,76,13,27,49}, 这里n=8。 试用快速排序方法由小到大进行排序。

  38. 9.3.2 快速排序 (1)分解操作函数hoare (2)递归求解

  39. 9.3.2 快速排序 3.快速排序算法 (1)分解操作算法(算法9.5 霍尔排序) int hoare(struct node r[MAXSIZE],int f,int h) { int i,j,pivot; i=f; j=h; pivot=r[i]; do { while((i<j) && (r[j].key>=pivot.key)) j--; if (i<j) {r[i]=r[j]; i++; } while((i<j) && (r[i].key<=pivot.key)) i++; if (i=j) {r[j]=r[i];j--; } }while(i<j); r[i]=pivot; return(i); }

  40. 9.3.2 快速排序 3.快速排序算法 (2)快速排序的递归算法和非递归算法 算法9.6 快速排序的非递归算法 void quicksort1(struct node r[MAXSIZE], int n) {int f=1;h=n;tag=1;top=0; do { if(f<h) { i=hoare(r,f,h); top++; s[top][0]=i+1; s[top][1]=h; h=i-1; } else { l=s[top][0]; h=s[top][1]; top--; } }while(tag==1);}

  41. 9.3.2 快速排序 算法9.7 快速排序的递归算法 void quicksort2(struct node r,int f,int h) { if(f<h) {i=hoare(r,f,h); /*划分两个区*/ quicksort2(r,f,i-1); /*对左分区快速排序*/ quicksort2(r,i+1,h); /*对右分区快速排序*/ } }

  42. 9.3.2 快速排序 4.算法分析 (1)算法的时间复杂度 快速排序的最坏时间复杂度应为0(n2),最好时间复杂度为O(nlog2n)。它的平均时间复杂度为O(nlog2n)。 (2)空间复杂度 最坏情况下,递归树的高度为O(n),所需的栈空间为O(n). (3)基准关键字的选取 ①“三者取中”的规则。 ②取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准 快速排序是不稳定的。

  43. 9.4 选择排序 • 选择排序(Selection Sort)的基本思想: • 每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。 • 常用的选择排序方法: • 直接选择排序。 • 堆排序。

  44. 9.4.1 直接选择排序 • 直接选择排序(Straight Selection Sort)是一种简单直观的排序方法。 • 它的做法是:首先从待排序的所有记录中,选取关键字最小的记录,把它与第一个记录交换,然后在其余的记录中再选出关键字最小的记录与第二个记录交换,如此重复下去,直到所有记录排序完成。

  45. 9.4.1 直接选择排序 1. 直接选择排序的基本思想 ①初始状态:无序区为R[1..n],有序区为空。 ②第1趟排序 在无序区R[1..n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换。 ③第i趟排序 该趟排序从当前无序区中选出关键字最小的记录R[k],将它与无序区的第1个记录R[i]交换。

  46. 9.4.1 直接选择排序 2. 直接选择排序的过程 对初始关键字为{19、23、37、23、16、08}的文件进行直接选择排序的过程见图9.7。

  47. 9.4.1 直接选择排序 3.算法描述 算法9.8 直接选择排序算法 void SelectSort (RecType R[],int n) {int i,j,k; for(i=1;i<n;i++) /*做第i趟排序(1≤i≤n-1)*/ {k=i; for(j=i+1;j<=n;j++)/*在当前无序区R[i..n]中选key最小的记录R[k]*/ if(R[j].key<R[k].key)   k=j;/*k记下目前找到的最小关键字所在的位置*/   if(k!=i)/*交换R[i]和R[k]*/ { R[0]=R[i];R[i]=R[k];R[k]=R[0];/*R[0]作暂存单元*/ }/*endif*/ }}

  48. 9.4.1 直接选择排序 4. 算法分析 (1)关键字比较次数 总的比较次数为: (n-1)+(n-2)++2+1+0=n(n-1)/2=O(n2) (2)记录的移动次数 • 当这组对象的初始状态是按其关键字从小到大有序(正序)的时候,对象的移动次数Rmin =0,达到最少。 • 文件初态为反序时,每趟排序均要执行交换操作,总的移动次数取最大值3(n-1)。 • 直接选择排序的平均时间复杂度为O(n2)。 直接选择排序是不稳定的。

  49. 9.4.2 堆排序 • 堆排序(heap sort)是利用堆树(heap tree)来进行排序的一种方法,1964年由威洛姆斯(J. Willioms)提出。 1.堆树和堆的定义 堆树是一种特殊的二叉树,其具备如下特征: • 堆树是一棵完全二叉树; • 每一个结点的值都小于或等于它的两个子结点的值,即: ki≤K2i且ki≤K2i+1 (1≤i≤n/2); • 树根的值是堆树中最小的。 这是一个上小、底大的堆,也称为小根堆。若是一个上大、底小的堆,只需把“≤”改为“≥”即可。

  50. 9.4.2 堆排序 根据以上堆树的定义,如下图9.9中(a)、 (c)是堆, (b)、 (d)不是堆。

More Related