360 likes | 470 Views
10.4 归并排序 基本思想 把 k(k ≥2)个有序子文件合并在一起,形成一个新的有序 文件。 同时归并 k 个有序子文件的排序过程称为 k- 路归并排序。 2-路归并排序 ---归并2个有序子文件的排序。 例. 将有序文件 A 和 B 归并为有序文件 C。 A =(2,10,15,18,21,30) B =(5,20,35,40) 按从小至大的次序从 A 或 B 中依次取出2,5,10,15,...,40, 顺序归并到 C 中,得: C =(2,5,10,15,18,20,21,30,35,40).
E N D
10.4 归并排序 基本思想 把k(k≥2)个有序子文件合并在一起,形成一个新的有序 文件。 同时归并k个有序子文件的排序过程称为k-路归并排序。 2-路归并排序---归并2个有序子文件的排序。 例. 将有序文件A和B归并为有序文件C。 A=(2,10,15,18,21,30) B=(5,20,35,40) 按从小至大的次序从A或B中依次取出2,5,10,15,...,40, 顺序归并到C中,得: C=(2,5,10,15,18,20,21,30,35,40)
一般地,2-路归并过程为: 假定文件r[low..high]中的相邻子文件(子表) (r[low],r[low+1],...,r[mid])和(r[mid+1],...,r[high]) 为有序子文件,其中:low≤mid<high 。 将这两个相邻有序子文件归并为有序文件y[low..high],即: (y[low],y[low+1],...,y[high]) r[9..16] 例 y[9..16] 9 10 11 12 13 14 15 16 9 10 11 12 13 14 15 16 i→ k→ 有序 子表 2-路归并 有序文件(表) j→ 有序 子表
将两个有序子文件归并为有一个有序文件的算法 将两个有序子文件归并为有一个有序文件的算法 void merge(r,y,low,mid,high) RecType r[],y[];int low,mid,high; { int k=i=low,j=mid+1; while (i<=mid && j<=high) { if (r[i].key<=r[j].key) { y[k]=r[i];i++;}//归并前一个子文件的记录 else { y[k]=r[j];j++;} //归并后一个子文件的记录 k++;} while (j<=high) //归并后一个子文件余下的记录 { y[k]=r[j]; j++; k++;} while (i<=mid) //归并前一个子文件余下的记录 { y[k]=r[i]; i++; k++;} } // merge
2-路归并排序 假定文件(r[1],r[2],...,r[n])中记录是随机排列的,进行 2-路归并排序,首先把它划分为长度均为1的n个有序子文件, 然后对它们逐步进行2-路归并排序。其步骤如下: 第1趟:从r[1..n]中的第1个和第2个有序子文件开始,调用 算法merge,每次归并两个相邻子文件,归并结果放到y[1..n]中。 在y中形成 n/2 个长度为2的有序子文件。若n为奇数,则y中最 后一个子文件的长度为1。 第2趟:把y[1..n]看作输入文件,将 n/2 个有序子文件两 两归并,归并结果回送到r[1..n]中,在r中形成 n/2/2个长度为4的有序子文件。若y中有奇数个子文件,则r中最后一个子文 件的长度为2。 ...... 共计经过 log2n 趟归并,最后得到n个记录的有序文件。
例1. 对8个记录作2路归并排序,共进行log28=3趟归并。 y[1..8] y[1..8] r[1..8] r[1..8] 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 第1趟 第2趟 第3趟
y[1..8] r[1..8] y[1..8] r[1..8] r[1..8] 1 2 3 4 5 6 7 8 9 10 11 1 2 3 4 5 6 7 8 9 10 11 1 2 3 4 5 6 7 8 9 10 11 1 2 3 4 5 6 7 8 9 10 11 1 2 3 4 5 6 7 8 9 10 11 例2. 对11个记录作2-路归并排序,进行log211=4趟归并。 第2趟 第1趟 第3趟 第4趟
一趟归并排序算法: void mergepass(r,y,s) // s为子文件的长度 RecType r[],y[];int s;//将r中的子文件归并到y中 { int i=1; while(i+2*s-1<=n) //两两归并长度均为s的子文件 { merge(r,y,i,i+s-1,i+2*s-1); i=i+2*s; } if (i+s-1<n) //最后两个子长度为s和长度不足s的文件 merge(r,y,i,i+s-1,n); else while(i<=n) //复制最后一个子文件,长度≤s { y[i]=r[i]; i++; } }
调用算法mergepass,对文件r[1..n]归并排序的算法调用算法mergepass,对文件r[1..n]归并排序的算法 void mergesort(RecType r[],int n) { RecType y[n+1]; int s=1; //子文件初始长度为1 while (s<n) { mergepass(r,y,s); //将r[1..n]归并到y[1..n] s=2*s; //修改子文件长度 mergepass(y,r,s); //将y[1..n]归并到r[1..n] s=2*s; //修改子文件长度 } } 算法分析 ● 对n个记录的文件进行归并排序,共需 log2n趟,每趟 所需比较关键字的次数不超过n, 共比较O(nlog2n)次。 ●每趟移动n个记录, 共移动O(nlog2n)个记录。 ● 归并排序需要一个大小为n的辅助空间y[1..n]。 ● 归并排序是稳定的。
10.5 交换排序 10.5.1 冒泡排序 基本思想: 设待排序的文件为r[1..n] 第1趟(遍):从r[1]开始,依次比较两个相邻记录的关键字 r[i].key和r[i+1].key,若r[i].key>r[i+1].key,则交换记录 r[i]和r[i+1]的位置;否则,不交换。 (i=1,2,...n-1) 第1趟之后,n个关键字中最大的记录移到了r[n]的位置上。 第2趟:从r[1]开始,依次比较两个相邻记录的关键字 r[i].key和r[i+1].key,若r[i].key>r[i+1].key,则交换记录 r[i]和r[i+1]的位置;否则,不交换。 (i=1,2,...n-2) 第2趟之后,前n-1个关键字中最大的记录移到了r[n-1]的位 置上。 ...... 作完n-1趟,或者不需再交换记录时为止。
例: 第1趟 第2趟 第3趟 第4趟 ┌─────────┐ ┌─┐ ┌─┐ ┌─┐ k1= 43 21 21 21 21 21 21 21 21 15 15 15 k2= 21 43 43 43 43 43 43 15 15 21 21 21 k3= 89 89 89 15 15 15 15 28 28 28 28 28 k4= 15 15 15 89 28 28 28 43 43 4343 43 k5= 28 28 28 28 89 43 43 4343 43 43 43 k6= 43 43 43 43 43 8989 8989 89 89 89 比较次数=5+4+3+2=14 交换记录的次数=3+2+1=6,移动记录次数=3*6=18
算法分析 ● 最好情况: 待排序的文件已是有序文件,只需要进行1趟 排序,共计比较关键字的次数为 n-1 不交换记录。 ●最坏情况: 要经过n-1趟排序,所需总的比较关键字的次 数为 (n-1)+(n-2)+...+1=n(n-1)/2 交换记录的次数最多为 (n-1)+(n-2)+...+1=n(n-1)/2 移动记录次数最多为 3n(n-1)/2 。 ●只需要少量中间变量作为辅助空间。 ● 算法是稳定的。
冒泡排序算法 // 对n个整数按递增次序作冒泡排序 Void bubble1(int a[],int n) { int i,j,temp; for(i=0;i<n-1;i++) //作n-1趟排序 for(j=0;j<n-1-i;j++) if (a[j]>a[j+1]) { temp=a[j]; //交换记录 a[j]=a[j+1]; a[j+1]=temp; } for(i=0;i<n;i++) printf("%d",a[i]); //输出排序后的元素 }
改进的冒泡排序算法 void bubblesort(RecType r[],int n) { int i,j,swap; RecType temp; j=1; //置比较的趟数为1 do{ swap=0; //置交换标志为0 for (i=1;i<=n-j;i++) { if (r[i].key>r[i+1].key) { temp=r[i]; //交换记录` r[i]=r[i+1]; r[i+1]=temp; swap=1; //置交换标志为1 } j++; //作下一趟排序 } } while (j<n && swap); } //未作完n-1趟,且标志为1
10.5.2 快速排序 基本思想:首先在r[1..n]中,确定一个r[i],经过比较和 移动,将r[i]放到"中间"某个位置上,使得r[i]左边所有记录 的关键字小于等于r[i].key,r[i]右边所有记录的关键字大于等 于r[i].key。以r[i]为界,将文件划分为左、右两个子文件。 用同样的方法分别对这两个子文件进行划分, 得到4个更小 的子文件。继续进行下去,使得每个子文件只有一个记录为止, 便得到原文件的有序文件。 例. 给定文件(20,05,37,08,63,12,59,15,44,08),选 用第1个元素20进行划分: x ↑ ↑ i j
x ↑ ↑ i j x 例 ↑ ↑ ↑ ↑ j i x ↑ ↑ ↑ ↑ i j x ↑ ↑ i j
x ↑ ↑ ↑ ↑ i j 例 x ↑ ↑ ↑ ↑ i j x ↑ ↑ i j x ↑ ↑ 左子文件 右子文件 i j
快速排序 void quksort(r,low,high) RecType r[];int low,high; { RecType x;int i,j; if (low<high) //有两个以上记录 { i=low;j=high;x=r[i]; //保存记录到变量x中 do{ while (i<j && r[j].key>x.key) j--; //j向左端扫描 if (i<j) //i,j未相遇 { r[i]=r[j]; i++; while(i<j && r[i].key<x.key) i++; //i向右端扫描 if (i<j) { r[j]=r[i];j--; } } } while (i!=j); //i,j未相遇
r[i]=x; //划分结束 quksort(r,low,i-1); //递归处理左子文件 quksort(r,i+1,high); //递归处理右子文件 } } 对文件r[1..n]快速排序 void quicksort(RecType r[],int n) { quksort(r,1,n); }
算法分析 ● 就平均速度而言,快速排序是已知内部排序方法中最好 的一种排序方法,其时间复杂度为O(nlog(n))。 ●但是,在最坏情况下,快速排序所需的比较次数和冒泡 排序的比较次数相同,其时间复杂度为O(n2)。 ●快速排序需要一个栈作辅助空间,用来实现递归处理左、 右子文件。在最坏情况下,递归深度为n,因此所需栈的空间大 小为O(n)数量级。 ● 快速排序是不稳定的。
10.6 基数排序 基数排序的一般过程: 将任一关键字K看做一个d元组:K=(K1,K2, … ,Kd) 其中:K1是最高位, Kd是最低位,按组成关键字的每一位的值进行排序。 排序过程(以十进制正整数为例): 设文件有n个记录(R1,R2, … ,Rn),记录Ri的关键字为Ki,且Ki是不超d位的非负整数。 ⅰ:建立十个队列,编号分别为0,1,2, … ,9 ⅱ:重复执行d遍 ⒈分配:若Ki【d】=j,则Ri放入第j号队列(j=0,1,…,9) ⒉收集:按0,1, … ,9的次序将各队列中的记录收集和排列起来。 ⒊ d=d-1
队列 例:设有关键字序列: 分配
队列 队列 收集 分配
队列 收 集 问题:各队列的长度 如何确定?
A[ ] 例2:已知 n=12, d=2,r=10(12个2位的十进制整数,低位优先) COUNT [0..9] 各队列中的记录个数 分配 收集 B[ ]
0 1 2 3 4 5 6 7 8 9 10 11 a[ ] 分配 count[0..9] 收集 b[ ] 0 1 2 3 4 5 6 7 8 9 10 11
参考程序如下: for (i=0,d=1;i<k; i++,d*=r) { for ( j=0; j<r; j++) count[j]=0; //初始化 for( j=0; j<n; j++) count[ a[j] / d % r]++; //统计各队列中的记录个数 for ( j=1; j<r; j++) count[j]=count[j-1]+count[j]; //分配 for( j=n-1; j>=0; j--) b[--count[a[j] / d % r]]=a[j]; //收集 for( j=0; j<n; j++) a[j]=b[j]; } 注: d — 1为个位,10为十位,… k — 整数的最大位数; r — 数值的基数(如八进制数,十进制数等); n —关键字个数。
内排序小结 内排序方法 ●插入排序:直接插入排序; ●选择排序:简单选择/选择排序;堆排序 ●归并排序 2-路归并排序 k-路归并排序 ● 交换排序 冒泡排序: 快速排序 ● 基数排序 最低位优先法
.什么是排序,什么是排序的稳定性 排序算法分析 (1)时间复杂度 ● 对n个记录排序,所需比较关键字的次数; 最好情况;最坏情况;平均情况 ● 对n个记录排序,所需移动记录的次数; 最好情况;最坏情况;平均情况 (2)空间复杂度 排序过程中,除文件中的记录所占的空间外, 所需的辅助存储空间的大小。
第11章 外部排序 • 外部排序思想 设磁盘有4500个记录的文件,磁盘的读∕写单位是250个记录的数据块,内存只能提供1500个记录的空间,试对此文件进行排序。 解:ⅰ⑴ 从磁盘文件输入三个数据块,计750个记录至内存,排序后再写入磁盘; ⑵ 按⑴同样的方法得如下归并段: R1 R2 R3 R4 R5 R6 1~750 751~1500 1501 ~2250 2251 ~3000 3001 ~ 3750 3751 ~ 4500
R1 R2 磁盘(1500个记录) 部分有序 ⅱ ⑴归并R1,R2 BF1 BF2 内存区 3块计750个记录 BF3 磁盘(1500个记录) 整体有序 R12
R1 R2 R3 R4 R5 R6 ⅱ ⑵ 按⑴同样得方法得:R34,R56,R1234,R123456 R12 R34 R56 1遍 1500 1500 1500 2/3遍 R1234 3000 1遍 R123456 4500个记录整体有序
建立初始归并段要把全部记录(4500)读写一遍,完成全部记录的归并要把(4500)个记录读写8/3遍。下面分析三路归并的情况:建立初始归并段要把全部记录(4500)读写一遍,完成全部记录的归并要把(4500)个记录读写8/3遍。下面分析三路归并的情况: R1 R2 R3 R4 R5 R6 完成全部记录的归并要把(4500)个记录读写2遍。
结论:M个初始归并段进行K路归并时,归并的趟数为:S = log(K(M)) 证明:设M为K的整幂次,即M=K**s (s为整数) 第一趟归并得 k**(s-1)个为归并段; 第二趟归并得 k**(s-2)个为归并段; … 第s趟归并得 k**s-s=1个归并段; 由m=k**s ,有log(k(m))=s 当m不是k的整幂次时,归并趟数是 log(k(m))的上界函数log(K(M))证毕 上式说明:要s ,只有K 或m
权值愈小的结点,离根愈近? • 最佳归并树 设有8个初始归并段,其长度(以外存物理块为单位)分别为2,3,6,9,12,17,18,24,求其3路最佳归并树,并求其WPL和对外存的访问次数。 解1: 2 3 6 9 11 12 17 18 24 32 59 0 91 WPL=(2+3+6)*3+(9+12) *2+(17+18+24)*2 =193 读写次数=WPL*2 =386
0 2 3 权值愈小的结点,离根愈远! • 解2:∵ k-(m-1) mod (k-1)-1 =3-(8-1) mod (3-1)-1=1 ∴ 虚设一个长度为“0”的归并段。 5 6 9 12 17 18 20 24 47 91 WPL=(0+2+3)*3+(6+9+12+17+18)*2+24*1=163 读写次数=WPL*2=326
如何判定虚设段的个数? 令 M ——初始归并段的个数 K ——归并的路数 若(M-1) MOD (K-1)= 0,则不加虚设段; 否则需加 P个虚设段。 P = K-(M-1) MOD (K-1) -1