510 likes | 646 Views
数据结构 第十章 内部排序. 本章内容 10.1 基本概念 10.2 插入排序 10.3 快速排序 10.4 选择排序 10.5 归并排序 10.6 基数排序. 10.1 基本概念. 关键字 是记录(数据元素)中的一个(或多个)字段。 通常用作检索和排序记录的依据 。 关键字通常可以进行比较操作。. 10.1 基本概念. 排序 : 设含有 n 个记录的文件 {R 1 ,R 2 ,...,R n } ,其相应的关键字为 {K 1 ,K 2 ,...,K n } ,将记录按关键字值非递减 ( 或非递增 ) 顺序排列的过程,称为 排序 。
E N D
本章内容 10.1基本概念 10.2插入排序 10.3快速排序 10.4选择排序 10.5归并排序 10.6基数排序
10.1 基本概念 • 关键字 • 是记录(数据元素)中的一个(或多个)字段。通常用作检索和排序记录的依据。 • 关键字通常可以进行比较操作。 10-3
10.1 基本概念 • 排序:设含有n个记录的文件{R1,R2,...,Rn},其相应的关键字为{K1,K2,...,Kn},将记录按关键字值非递减(或非递增)顺序排列的过程,称为排序。 • 排序的稳定性:对所有的Ki=Kj (i≠j),若排序前Ri领先于Rj,排序后Ri仍领先于Rj,则称该排序方法是稳定的,反之,称为不稳定的。稳定性是对序列中的两个相同的关键字在初始序列和最终有序序列中相对位置(即领先关系)是否变化。 • 排序分类 • 内部排序:待排序文件的全部记录存放在内存进行的排序,称为内部排序。 • 外部排序:排序过程中需要进行内外存数据交换的排序,称为外部排序。 10-4
10.1 基本概念 • 内排序分类: • 按排序过程依据的原则分为: • 插入排序 • 交换排序 • 选择排序 • 归并排序 • 计数排序 • 按排序过程所需的工作量分: • 简单排序 • 先进排序 • 基数排序 10-5
10.2 插入排序 10.2.1 直接插入排序 它是最简单的排序方法 • 基本思想:依次将每个待排序的记录插入到一个有序子文件的合适位置(有序子文件记录数增1) • 例如:已有待排序文件为:45,34,78,12,34,32,29,64。首先将文件的第一个记录,视为有序文件,然后从第二个记录开始,直到最后一个记录,依次将他们插入到有序文件的合适位置。 45 34 78 12 34’ 32 29 64 10-6
10.2 插入排序 • 直接插入排序算法 • 说明:监视哨L.r[0]的作用: • 保存记录L.r[i]的副本; • 监视下标j是否越界,自动控制循环结束 void InsertSort( SqList &L ) { int i, j; for( i=2; i<=L.length; ++i ) if (L.r[i].key < L.r[i-1].key) // "<",需将L.r[i]插入有序子表 { L.r[0]=L.r[i]; // 复制为哨兵 for(j=i-1; L.r[0].key<L.r[j].key; --j ) L.r[j+1]=L.r[j]; // 记录后移 L.r[j+1]=L.r[0]; // 插入到正确位置 } } 10-7
10.2 插入排序 • 算法分析 直接插入排序的算法简洁,容易实现。从时间来看,排序的基本操作为:比较两个记录的大小和移动记录。其中: 最小比较次数:Cmin = n-1 = O(n) 最大比较次数:Cmax =(2+3+…+n)=(n+2)(n-1)/2 = O(n2 ) 最小移动次数:Mmin = 0 最大移动次数:Mmax = (2+1 + 3+1 + … + n+1) = O(n2) • 若待排序记录序列中出现各种可能排列的概率相同,则可取上述最好情况和最坏情况的平均情况。在平均情况下的关键字比较次数和记录移动次数约为 n2/4。因此,直接插入排序的时间复杂度为 o(n2)。 10-8
10.2 插入排序 • 结论 • 直接插入排序的效率与待排文件的关键字排列有关; • 直接插入排序的时间复杂度为O(n2); • 直接插入排序是稳定的(这一点由过程中WHILE语句的条件“<”保证的)。 10-9
10.2 插入排序 10.2.2折半插入排序(Binary Insertsort) 由于是在有序子文件中确定插入的位置,因此可用折半查找来代替直接插入排序法中的顺序查找,从而可减少比较次数。 • 基本思想 • 设在顺序表中有一个记录序列 R[1], R[2], …, R[n]。其中,R[1], R[2], …, R[i-1]是已经排好序的记录。 • 在插入 R[i]时,利用折半搜索法寻找 R[i]的插入位置。 10-10
10.2 插入排序 • 折半插入排序算法 void BInsertSort(SqList &L) { int i, j, m, low, high; for( i=2; i<=L.length; ++i ){ L.r[0]=L.r[i]; // 将L.r[i]暂存到L.r[0] low=1; high=i-1; while( low <= high ){ // 在r[low..high]中折半查找有序插入的位置 m = (low+high)/2; // 折半 if ( L.r[0].key < L.r[m].key ) high = m-1; // 插入点在低半区 else low = m+1; // 插入点在高半区 } for( j=i-1; j>=high+1; --j ) L.r[j+1]=L.r[j]; // 记录后移 L.r[high+1]=L.r[0]; // 插入 } } 10-11
10.2 插入排序 • 算法分析 • 折半插入排序所需要的关键字比较次数与待排序记录序列的初始排列无关,仅依赖于记录个数。在插入第 i 个记录时,需要经过 log2i +1次关键字比较,才能确定它应插入的位置。将 n个记录用折半插入排序所进行的关键字比较次数为O(nlog2n) . • 折半插入排序的时间复杂度仍为O(n2) • 当 n较大时,总的关键字比较次数比直接插入排序的最坏情况要好得多,但比其最好情况要差。 • 当初始排列已经按关键字排好序或接近有序时,直接插入排序比折半插入排序执行的关键字比较次数要少。折半插入排序的记录移动次数与直接插入排序相同,依赖于记录的初始排列。 • 折半插入排序是一个稳定的排序方法。 10-12
10.2 插入排序 10.2.3 Shell排序 • 基本思想:希尔排序(Shell`s Methool)又称为缩小增量排序,也是一种插入排序方法。它将待排序数据文件分割成若干个较小的子文件,对各个子文件分别进行直接插入排序,当文件达到基本有序时,再对整个文件进行一次直接插入排序。 • 适用条件 如果待排序文件"基本有序",即文件中具有特性: r[i].key < Max {r[j ].key} 1≤j<I 的记录数较少 ,则文件中大多数记录不需要进行插入, 因而排序效率可以提高,接近于O(n)。 10-13
10.2 插入排序 例1:设初始关键字为: • 第一趟以步长为5分割为5个子文件: {R1,R6} {R2,R7} {R3,R8} { R4,R6} {R5,R10} 对每个子文件进行直接插入排序 • 第二趟以步长为3对第一趟排序结果分割为3 个子文件: {R1,R4,R7,R10} {R2,R5,R8} {(R3,R6,R9} • 对每个子文件进行直接插入排序 第三趟以步长为1对第二趟排序结果进行直接插入排序 原始数据: 4938 659776 13 2749' 5504 第一趟排序: 132749'55 044938 6597 76 第二趟排序: 1338 5576 042765 49'4997 第三趟排序: 04 1327 38 49'49 5565 76 97 10-14
10.2 插入排序 例2:对下列数据进行shell排序,步长分别选为4、2、1。 45 34 78 12 34’ 32 29 64 10-15
10.2 插入排序 • 希尔排序算法 void ShellInsert( SqList &L,int dk ){ for( i=dk+1; i<=L.Length; i++ ) if(L.r[i].key < L.r[i-dk].key){ // 需将L.r[i]插入有序增量子表 L.r[0] = L.r[i]; // 暂存在L.r[0],但不是哨兵。 for(j=i-dk; j>0 && L.r[0].key<L.r[j].key; j-=dk) L.r[j+dk] = L.r[j]; // 记录后移,查找插入位置 L.r[j+dk] = L.r[0]; } } void ShellSort(SqList &L, int dlta[], int t){ // 按增量序列dlta[0..t-1]对顺序表L作希尔排序 for(k=0; k<t; t++){ ShellInsert( L, dlta[k] ); // 一趟增量为dlta[k]的插入排序 } 10-16
10.2 插入排序 • 增量的取法 最初 shell提出取 d1 = n/2,d2 = d1/2,直到dt = 1。后来 knuth提出取di+1 = di/3 +1。还有人提出都取奇数为好,也有人提出各增量互质为好。 • 算法分析 • 不稳定 • 空间代价:O(1) • 增量每次除以2递减,时间代价:O(n2) • 选择适当的增量序列,可以使得时间代价接近O(n) • 增量每次除以2递减”时,效率仍然为O(n2) • 问题:选取的增量之间并不互质 • 间距为2k-1的子序列都是由那些间距为2k的子序列组成的 • 上一轮循环中这些子序列都已经排过序了,导致处理效率不高 10-17
10.3 交换排序 10.3.1 冒泡排序 冒泡排序是一种简单而且容易理解的排序方法,它和气泡从水中不断往上冒的情况有些类似。 • 其基本思想 对存放原始数据的数组,按从后往前的方向进行多次扫描,每次扫描称为一趟 (pass)。当发现相邻两个数据的次序与排序要求的“递增次序”不符合时,就互换两个数据。这样,较小的数据就会逐单元向前移动,好象气泡向上浮起一样。 • 示例 45 34 78 12 34’ 32 29 64 10-18
10.3 交换排序 • 冒泡排序算法 void BubbleSort( SqList &L ){// 从下往上扫描的起泡排序 for( i=0; i<n-2;i++ ){// 做n-1趟排序 noSwap=TRUE;// 置未交换标志 for( j=n-1;j>=i;j-- )// 从下往上扫描 if(L.r[j+1].key < L.r[j].key) { temp=L.r[j+1]; // 交换记录 L.r[j+1]=L.r[j]; L.r[j]=temp; noSwap=FALSE; } if( noSwap ) break;// 本趟扫描未发生交换,则终止算法 } } 10-19
10.3 交换排序 • 算法评价 • 算法稳定 • 空间代价:O(1) • 时间代价 : • 比较次数 : • 交换次数最多为O(n2),最少为0,平均为O(n2)。 • 最大,平均时间代价均为O(n2)。 • 最小时间代价为O(n):最佳情况下只运行第一轮循环 10-20
10.3 交换排序 10.3.2 快速排序 • 基本思想 • 任取某个记录作为基准(通常选文件的第一个记录),将所有关键字不大于它的记录放在它的前面,将所有关键字不小于它的记录放在它的后面; • 这样遍历一趟文件后,将文件以该记录为界分为两部分; • 然后对各部分重复上述过程,直到每一部分仅剩一个记录为止。 • 特点:基于分治法的排序:快速、归并。分治策略的实例 • BST查找、插入、删除算法 • 快速排序、归并排序 • 二分检索 • 主要思想:划分、求解子问题(子问题不重叠)、综合解 10-21
25 29 12 34’64 45 34 34’34 45 12 25 12 34’ 10.3 交换排序 25 34 45 32 34’12 29 64 32 64 29 45 25 34 最终排序结果:12 25 29 32 34’ 34 45 64 10-22
10.3 交换排序 • 快速排序算法评价 • 快速排序算法不稳定。 • 常用“三者取中”法来选取划分记录,即取首记录r[s].key.尾记录r[t].key和中间记录r[(s+t)/2].key三者的中间值为划分记录。 • 算法分析 • 最差情况: • 时间代价: O(n2) • 空间代价: O(n) • 最佳情况: • 时间代价:O(nlog n) • 空间代价:O(log n) • 平均情况: • 时间代价:O(nlog n) • 空间代价:O(log n) 10-23
10.4 选择排序 • 基本思想 每一趟 (例如第 i趟,i = 1, 2, …, n-1) 在后面 n-i+1个待排序记录中选出关键字最小的记录, 作为有序记录序列的第 i个记录。待到第n-1趟作完,待排序记录只剩下1个,就不用再选了。 10-24
10.4 选择排序 10.4.1 简单选择排序 • 基本思想 首先在所有记录中选出关键字最小的记录,把它与第1个记录交换,然后在其余的记录中再选出关键字次最小的记录与第2个记录交换,以次类推……,直到所有记录排序完成。 10-25
1 2 3 4 5 6 7 8 i=5 i=7 i=6 i=2 i=4 i=3 i=1 12 12 12 12 12 12 12 28 28 28 28 28 34 28 31 31 31 31 49 31 49 34 34 28 34 34 34 34 49 49 49 49 49 31 31 49* 52 52 49* 52 52 52 51 51 51 51 51 51 51 52 52 49* 49* 49* 49* 49* 10.4 选择排序 0 初始关键字序列 34 12 49 28 31 52 51 49* 10-26
10.4 选择排序 • 简单选择排序的算法 void SelectSort(SqList &L){ int i, j; RedType temp; for( i=0; i<L.length-1; i++ ){ // 做n-1趟选择排序 k = i; // 在当前无序区选关键字最小的记录 for( j=i+1; j<L.length; j++ ) if( L.r[j].key < L.r[k].key ) k = j ; if( k != i ){ temp = L.r[i]; L.r[i] = L.r[k]; L.r[k] =temp; } } } 10-27
10.4 选择排序 • 算法分析 • 交换次数:正序时交换次数最少,为0次,逆序时最多,为n-1次。 • 比较次数:与初始文件关键字排列无关,为n(n-1)/2次。 • 简单选择排序时间复杂度为O(n2),并且是稳定的排序。 10-28
或 12 96 36 24 83 27 85 47 30 53 38 11 9 91 (a) 大顶堆(max heap) (b) 小顶堆(min heap) 10.4 选择排序 10.4.2 堆排序 • 堆的定义 对于n个元素的序列{k1,k2,...,kn},当且仅当满足以下关系时,称之为堆。 10-29
10.4 选择排序 • 若将堆视为一个完全二叉树,则堆的含义为:完全二叉树中所有非叶结点的值(ri) 均不大于(或不小于)其左孩子的值(r2i)、右孩子的值(r2i+1) 。 • 堆顶元素(完全二叉树的根)是序列中最小(或最大)的元素。 12 36 27 65 40 98 34 14 81 73 55 49 不 是堆 10-30
10.4 选择排序 • 堆排序(Heap Sort)基本思想 • 以初始关键字序列,建立堆; • 输出堆顶最小元素; • 调整余下的元素,使其成为一个新堆; • 重复2,3步,直到n个元素输出,得到 一个有序序列。 • 关键问题: • 要解决1和3,即如何由一个无序序列建成一个堆? • 如何调整余下的元素成为一个新堆? 10-31
10.4 选择排序 • 调整方法 • 将堆顶元素和堆的最后一个元素位置交换; • 然后以当前堆顶元素和其左、右子树的根结点进行比较(此时,左、右子树均为堆),并与值较小的结点进行交换; • 重复第2步,继续调整被交换过的子树,直到叶结点或没进行交换为止。 称这个调整过程为"筛选"。 10-32
13 38 3 输出13 1 49‘ 38 49 27 筛选 97 27 2 输出27 筛选结果 97 49‘ 76 65 27 76 65 49 38 49 38 27 13 97 49‘ 76 65 97 97 49’ 76 65 49 3 13 1 38 49 13 49’ 2 76 65 27 13 10.4 选择排序 • 例如:设有关键字{13,38,27,49’,76,65,49,97},按初始次序构成一棵完全二叉树,形成一个堆如下图: 10-33
10.4 选择排序 • 筛选过程 void HeapAdjust (SqList R[], int s, int m) { //已知R[s..m]中记录的关键字除R[s].key之外均满足堆的定义,本函数调整 //R[s] 的关键字,使R[s..m]成为一个大顶堆(对其中记录的关键字而言) rc = R[s]; for ( j=2*s; j<=m; j*=2 ) { // 沿key较大的孩子结点向下筛选 if ( j<m && R[j].key<R[j+1].key ) ++j; // j为key较大的记录的下标 if ( rc.key >= R[j].key ) break; // rc应插入在位置s上 R[s] = R[j]; s = j; } R[s] = rc; // 插入 } // HeapAdjust 10-34
13 49 49 27 13 65 38 38 38 97 49‘ 49‘ 76 76 76 13 65 65 27 49 27 97 97 49’ 10.4 选择排序 • 无序序列建堆过程 方法:从完全二叉树的最后一个非叶结点 n/2 开始,反复调用筛选过程,直到第一个结点,则得到一个堆。 • 例:{49,38,65,97,76,13,27,49’} 10-35
10.4 选择排序 • 堆排序的算法 void HeapSort ( SqList R[], int n ) { // 对记录序列R[1..n]进行堆排序。 for ( i=n/2; i>0; --i ) // 把R[1..n]建成大顶堆 HeapAdjust ( R, i, n ); for ( i=n; i>1; --i ) { R[1]←→R[i]; // 将堆顶记录和当前未经排序子序列R[1..i]中最后一个记录相互交换 HeapAdjust(R, 1, i-1); } //将R[1..i-1] 重新调整为大顶堆 } // HeapSort 10-36
10.4 选择排序 • 堆排序的时间复杂度分析 • 对深度为k的堆,“筛选”所需进行的关键字比较的次数至多为2(k-1); • 对n个关键字,建成深度为h(=log2n+1)的堆,所需进行的关键字比较的次数至多为4n; • 调整“堆顶”n-1次,总共进行的关键字比较的次数不超过2( log2(n-1) + log2(n-2)+ …+log22)<2n( log2n ) 因此,堆排序的时间复杂度为O(nlogn),且不稳定。 10-37
10.5 归并排序 • 归并 归并是指将若干个已排序好的有序表合并成一个有序表。两个有序表的归并称为二路归并。 • 归并排序 将待排序的n个记录,看作n个有序的子序列,每个子序列的长度为1。然后两两归并,得到n/2个长度为2或为1的子序列;再两两归并,...,如此重复,直到得到长度为n的子序列为止。这种排序的方法称为2_路归并排序。 10-38
10.5 归并排序 • 2_路归并排序的核心操作:将一维数组中前后两个有序序列归并为一个有序序列。 • 例: 将一维数组49,38,65,97,76,13,27,49进行2_路归并排序: 初始: [49],[38],[65],[97],[76],[13],[27],[49] 第一趟:[38,49],[65,97],[13,76],[27,49] 第二趟:[38,49,65,97],[13,27,49,76] 第三趟: [13,27,38,49,49,65,76,97] 10-39
10.5 归并排序 • 将两个有序序列归并为一个有序序列的算法 void merge (Sqlist SR,Sqlist &TR,int i,int m, int n) //将有序表 SR.r[i..m]以及SR.r[m+1..n]有序归并到 TR.r[i..n]中 {la=i;lb=m+1;lc=i; //序列 la,lb,lc 的始点 while(la<=m &&lb<=n) { if LT(SR.r[la].key, SR.r[lb].key) TR.r[lc++]=SR.r[la++] //有序合并 else TR.r[lc++]=SR.r[lb++] } if ( la<=m) TR.r[lc..n]=SR.r[la..m]; //剩余复制 if ( lb<=n) TR.r[lc..n]=SR.r[lb..n]; } 10-40
10.5 归并排序 • 一趟归并排序操作 需调用n/(2h)次算法merge,将SR[1..n]前后相邻且长度为h的有序段两两归并,得到前后眼相邻、长度为2h的有序段,并放在TR[1..n]中。整个归并排序需要[log2n]趟。 递归算法:排序区间:R[s..t] 设:m=(int)((low+high)/2) 可递归地对两个子区间R[s..m]和R[m+1..t]进行归并排序。然后将两个已排序子区间合并为一个有序区间。 void MSort(SeqList SR,SeqList TR, ints,int t) //将有序表 SR.r[s..t]有序归并排序到 TR.r[s..t]中 { if (s==t) TR.r[s]=SR.r[s]; else { m=(s+t)/2; MSort( SR,MR,s,m); MSort( SR,MR,m+1,t); merge(MR,TR,s,m,t) } } 10-41
10.5 归并排序 • 算法分析 • 每趟归并的时间复杂度为O(n), • 整个算法需㏒2n趟。时间复杂度为O(nlog2n)。 • 归并排序算法虽简单,但占用辅助空间大,实用性差。 10-42
10.6 基数排序 • 基数排序 是一种无需进行关键字比较的新排序方法,其基本操作是“分配”和“收集”。 • 基数排序原理 基数排序是按组成关键字的各位的值进行分配和收集,与前面介绍的排序方法不同,它无需进行关键字之间的比较。 设关键字有d 位,每位的取值范围为 r (称为基数),则需要进行d 趟分配与收集,需要设立 r 个队列。例如,若每位是十进制数字,则需要设立10个队列,若每位由小写字母组成,则要设立26个队列 。 10-43
10.6 基数排序 • 基数排序的步骤 • 从关键字的低位开始进行第i趟(i=1,2,...d)分配即将单链表中的记录依次按关键字的第i位分配到相应编号的队列中; • 分配完毕后,将各队列的记录按队列编号顺序收集成一个单链表; • 上一趟形成的链队,作为下一趟的输入,重复⑴⑵,直到第d趟收集完毕,所得单链表已成为有序表。 10-44
278 109 063 930 589 184 505 269 008 083 例:初始 278—109—063—930—589—184—505—269—008—083 0 1 2 3 4 5 6 7 8 9 第一趟分配 10.6 基数排序 第一趟收集930—063—083—184—505—278—008—109—589—269 第二趟分配109 589 008 269 184 505 930 063 278 083 第二趟收集 505—008—109—930—063—269—278—083—184—589 第三趟分配 083 063 184 278 589 008 109 269 505 930 第三趟收集008—063—083—109—184—269—278—505—589—930 10-45
10.6 基数排序 • 基数排序的特点 • 基数排序的基本操作是"分配"和"收集",而不是关键字之间的比较; • 基数排序是稳定的,其时间复杂度为O(d(n+rd)),其中n是记录数,d是关键字的位数,r是关键字各位的值域; • 基数排序要进行d趟分配和收集,需r个队列。 10-46
10.7 各种内部排序方法的比较 • 排序方法选择主要考虑: • 待排序记录个数n • 记录本身的大小 • 关键字的分布情况 • 对排序稳定性要求 10-47
10.7 各种内部排序方法的比较 • 时间特性: • 时间复杂度为O(nlogn):快速、堆、归并排序 快速最快,在n较大时,归并较堆更快 • 时间复杂度为O(n2):插入、冒泡、选择排序 插入最常用,尤其基本有序时,选择记录移动次数最少 • 时间复杂度为O(n):基数排序 • 当待排序记录有序时:插入和冒泡可达到O(n),快速蜕化到O(n2); • 选择、堆和归并排序的时间特性不随关键字分布而改变 10-48
10.7 各种内部排序方法的比较 • 空间特性: • 所有的简单排序和堆排序的空间复杂度均为O(1); • 快速排序为O(logn); • 归并排序和基数排序所需辅助空间最多,其空间复杂度为O(n). • 稳定性: • 快速排序、希尔排序和堆排序是不稳定的; • 其他排序方法都是稳定的 10-49
10.7 各种内部排序方法的比较 • 排序方法比较 10-50