410 likes | 571 Views
Chap9. 排序. 第 9 章 文件. 9 .1 基本 概念. 排序 (Sorting) 是把一个无序的数据元素序列按某个关键字进行有序 ( 递增或递减 ) 排列的过程。排序中经常把数据元素称为 记录 (Record) 。把记录中作为排序依据的某个数据项称为排序关键字,简称 关键字 (Key) 。. 9 .1 基本 概念. 所谓排序 ,就是把一组杂乱无章的记录按照某种次序排列起来,使其具有一定的顺序。一般,设有一个由记录 { R(1), R(2), …, R(n)} 组成的文件,其相应的关键字值为 { K(1), K(2), …, K(n)}
E N D
Chap9 排序
9.1基本概念 排序(Sorting)是把一个无序的数据元素序列按某个关键字进行有序(递增或递减)排列的过程。排序中经常把数据元素称为记录(Record)。把记录中作为排序依据的某个数据项称为排序关键字,简称关键字(Key)。
9.1基本概念 所谓排序,就是把一组杂乱无章的记录按照某种次序排列起来,使其具有一定的顺序。一般,设有一个由记录 {R(1), R(2), …, R(n)} 组成的文件,其相应的关键字值为 {K(1), K(2), …, K(n)} 按关键字值的某种次序,寻求一种排列P(1),P(2),…, P(n),使其相应的关键字满足非减关系 K(P(i))≤K(P(i+1)) 1≤i≤n-1 或满足非增关系 K(P(i))≥K(P(i+1)) 1≤i≤n-1 从而得到文件中各记录的一种线性有序序列 {R(P(1)), R(P(2)), …, R(P(n))} 这个过程叫做排序。
9.1基本概念 我们约定:在未排序的文件中,如果有i, j且K(i)=K(j),则在经过排序的文件中,R(i)仍处在R(j)的前面,即具有相同关键字值的记录在排序过程中其相对位置不变。能产生这种排序的方法称为稳定的,反之称为不稳定的。 排序分类: 内部排序 外部排序
9.2 插入排序 9.2.1 直接插入排序 1.插入排序的过程 插入排序的基本思想是把一个记录插入到一个有序的文件中,在插入后使该文件仍然是有序的。设有一个包含n个记录{R(1), R(2), …, R(n)}的源文件。假设有一个子文件,它是由源文件的第一个记录R(1)构成的,显然,这个只有一个记录的源文件是有序的。然后,把源文件的第二个记录R(2)按记录关键值的有序性插入到只包含一个记录R(1)的子文件中。
9.2 插入排序 9.2.1 直接插入排序
9.2 插入排序 9.2.1 直接插入排序 2.直接插入排序的算法 INSORT(R) { k[0]=-max; // 虚设一个记录R[0]的关键值k(0),其关键字为负无穷 for(i=2; i<=n;i++) // 从第二个记录起进行插入 { t=k[i]; // 保存待插入的记录 j=i-1; while(k[i]<k[j]){ k[j+1]= k[j]; j--; } //将第j个记录赋值给第j+1个记录,直至关键字值不大于待插入记录的关键字为止 k[j+1]=t;// 将第i个记录插入 } }
9.2 插入排序 9.2.1 直接插入排序 3.算法分析 直接插入排序所需移动记录的次数的量级为O(n2),顺序插入排序是稳定的。
9.2 插入排序 9.2.2 希尔排序 1.希尔排序过程 希尔排序(Shell’s Sort)也称为“缩小增量法排序”,是由D.L.Shell对直接插入排序进行改进后提出来的。其基本思想是:不断地把待排序的一组记录按间隔值分成若干小组,分别进行组内直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。 对间隔值(用d表示)的取法有多种,希尔提出的方法是:d1=[n/2],di+1=[di/2],最后一次排序时间的间隔值必须为1,其中n为记录数。
9.2 插入排序 9.2.2 希尔排序
9.2 插入排序 9.2.2 希尔排序 2.希尔排序算法 Shellsort (Recordnode r[],int n) {inti,j,d; structrecordnode x; d=n/2; //设置初值 while(d>1) { for(i=d+1;i<=n;i++) { j=i-d; while(j>=0) if (r[j].key>r[j+d].key) { x=r[j]; //将r[j]与r[j+d]进行交换 r[j]=r[j+d]; r[j+d]=x; j=j-d; } else j=0; } d=d/2; //减小间隔值 } }
9.2 插入排序 9.2.2 希尔排序 3.算法分析 该算法通过三重循环来实现,外循环由不同间隔值d来控制,直到d=1为止,中间循环是在某个d值下对各组进行排序,用变量i控制。 可以看出,希尔排序实际上是对直接插入排序的一种改进,它的排序速度一般要比直接插入排序快。希尔排序的时间复杂速度取决于所取的间隔,一般认为是O(n lb n)。希尔排序是一个较复杂的问题,并且它还是一种不稳定的排序方法。
9.3 交换排序 9.3.1 冒泡排序 1.冒泡排序过程 冒泡排序是一种简单常用的排序方法。其排序思想是:通过相邻记录关键字间的比较和交换,使关键字最小的记录像气泡一样逐渐上浮。比较可以采用不同的方法,本算法是从最下面的记录开始,对两个相邻的关键字进行比较并且使关键字较小的记录换至关键字较大的记录之上,使得经过一次冒泡后,关键字最小的记录到达最上端,接着,再在剩下的记录中找关键字最小的记录,把它换到第二个位置上。依次类推,一直到所有记录都有序为止。一般情况下,记录数为n,需要做n-1次冒泡。
9.3 交换排序 9.3.1 冒泡排序
9.3 交换排序 9.3.1 冒泡排序 2.冒泡排序算法 Bubblesort (Recordnode r[],int n) {inti,j,f; f=1; for(i=1;i<=n-1&&F==1;i++) { f=0; for(j=n;j>=i+1;j--) if(r[j].key<r[j-1].key) { f=1; r[0]=r[j]; //r[0]用于中转 r[j]=r[j-1]; r[j-1]=r[0]; } } }
9.3 交换排序 9.3.1 冒泡排序 3.算法分析 冒泡排序是一种稳定的排序方法,算法的执行时间与原始记录的有序程度有很大关系,如果原始记录已经是有序排列时,比较次数为n-1次,交换次数为0;如果原始记录是“逆序”排列时,则总的比较次数为n(n-1)/2,交换次数为3n(n-1)/2。所以,算法的平均时间复杂度为O(n2)。
9.3 交换排序 9.3.2 快速排序 1.排序过程 快速排序(Quick Sort)是目前排序中速度较快的方法之一,其实质是在分区内交换未排的记录而完成排序。该文件的存储结构是一个向量,即线性表。 快速排序又叫分区交换排序,它是对冒泡排序的一种改进。其排序的基本思想是:取记录序列中一个合适的关键字(通常选取序列中的第一个),以此关键字取对应的记录ri作为基准,把一个序列分割成两个独立的子序列,使得基准位置前的所有记录的关键字都小于ri key,而基准位置后的所有记录的关键字都大于ri.key。这里把这样的一次过程称作一次快速排序,在第一次快速排序中,确定了所选取的基准记录ri在序列中的最终排列位置,同时也把剩余的记录分成了两个序列,然后对每个序列再进行分割,重复上述过程,直到所有记录全部排好序为止。
9.3 交换排序 9.3.2 快速排序
9.3 交换排序 9.3.2 快速排序 1.排序过程 第一次快速排序后,用同样的方法对产生的两个子序列继续进行快速排序,下面给出整个排序过程: 初始关键字序列:{37 19 90 64 13 49 20 40} 第一次排序结果:{20 19 13} 37 {64 49 90 40} 第二次排序结果:{13 19} 20 37 {40 49} 64 {90} 第三次排序结果: 13 {19} 20 37 40 {49} 64 90 最后结果:13 19 20 37 40 49 64 90
9.3 交换排序 9.3.2 快速排序 2.算法描述 Quicskort(Recordnode r[],intlow,int high) //low和high为记录序列的下界和上界 {inti,j; structRecordnode x; i=low; j=high; x=r[low]; while(i<j) //在序列的右端扫描 { while(i<j&&r[j].key>=x.key) j--; if(i<j) //在序列的左端扫描 {r[i]=r[j]; i++;} while(i<j&&r[i].key<x.key) i++; if(i<j) {r[j]=r[i];j--;} } r[i]=x; //对序列进行快速排序,使用递归 if(low<i) Quicksort(r,low,i-1); if(i<high) Quicksort(r,j+1,high); }
9.3 交换排序 9.3.2 快速排序 3.算法分析: 快速排序的执行时间和基准记录的选取有关,若基准记录的关键字能把当前记录从中间分成两个相等的子区间,则运行效率最高;若基准记录的关键字在记录排序的首或尾时,即只有一个子区间,该算法就退化成冒泡排序法,最好和最坏情况的平均时间复杂度为O(n lb n)。快速排序也是一种不稳定的排序方法。
9.4 选择排序 9.4.1 简单选择排序 1. 简单选择排序过程 选择排序的基本思想是:第一趟在n-i+1(i=1,2,…,n-1)个记录中选择关键字进行n-i次比较,从n-i+1个记录中选出关键字最小的记录和第i个记录进行交换。具体步骤是: (1) 在未排序的文件中找出关键字值最小的记录,然后把这个记录与第一个位置上的记录对换,使得关键字值最小的记录定位; (2) 在余下的记录中找出关键字值最小的记录,并把它与第二个位置上的记录进行对调,使关键字值次小的记录在已排序的序列中定位; (3) 依次类推,一直到所有的记录逐个在排序的序列中定位。
9.4 选择排序 9.4.1 简单选择排序
9.4 选择排序 9.4.1 简单选择排序 2.排序算法: SELECTSORT(r) { for(i=1;i<=n-1; i++) // 共进行n-1趟排序 min=i;// min是在未排序的记录中,指示关键字值最小记录的序号 for(j=i+1;j<=n;j++) if(k[j]<k[min]) min=j; if(min!=j) { t=k[i]; k[i]= k[min]; k[min]=t; } // 选出第i个最小关键字的记录 }
9.4 选择排序 9.4.1 简单选择排序 3.算法分析: (1)比较次数 具有n个记录的文件,选择排序所需时间的量级为O(n2)。从整个算法分析,外层for语句和内层for语句的循环,形成排序所需时间的量级为O(n2)。 (2)移动次数 在算法的外循环for语句中,每次循环选择关键值最小的记录的指针min,若min与i不等,也就是说,最小关键值记录指针min的位置不是在未排序记录中的第一个,即第i位序号,则就要进行一次两个记录的对换,而两个记录的位置交换需要三次移动记录。由于n个记录的选择排序要进行n-1次最小关键字值选择,因此记录的移动次数最多为3(n-1)。
9.4 选择排序 9.4.2 堆排序 1.排序过程: 堆排序是借助于一种称为堆的完全二叉树结构进行排序的,排序过程中,将向量中存储的数据看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中的父结点和孩子结点之间的内在关系来选择关键字最小的记录。 具体做法是:把待排序的记录存放在数组r[1‥n]中,将r看作一棵二叉树,每个结点表示一个记录,第一个记录r[1]作为二叉树的根,以后各记录r[ 2 ],…,r[n]依次逐层从左到右顺序排列,构成一棵完全二叉树,任意结点r [i]的左孩子是r[2i],右孩子是r[2i+1],双亲是r[i/2]。 对这棵完全二叉树的结点进行调整,使各结点的关键字满足下列条件:r[i]≤r[2i] 且 r[i] ≤r[2i+1]
9.4 选择排序 9.4.2 堆排序
9.4 选择排序 9.4.2 堆排序
9.4 选择排序 9.4.2 堆排序 2.堆排序算法 /*建立初始堆*/ sift (Recordnode r[], int L, int m) //对有n个元素的数组r中的第L个元素进行筛选 {inti, j ; structRecordnode x; i=L;j=2*i; //r[j]是r[i]的左孩子 x=r[i]; //把筛选结点的值存入变量 x中 while (j<=m) { if (j<m && r[j].key<r[j+1].key) j++; //左孩子小于右孩子,沿右子树筛选 if(x<r[j]) { r[i]=r[j]; //大孩子上移到双亲的位置 i=j; //往下搜索,令r[j]为根结点 i=2*i; //下沉一层,求出左孩子 } else //根不小于它的孩子时退出循环 j=m+1; } r[i]=x; } //被筛选的结点放入最终位置
9.4 选择排序 9.4.2 堆排序 2.堆排序算法 /*堆排序*/ heapsort (Recordnode r[],int n) {inti,k; struck Recordnode x; for(i=n/2;i>0;i--) //建立初始堆 sift(r,i,n); for(i=n;i>1;i--) //进行n-1次循环,完成堆排序 { x=r[1]; //将第一个元素与当前区间的最后一个元素对调 r[1]=r[i]; r[i]=x; sift(r,1,i-1); //调用筛选子函数 } }
9.4 选择排序 9.4.2 堆排序 3.算法分析 从堆排序的算法知道,堆排序所需的比较次数是建立初始堆与重新建堆所需的比较次数之和,其平均时间复杂度和最坏的时间复杂度均为O(n㏒2n)。它是一种不稳定的排序方法。
9.5选择排序 9.5.1 归并排序的过程 归并排序的过程为:把源文件中的n个记录看成是n个子文件,每个子文件只有一个记录。因此,这n个子文件是有序的。这样可利用归并办法把这n个有序的子文件两两归并。经一趟归并后的每个子文件包含两个记录,若n是奇数时,尚有一个只包含一个记录的子文件,然后,再继续两两归并下去,最后便得到一个包含全部n个记录的有序文件,这个过程叫做2路归并排序。
9.5选择排序 9.5.1 归并排序的过程
9.5选择排序 9.5.2 归并排序的算法 基本归并算法如下: /* 把两个首尾相接的各自有序的文件x[1, …, m]和x[m+1, …, n]归并成一个有序文件z[1,…,n] */ MERGE(x,l,m,n,z) { i=1; j=m+1; p=1; // 三个子文件指针初始化 while((i<=m)&&(j<=n)) // 当两个文件都没有结束 {if(k[i]<k[j]) {z[p]=k[i]; // k[i]作为新文件z中第p个记录 i=i+1; // 移动指针 } else {z[p]=k[j]; j=j+1;} p=p+1; // 移动新文件的指针 } if(i>m) [z(p), …, z(n)]=[k(j), …, k(n)] else [z(p), …, z(n)]=[k(i), …, k(m)] // 把剩余子文件的记录复制到新文件z中 }
9.5选择排序 9.5.2 归并排序的算法 一趟归并算法/* 把n个记录的文件x,分成长度为l的有序文件,执行一趟归并,结果放在y文件中 */ MERGEPASS(n,l,x,y) {i=1; while(i<=n-2*l+1) //剩余部分不少于2*l个记录 { MERGE(x, i, i+l-1, i+2*l-1,y); // 归并两个长度为2*l的文件 i=i+2*l; // 移动指针,归并下一个长度为2*l的两个子文件 } if((i+l-1)<n) MERGE(x, i, i+l-1, n, y); // 剩余部分有一部分长度为l,另一部分长度不够l,归并这两部分 else [y(i), …, y(n)]=[x(i), …, x(n)]; 剩余部分长度不够l,复抄余下的记录 }
9.5选择排序 9.5.2 归并排序的算法 2路归并排序算法 /* 归并有n个记录的有序文件x,排序后结果仍放在x中 */ MERGESORT(n,x) {l=1; // 待归并的有序子文件的初始长度 while(l<n) // 若还没有得到长度为n的有序文件 {MERGESORT(n, l, x, y); // 把有n个记录的文件x分成长度为l的有序子文件,执行一趟归并,结果放在y中 l=2*l;// 改变待归并有序子文件的长度l MERGESORT(n, l, y, x); // 把有n个记录的文件y分成长度为l的有序子文件,执行一趟归并,结果放在x中 l=2*l;// 改变待归并有序子文件的长度 } }
9.5选择排序 9.5.3 算法分析 (1) 比较次数 在2路归并排序算法中,第一趟归并,大小为1的子文件得到归并;在第二趟中,归并的子文件长度为2;在第i趟中,归并文件的长度为2i。因此,对包含有n个记录的文件,完成归并排序总共需要作㏒2n趟归并。而完成一趟归并所需要的时间为O(n),因此对有n个记录的文件进行归并排序所需要的时间为O(n㏒2n)。 (2) 空间 完成归并排序除了待排序文件长度为n的存储空间外,还需要附加产生与新文件同样长度的存储空间。因此,附加存储量是比较大的。
9.6 各种内部排序的比较 9.5.3 算法分析
本章小结 1. 掌握如下基本概念: 排序、记录、关键字、内部排序、外部排序、稳定性、直接插入排序、起泡排序、选择排序、归并排序; 2.插入排序(直接插入排序、折半排序),交换排序(起泡排序、快速排序),选择排序(简单选择排序、堆排序),二路归并排序等排序的方法、算法及性能分析方法; 3.各种排序方法的比较。