820 likes | 976 Views
第九章 排 序. 根据关键字将文件中的各记录按非递增或非递减次序排列起来. 学习要点:. 数据排序基本概念,内排序与外排序,排序的稳定性。 插入排序基本概念,直接插入排序和希尔排序。 交换排序基本概念,冒泡排序和快速排序。 选择排序基本概念,简单选择排序和堆排序。 归并排序基本概念,一次归并排序与二路归并排序。 外排序中败者树与 k 路归并算法。 各种排序方法的对比理解和性能分析 。. §9.1 数据排序. 9.1.1 排序基本概念
E N D
第九章 排 序 根据关键字将文件中的各记录按非递增或非递减次序排列起来
学习要点: • 数据排序基本概念,内排序与外排序,排序的稳定性。 • 插入排序基本概念,直接插入排序和希尔排序。 • 交换排序基本概念,冒泡排序和快速排序。 • 选择排序基本概念,简单选择排序和堆排序。 • 归并排序基本概念,一次归并排序与二路归并排序。 • 外排序中败者树与k路归并算法。 • 各种排序方法的对比理解和性能分析。
§9.1 数据排序 9.1.1排序基本概念 • 排序 给定一组数据记录集合:r1、r2、…、rn,对应排序码即选定的数据字段项分别为:k1、k2、…、kn。将上述数据记录重新排列成顺序为rs1、rs2、…、 rsn,使得对应排序码满足: ks1≤ks2≤…≤ksn的升序条件。这种重排一组数据记录而使其排序码值具非递减顺序的过程,就称为“排序”(sort)。
§9.1 数据排序 9.1.1 排序基本概念2 按照依据排序码个数划分,可以分为: • 单键排序 按照一个给定排序码进行排序。 • 多键排序 按照多个给定排序码进行排序。 按照数据记录放置在内存或外存,可以分为: • 内排序 是指待排记录序列全部存放在内存,整个排序过程也都在内存里完成; • 外排序 是指内存中容纳不下所有待排记录序列,排序过程中需要不断地与外存进行数据交换。
§9.1 数据排序 9.1.2 排序算法稳定性 • 若经某种排序之后,那些具有相同排序码值的记录与排序前的相对位置保持不变,那么称这种排序方法是“稳定的”,否则就是“不稳定的”。 分析各种算法要素: • 时间复杂度:即关键字的比较次数和移动次数 • 稳定性 • 特殊情况 • 适应场合
按排序方法分类: • 插入排序 • 交换排序 • 选择排序 • 归并排序
§9.2 插入排序 • 插入排序(insertion sort)就是一趟一个地将待排序记录插入到已经排好序的部分记录的适当位置中,使其成为一个新的有序序列,直到所有待排序记录全部插入完毕。 9.2.1 直接插入排序 • 排序过程中,每次将“被考察记录”依次与“比较范围”内的各个记录比较,插入到合适位置。 元素存储结构:
例子: • 已知一个排序表的排序码为:67,34,89,56,23,45,78,12,对其实施直接插入排序步骤如下:
例子(续): • 已知一个排序表的排序码为:67,34,89,56,23,45,78,12,对其实施直接插入排序步骤如下:
9.2.1 直接插入排序2 算法9-1 直接插入排序算法 00 Ins_Sort(DataType Ar[], int n) 01 { 02 int i,j; DataType temp; 03 for (i=1; i<n; i++) /* 总共要进行n−1次扫描 */ 04 { 05 temp = Ar[i]; /* 把被考察排序码存入临时单元 */ 06 j = i-1; /* j规定了这次比较范围的上界 */ 07 while ( (temp.key<Ar[j].key)&& (j>=0) ) 08 { 09 Ar[j+1] = Ar[j]; 10 j--; 11 } 12 Ar[j+1] = temp; /* 完成插入 */ 13 } 14 }
9.2.1 直接插入排序3 算法分析: 1、最好情况:正序O(n) 2、最坏情况:倒序 O(n2) 3、一般情况下,时间复杂度O(n2) 4、稳定性:稳定的(由内循环判断条件决定) 5、适应场合:比较移动次数多,适合规模小的排序
9.2.2 二分插入排序 • 在已排好序的子序列中确定“被考察记录”插入位置时,采用二分查找法确定该记录插入位置的插入排序方法称为二分插入排序(binary insertion sort)。 • 减少关键字比较次数,但不减少移动次数, • 时间复杂度O(n2)。
9.2.2 二分插入排序2 算法9-2 二分插入排序算法 00 Bin_Sort(DataType Ar[], int n) 01 { 02 int i,low,mid,high; 03 DataType temp; 04 for (i=2; i<=n; i++) 05 { 06 temp = Ar[i]; 07 low = 1; 08 high = i-1; 09 while (low<=high) 10 { 11 mid = (low+high)/2; 12 if (temp.key>Ar[mid].key) 13 low = mid+1; 14 else 15 high = mid -1; 16 } 17 for (j = i-1; j>=high+1; j--) 18 Ar[j+1] = Ar[j]; 19 Ar[high+1] = temp; 20 } 21 }
9.2.3 表插入排序 • 表插入排序(table insertion sort)的基本思想是不断地用被考察排序码与已排好序的子序列进行比较,着眼于减少移动数据开销,在记录的存储结构里增加了一个指针域,通过指针域的链接,让记录按照排序码的大小加以排列。 • 存储结构:
例子: • 设有排序表的排序码序列56,34,67,89,56,23,45,78,采用表插入排序方法完成其排序过程如下:
例子(续): • 设有排序表的排序码序列56,34,67,89,56,23,45,78,采用表插入排序方法完成其排序过程如下:
9.2.4 Shell排序 • Shell排序其排序点是先对待排记录序列先作“宏观”调整,再作“微观”调整。 • “宏观”调整就是以减少问题规模为目的,进行“跳跃式”的插入排序。 • 即将记录序列按一增量分成若干子序列,每个子序列分别进行插入排序。再缩小增量,重新分组,再组内排序;……直到增量为1,所有记录为一组。
例子: • 设待排序的8记录排序码序列为56,34,67,89,56,23,45,78,12,初始d=5,此后每趟取d=d/2,相应排序过程如下:
9.2.4 Shell排序2 算法9-4 shell排序算法 00 void shellsort(table *L) /*希尔排序*/ 01 { 02 int i,j,d; 03 d=L->length/2; 04 while ( d>=1 ) 05 { 06 for (i=d+1; i<=L->length; i=i+1) /*从第d+1个元素开始,将所有元素依次有序插入到相应的分组中*/ 07 { 08 L->data[0]=L->data[i]; /*保存第i个元素*/ 09 j=i-d; /*向前找插入位置*/
9.2.4 Shell排序2 算法9-4 shell排序算法2 10 while(j>0 && L->data[0].key<L->data[j].key) /*找插入位置并后移*/ 11 { 12 L->data[j+d]=L->data[j]; /*后移*/ 13 j=j-d; /*继续向前查找*/ 14 } 15 L->data[j+d]=L->data[0]; /*插入第i个元素的副本*/ 16 } 17 d=d/2; 18 } 19 }
9.2.4 Shell排序3 Shell排序分析: 1、每排一次序记录相对有序,减少比较次数和移动次数 2、稳定性:不稳定的 3、时间复杂度O(n1.3)
练习: 对数据 {46,74,18,53,14,6,40,38,86,65},写出其使用Shell排序时的每一趟排序结果。
§9.3 交换排序 • 交换排序(swap sort)就是不断地将待排序列中的两两记录做排序码比较,若发现它们的大小次序相反,就进行交换,直到所有记录的排序码都满足排序要求时为止。 • 基本特征是将键值较大的向排序表的一端移动,而将键值较小的向另一段移动。
§9.3 交换排序2 9.3.1 冒泡排序 • 对n个记录的排序码序列进行n−1次扫描。每次扫描时,都从下到上对相邻的两个排序码进行比较,如果不符合由小到大的顺序,就将它们交换位置。(小的往上冒,大的往下沉)
例子: 33 24 46 18 80 25 63 初始值 24 33 18 46 25 63 80 i=1 24 18 33 25 46 63 80 i=2 18 24 25 33 46 63 80 i=3 18 24 25 33 46 63 80 i=4
9.3.1 冒泡排序2 算法9-5 冒泡排序算法 00 Bub_Sort(DataType Ar[], int n) 01 { 02 int i,flag; 03 KeyType key; 04 for (i=1; i<n; i++) /* 对无序记录序列进行n−1趟扫描 */ 05 { 06 flag = 0; /* 这一趟是否发生交换的标志 */ 07 for (j=1; j<=n-i; j++) /* 这趟扫描的范围是从1到n−i */ 08 if (Ar[j].key>Ar[j+1].key) 09 {……(交换); 13 flag = 1; 14 } 15 if (flag == 0) /* 若没有发生交换,就结束算法 */ 16 break; 17 } 18 }
9.3.1 冒泡排序3 冒泡排序分析: 1、循环(n-1)趟即排好n个数字,效率O(n2) 2、最坏情况:倒序 3、最好情况:正序 4、稳定性:稳定的(关键看发生交换的条件) 5、适应场合:问题规模不大。 6、改良:一趟中全无交换即可认为全有序
练习: 对数据『18,6,20,10,17,24』进行冒泡排序,写出其第三趟排序结果。
9.3.2 快速排序 • 在排序表的n个排序码序列中,选择一个基准元素x作为“枢轴(Pivot)”;然后把所有小于等于x的排序码调整到x的左边,把大于x的排序码调整到x的右边,这被称为是快速排序的一次划分。 • 继续对左、右两个部分重复进行相同的这种划分过程,每次划分后就会让作为枢轴的排序码位于它最终应该在的位置上,直到最后分割的每一部分都只有一个排序码时就结束整个排序过程。
例子: • 将排序码序列56,34,67,89,56,23,45,78,48存放在一维数组Ar[]中。使用快速排序一次划分算法完成一次划分的过程如下:
例子(续): • 将排序码序列56,34,67,89,56,23,45,78,48存放在一维数组Ar[]中。使用快速排序一次划分算法完成一次划分的过程如下: 一次划分结果:
9.3.2 快速排序2 算法9-6 快速排序一次划分算法 00 Qukpass_Sort(DataType Ar[], int s, int t) 01 { 02 int low,high; 03 DataType temp; 04 low = s; /* low为划分时序列的左边界 */ 05 high = t; /* high为划分时序列的右边界 */ 06 temp = Ar[s]; /* 把枢轴值暂存于temp */ 07 while (low<high) 08 { 09 while ( (high>low) && (temp.key<Ar[high].key)) /* 把大于枢轴的排序码留在右边的子序列里 */ 10 high--;
9.3.2 快速排序2 算法9-6 快速排序一次划分算法2 11 if (Ar[high].key<=temp.key) /* 把小于等于枢轴的移到左边 */ 12 { 13 Ar[low] = Ar[high]; 14 low++; 15 } 16 while ((low<high) && (Ar[low].key<=temp.key)) /* 把小于枢轴的排序码留在左边的子序列里 */ 17 low++; 18 if (temp.key<=Ar[low].key) /* 把大于枢轴的排序码移到右边 */ 19 { 20 Ar[high] = Ar[low]; 21 high--; 22 } 23 } 24 Ar[low]=temp; /* 把枢轴值存入正确位置 */ 25 }
9.3.2 快速排序3 算法9-7 快速排序算法 00 Quk_Sort(DataType Ar[], int s, int t) 01 { 02 int i; 03 if (s<t) 04 i = Qukpass_Sort(Ar, s, t); 05 Quk_Sort(Ar, s, i-1); 06 Quk_Sort(Ar, i+1, t); 07 }
例子: • 有待排排序码序列56,34,67,89,56,23,45,78,48,对其排序码序列实施快速排序算法的整个过程如下:
9.3.2 快速排序4 快速排序分析: 1、原始文件无序,杂乱无章时是最快的一种排序 2、最坏情况:原始文件有序 O(n2) 3、一般情况下,需栈深度log2n,时间复杂度O(n*log2n) 4、稳定性:不稳定的 5、适应场合:原始文件杂乱无章,对稳定性无要求
练习: 对数据 {18,6,20,10,17,24} 进行快速排序,写出其第一趟排序结果。
上机: 键盘输入数列,编写程序进行一趟快速排序,并输出结果。 • 1、键盘输入一个数组 • 2、l=0;h=n-1; • 3、while (l<h) 循环: • 1)while (l<h):从h向前找,直到有数比a[l]小; • 2)if (l<h):a[l]与a[h]交换,l++; • 3)while (l<h):从l向后找,直到有数比a[h]大; • 4)if (l<h):a[l]与a[h]交换,h--; • 4、输出一趟快速排序后的结果。
§9.4 选择排序 • 选择排序(selection sort)就是在操作过程第1趟中从n个待排序的排序表里选出键值最小者,第2趟从n−1个待排序的排序表中选出次小者,如此反复进行直到整个排序结束。 9.4.1 简单选择排序 • 每趟过程中将待排序表中最小排序码放到最前,使得排序表规模比上一次小1。
例子: 35 26 37 28 96 45 37 • i=0:【26】 35 37 28 96 45 37 • i=1:【26 28】 37 35 96 45 37 • i=2:【26 28 35】 37 96 45 37 • i=3:【26 28 35 37】 96 45 37 • i=4:【26 28 35 37 37】 45 96 • i=5:【26 28 35 37 37 45】 96
9.4.1 简单选择排序2 算法9-8 直接选择排序算法 00 Sel_sort(DataType Ar[], int n) 01 { 02 int i,small; 03 DataType temp; 04 for (i=0; i<n-1; i++) /* i控制n−1趟扫描 */ 05 { 06 small = i; /* 用变量small记住当前最小排序码的位置 */ 07 for (j = i+1; j<n; j++) /* j控制这趟扫描的比较范围 */ 08 if (Ar[j].key<Ar[small].key) 09 small = j; 10 if (small != i) /* small与比较范围首元素下标不同,交换 */ 11 { 12 …… ;Ar[i]与Ar[small]交换; 15 } 16 } 17 }
9.4.1 简单选择排序3 简单选择排序算法: 1、无最好、最坏情况:时间复杂度均为 O(n2) 2、稳定性:不稳定的(包含远距离的交换位置) 3、适应场合:适合规模小且对稳定性无要求的排序
上机: 键盘输入杂乱数列,采用简单选择排序方法编写程序使之有序,并输出结果。
9.4.2 堆排序 1、堆的基本概念 • 设有n个记录的排序码序列:k1、k2、…、kn,若下述条件成立时,就称其为一个小顶堆:ki≤k2i,并且ki≤k2i+1(i=1,2,…,n/2,且2i+1≤n)。 • 对于n个记录的排序码序列:k1、k2、…、kn,若下述条件成立时,就称其为一个大顶堆: ki≥k2i,并且ki≥k2i+1(i=1, 2, …, n/2, 且2i+1≤n)。 换言之: • 小顶堆:完全二叉树,非叶子结点均不大于其左右孩子的值。 • 大顶堆:完全二叉树,非叶子结点均不小于其左右孩子的值。
9.4.2 堆排序 1、堆的基本概念2 小顶堆例子: 堆的特点: • 在一个堆里,k1(即根结点)是堆中最小的排序码值; • 堆的任何一棵子树本身也是一个堆; • 堆中任一结点的排序码值都不大于左、右两个孩子的排序码值,但在左、右孩子的排序码值之间没有大小关系存在。
9.4.2 堆排序2 2、堆排序 • 堆排序(heap sort)基本思想是在(小顶)堆中,首先输出根结点k1(根结点排序码值k1为堆中最小),然后通过一定规则对剩余结点进行调整,使之再成为一个堆。再输出新的根结点,如此下去,最终达到由小到大的排序目标。
89 23 34 23 56 34 45 56 56 67 45 56 56 67 89 12 c ( ) b ( ) 例子: • 将小根堆『 12,34,23,56,56,67,45,89 』排序。 方法:交换输出调整交换输出调整……(直到全无元素) 交换输出 调整
例子(续): • 将小根堆『 12,34,23,56,56,67,45,89 』排序。 交换输出 调整 交换输出
例子(续): • 将小根堆『 12,34,23,56,56,67,45,89 』排序。 调整 交换输出 调整
例子(续): • 将小根堆『 12,34,23,56,56,67,45,89 』排序。 交换输出 调整 交换输出 调整 交换输出