1.19k likes | 1.3k Views
第 9 章 排序. 9.1 排序基本概念 9.2 插入排序 9.3 交换排序 9.4 选择排序 9.5 归并排序 9.6 基数排序 9.7 各种内排序算法的性能比较和选择 9.8 外排序. 9.1 排序基本概念. 1 .关键字项及关键字 记录中用来标识一个记录的数据项,称为关键字( Key )项,该数据项的值称为关键字。 2 .排序 排序( Sorting )又称分类,是指将一个数据元素的任意序列,重新排列成一个按关键字有序的序列。 3 .排序方法的稳定性
E N D
第9章 排序 9.1 排序基本概念 9.2 插入排序 9.3 交换排序 9.4 选择排序 9.5 归并排序 9.6 基数排序 9.7各种内排序算法的性能比较和选择 9.8 外排序
9.1 排序基本概念 1.关键字项及关键字 记录中用来标识一个记录的数据项,称为关键字(Key)项,该数据项的值称为关键字。 2.排序 排序(Sorting)又称分类,是指将一个数据元素的任意序列,重新排列成一个按关键字有序的序列。 3.排序方法的稳定性 序列中的对象r[i]和r[j], 在排序之后, r[i]与r[j]在序列中的前后位置不变, 则称这个排序方法是稳定的, 否则称这个排序方法是不稳定的。
9.1 排序基本概念 4.排序过程的基本操作 (1)比较两个关键字的大小; (2)改变指向记录的指针或移动记录本身的位置。 5.排序方法的分类 (1)按是否涉及数据的内、外存交换分 内部排序(Internal Sorting)(简称内排序); 外部排序(External Sorting)(简称外排序)。 (2)按策略划分内部排序方法 插入排序、交换排序、选择排序、归并排序、分配排序。
9.1 排序基本概念 6.待排文件的常用存储方式 (1)顺序表结构 (2)链表结构 (3)索引表结构
9.1 排序基本概念 7.排序算法性能评价 (1)评价排序算法好坏的标准 ① 执行时间和所需的辅助空间;② 算法的复杂程度。 (2) 排序算法的空间复杂度 就地排序:排序算法所需辅助空间不依赖于问题规模n,辅助空间是O(1)。非就地排序一般要求的辅助空间为O(n)。 (3) 排序算法的时间开销 大多数排序算法的时间开销主要是关键字之间的比较和记录的移动。有的排序算法其执行时间不仅依赖于问题的规模,还取决于输入实例中数据的状态。
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单元用作哨兵*/
9.2 插入排序 • 插入排序(Insertion Sort)的基本思想: 每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子文件中的适当位置,直到全部记录插入完成为止。 • 三种插入排序方法: • 直接插入排序 • 折半插入排序 • 希尔排序。
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.2.1 直接插入排序 2.第i-1趟直接插入排序 • 通常将一个记录R[i](i=2,3,…,n)插入到当前的有序区,使得插入后仍保证该区间里的记录是按关键字有序的操作称第i-1趟直接插入排序。 • 排序过程的某一中间时刻,R被划分成两个子区间R[1,…,i-1](已排好序的有序区)和R[i,…,n](当前未排序的部分,可称无序区)。
9.2.1 直接插入排序 • 直接插入排序基本操作:是将当前无序区的第i个记录R[i]插入到有序区R[1..i-1]中适当的位置上,使R[1, …,i]变为新的有序区。 • 该方法每次使有序区增加1个记录,称增量法。
9.2.1 直接插入排序 3.直接插入排序过程示例 设有一组关键字{19,09,13,27,16,08},其直接插入排序过程如下图9.1所示:
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) 的关键字进行比较:
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]*/
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*/
9.2.1 直接插入排序 ② 哨兵的作用 算法中引进的附加记录R[0]称监视哨或哨兵(Sentinel)。 哨兵有两个作用: 其一:进入查找(插入位置)循环之前,它保存了R[i]的副本;其二:在查找循环中“监视”下标变量j是否越界。 注意: (a) 一切为简化边界条件而引入的附加结点(元素)均可称为哨兵。 (b)引入哨兵使测试查找循环条件时间约减少一半,对于记录数较大的文件节约的时间相当可观。
9.2.1 直接插入排序 (4)算法分析 ① 算法的时间性能分析 :具有n个记录的文件进行n-1趟排序。各种状态下的时间复杂度:
9.2.1 直接插入排序 注意:初始文件按关键字递增有序,简称“正序”。初始文件按关键字递减有序,简称“反序”。 ② 算法的空间复杂度分析:辅助空间复杂度S(n)=O(1),是就地排序。 ③ 直接插入排序的稳定性:稳定的。
9.2.2 折半插入排序 1.基本思想 • 设在顺序表中有一个对象序列 R[0], R[1], …, R[n-1]。其中, R[0], R[1], …, R[i-1] 是已经排好序的对象。 • 在插入R[i] 时, 利用折半搜索法寻找R[i]的插入位置。 • 找到R[i]的插入位置后,直接插入即可。
9.2.2 折半插入排序 2.折半插入排序过程举例 设初始序列为空,用折半插入排序的方法,依次插入关键字序列{21,5,3,4,8,16},其折半插入排序过程如图9.2所示:
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; /*插入*/ } }
9.2.2 折半插入排序 4.折半插入排序算法分析 • 折半插入排序就平均性能来说比直接插入排序快。 • 它所需的排序码比较次数与待排序对象序列的初始排列无关, 仅依赖于对象个数。将 n 个对象(为推导方便, 设为 n=2k )用折半插入排序所进行的排序码比较次数为: • 当 n 较大时, 总排序码比较次数比直接插入排序的最坏情况要好得多, 但比其最好情况要差。 • 折半插入排序是一个稳定的排序方法。 • 折半插入排序的时间复杂度为O(n2)。
9.2.3 希尔排序 1.基本思想: • 设待排序对象序列有 n 个对象,首先取一个整数gap<n作为间隔,将全部对象分为gap 个子序列,所有距离为gap的对象放在同一个子序列中,在每一个子序列中分别施行直接插入排序。 • 然后缩小间隔 gap,例如取gap = gap/2,重复上述的子序列划分和排序工作。直到最后取gap = 1,将所有对象放在同一个序列中排序为止。 • 希尔排序方法又称为缩小增量排序。该方法实质上是一种分组插入方法。
9.2.3 希尔排序 2.给定实例的shell排序的排序过程 假设待排序文件有6个记录,其关键字分别是:21,25,49,25,16,08。增量序列的取值依次为:3,2,1,排序过程如图9.3所示:
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; }
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",以防下标越界。
9.3 交换排序 • 基本思想: 两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。 • 主要排序方法有: • 冒泡排序。 • 快速排序。
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]交换位置, 否则不进行交换。
9.3.1 冒泡排序 2.排序方法 基本过程为: • 初始, R[1..n]为无序区。 • 第一趟扫描。从无序区底部向上依次比较相邻的两个气泡的重量,若发现轻者在下、重者在上,则交换二者的位置。 • 第二趟扫描。扫描R[2..n]。扫描完毕时,"次轻"的气泡飘浮到R[2]的位置上…… • 最后,经过n-1 趟扫描可得到有序区R[1..n]。
9.3.1 冒泡排序 3.排序算法 (1)分析 • 因为每一趟排序都使有序区增加了一个气泡,在经过n-1趟排序之后,有序区中就有n-1个气泡,而无序区中气泡的重量总是大于等于有序区中气泡的重量,所以整个冒泡排序过程至多需要进行n-1趟排序。 • 若在某一趟排序中未发现气泡位置的交换,则说明待排序的无序区中所有气泡均满足轻者在上,重者在下的原则,因此,冒泡排序过程可在此趟排序后终止。
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; } }
9.3.1 冒泡排序 例:设有一组关键字[41,52,23,34,15,66,87,78], 这里n=8, 对它们进行冒泡排序。排序过程如图9.4所示。第4趟处理中, 关键字进行两两比较, 并未发生记录交换,这表明关键字已经有序, 因此不必要进行第5趟至第7趟处理。
9.3.1 冒泡排序 4.算法分析 • 算法的最好时间复杂度:若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值: Cmin=n-1 Mmin=0。 • 冒泡排序最好的时间复杂度为O(n)。 • 在最坏情况下,即初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种
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,(该位置之前的相邻记录均已有序)。
9.3.1 冒泡排序 下一趟排序开始时,R[1..lastExchange-1]是有序区,R[lastExchange..n]是无序区。 (2) 改变扫描方向的冒泡排序 ①冒泡排序的不对称性 ②造成不对称性的原因 ③改进不对称性的方法:在排序过程中交替改变扫描方向,可改进不对称性。
9.3.2 快速排序 就排序时间而言,快速排序被认为是一种最好的内部排序方法。 快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。 它采用了一种分治的策略,通常称其为分治法(Divide-and-Conquer Method)。 1. 算法思想 (1)分治法的基本思想 将原问题分解为若干个规模更小但结构与原 问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合
9.3.2 快速排序 为原问题的解。 (2)快速排序的基本思想 ①分解: 在R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)和R[pivotpos+1..high]。 ②求解: 通过递归调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。 ③组合:空操作。
9.3.2 快速排序 2.快速排序示例 例9.2 设有一组关键字{49,38, 65, 97,76,13,27,49}, 这里n=8。 试用快速排序方法由小到大进行排序。
9.3.2 快速排序 (1)分解操作函数hoare (2)递归求解
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); }
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);}
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); /*对右分区快速排序*/ } }
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]作为基准 快速排序是不稳定的。
9.4 选择排序 • 选择排序(Selection Sort)的基本思想: • 每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。 • 常用的选择排序方法: • 直接选择排序。 • 堆排序。
9.4.1 直接选择排序 • 直接选择排序(Straight Selection Sort)是一种简单直观的排序方法。 • 它的做法是:首先从待排序的所有记录中,选取关键字最小的记录,把它与第一个记录交换,然后在其余的记录中再选出关键字最小的记录与第二个记录交换,如此重复下去,直到所有记录排序完成。
9.4.1 直接选择排序 1. 直接选择排序的基本思想 ①初始状态:无序区为R[1..n],有序区为空。 ②第1趟排序 在无序区R[1..n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换。 ③第i趟排序 该趟排序从当前无序区中选出关键字最小的记录R[k],将它与无序区的第1个记录R[i]交换。
9.4.1 直接选择排序 2. 直接选择排序的过程 对初始关键字为{19、23、37、23、16、08}的文件进行直接选择排序的过程见图9.7。
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*/ }}
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)。 直接选择排序是不稳定的。
9.4.2 堆排序 • 堆排序(heap sort)是利用堆树(heap tree)来进行排序的一种方法,1964年由威洛姆斯(J. Willioms)提出。 1.堆树和堆的定义 堆树是一种特殊的二叉树,其具备如下特征: • 堆树是一棵完全二叉树; • 每一个结点的值都小于或等于它的两个子结点的值,即: ki≤K2i且ki≤K2i+1 (1≤i≤n/2); • 树根的值是堆树中最小的。 这是一个上小、底大的堆,也称为小根堆。若是一个上大、底小的堆,只需把“≤”改为“≥”即可。
9.4.2 堆排序 根据以上堆树的定义,如下图9.9中(a)、 (c)是堆, (b)、 (d)不是堆。