1.05k likes | 1.13k Views
第十章 内部排序. 10.1、基本概念 10.2、插入排序 10.3、快速排序 10.4、选择排序 10.5、归并排序 10.6、基数排序 10.7、讨论. 8.1 基本概念. 术语 记录 —— 结点;文件 —— 线性表 关键码:能够唯一确定结点的一个或若干域。 排序码:作为排序运算依据的一个或若干域。 组合排序码,(主关键码,次关键码) 例 :(总分数,数据结构分数,大学英语分数) 排序 :将一个数据元素 ( 或记录 ) 的任意序列,重排成一个按排序码有序的序列。即排序码域的值具有不减 ( 或不增 ) 的顺序。.
E N D
第十章 内部排序 10.1、基本概念 10.2、插入排序 10.3、快速排序 10.4、选择排序 10.5、归并排序 10.6、基数排序 10.7、讨论
8.1 基本概念 • 术语 • 记录——结点;文件——线性表 • 关键码:能够唯一确定结点的一个或若干域。 • 排序码:作为排序运算依据的一个或若干域。 • 组合排序码,(主关键码,次关键码) • 例:(总分数,数据结构分数,大学英语分数) • 排序:将一个数据元素(或记录)的任意序列,重排成一个按排序码有序的序列。即排序码域的值具有不减(或不增)的顺序。
排序问题:给定一组记录r1, r2, …rn,其排序码分别为k1, k2, …,kn,将这些记录排成顺序为rs1,rs2,…rsn的一个序列S,满足条件ks1≤ks2≤…≤ksn。 • 稳定与不稳定排序:若记录序列中的任意两个记录 Rx、Ry 的关键字 Kx = Ky;如果在排序之前和排序之后,它们的相对位置保持不变,则这种排序方法是稳定的排序,否则是不稳定的排序。 • 分类: • 内部排序:全部记录都可以同时调入内存进行的排序 • 外部排序:文件中的记录太大,无法全部将其同时调入内存进行的排序。
待排序数据的存储结构 #define MAXSIZE 1000 // 待排顺序表最大长度 typedef int KeyType; // 关键字类型为整数类型 typedef struct { KeyType key; // 关键字项 InfoType otherinfo; // 其它数据项 } RcdType; // 记录类型 typedef struct { RcdType r[MAXSIZE+1]; // r[0]闲置 int length; // 顺序表长度 } SqList; // 顺序表类型
8.2 插入排序 插入排序的基本思想:共做n趟排序,第i趟排序的过程如下 有序序列R[1..i-1] 无序序列 R[i..n] R[i] 有序序列R[1..i] 无序序列 R[i+1..n]
8.2 插入排序 1.直接插入排序(基于顺序查找) 2.折半插入排序(基于折半查找) 3.表插入排序(基于链表存储) 4.希尔排序(基于逐趟缩小增量)
8.2 插入排序 插入过程分为两步, 1、在已排好序的表中查找插入位置 2、插入 不同的插入算法的区别在于第一步的不同 1、直接插入排序 利用 “顺序查找”实现“在R[1..i-1]中查找R[i]的插入位置”
8.2 插入排序 从R[i-1]起向前进行顺序查找,监视哨设置在R[0]; R[0] R[i] j j=i-1 插入位置 R[0] = R[i]; // 设置“哨兵” for (j=i-1; R[0].key<R[j].key; --j); // 从后往前找 循环结束表明R[i]的插入位置为 j +1
8.2 插入排序 对于在查找过程中找到的那些关键字不小于[i].key的记录,并在查找的同时实现记录向后移动; for (j=i-1; R[0].key<R[j].key; --j); R[j+1] = R[j] R[0] R[i] j j= i-1 插入位置 上述循环结束后可以直接进行“插入”
8.2 插入排序 void InsertionSort ( SqList &L ) { // 对顺序表 L 作直接插入排序。 for ( i=2; i<=L.length; ++i ) if (L.r[i].key < L.r[i-1].key) { } } // InsertSort 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]; // 插入到正确位置
49 暂 存 25 25* 21 16 08 25* 25* 0 1 2 3 4 5 6 49 25* 08 08 49 25 25* 21 21 *表示后一个25 例2:关键字序列T= (21,25,49,25*,16,08),请写出直接插入排序的具体实现过程。 解:假设该序列已存入一维数组V[7]中,将V[0]作为缓冲或暂存单元(Temp)。则程序执行过程为: 49 49 49 49 49 25 25 初态: 16 25 21 25 25* 21 16 16 16 08 完成! i=1 i=2 i=3 i=4 i=5 i=6
直接插入排序算法分析: • 最好的情况(关键字在记录序列中顺序有序): “比较”的次数: “移动”的次数: 0 • 最坏的情况(关键字在记录序列中逆序有序): “比较”的次数: “移动”的次数: 空间效率:O(1)——因为仅占用1个缓冲单元 算法的稳定性:稳定
平均的情况:若待排序对象序列中出现各种可能排列的概率相同,则可取上述最好情况和最坏情况的平均情况。在平均情况下的关键码比较次数和对象移动次数约为 n2/4。因此,直接插入排序的时间复杂度为 O(n2)。 • 直接插入排序是一种稳定的排序方法。 • 空间效率:O(1)——因为仅占用1个缓冲单元
二、折半插入排序 因为 R[1..i-1] 是一个按关键字有序的有序序列,则可以利用折半查找实现“在R[1..i-1]中查找R[i]的插入位置”,如此实现的插入排序为折半插入排序。
void BiInsertionSort ( SqList &L ) { } // BInsertSort for ( i=2; i<=L.length; ++i ) { } // for L.r[0] = L.r[i]; // 将 L.r[i] 暂存到 L.r[0] 在 L.r[1..i-1]中折半查找插入位置; for ( j=i-1; j>=high+1; --j ) L.r[j+1] = L.r[j]; // 记录后移 L.r[high+1] = L.r[0]; // 插入
low = 1; high = i-1; while(low<=high){ } m = (low+high)/2; // 折半 if(L.r[0].key < L.r[m].key) high = m-1; // 插入点在低半区 elselow = m+1; // 插入点在高半区
插入 位置 例如: i 58 61 23 97 75 L.r 14 36 49 52 80 high high low low low m m 插入 位置 m 再如: i 14 36 49 52 58 61 80 23 97 75 L.r low high high high low m m m
折半插入排序的算法分析 时间效率:在插入第 i 个对象时,需要经过 log2i +1 次关键码比较,才能确定它应插入的位置。因此,将 n 个对象用折半插入排序所进行的关键码比较次数为:n*log2n 但是移动次数并未减少,所以排序效率仍为O(n2) 。 空间效率:O(1) 稳定性:稳定 讨论:若记录是链表结构,用直接插入排序行否?折半插入排序呢? 答:直接插入不仅可行,而且还无需移动元素,时间效率更高! 但链表无法“折半”!
三、表插入排序(地址排序) 为了减少在排序过程中进行的“移动”记录的操作,必须改变排序过程中采用的存储结构。利用静态链表进行排序,并在排序完成之后,一次性地调整各个记录相互之间的位置,即将每个记录都调整到它们所应该在的位置上。
静态链表的存储结构 #define SIZE 100 Type struct{ RcdType rc; int next; }SLNode Type struct{ SLNode r[SIZE]; int length; }SLinkListType
例:关键字序列 T=(21,25,49,25*,16,08),请写出表插入排序的具体实现过程。 假设该序列(结构类型)已存入一维数组V[7]中,将V[0]作为表头结点。则算法执行过程为: 初态 i=1 1 6 5 0 2 i=2 0 4 3 i=3 0 i=4 3 i=5 1 i=6 5
void LInsertionSort (Elem SL[ ], int n){ // 对记录序列SL[1..n]作表插入排序。 SL[0].key = MAXINT ; SL[0].next = 1; SL[1].next = 0; for ( i=2; i<=n; ++i ) for ( j=0, k = SL[0].next;SL[k].key<= SL[i].key ; j=k, k=SL[k].next ) { SL[j].next = i; SL[i].next = k; } // 结点i插入在结点j和结点k之间 }// LinsertionSort
排序的结果只是求得一个有序链表,但只能进行顺序查找,不能进行随机查找,为了实现折半查找,还需要对纪录进行重新排列。排序的结果只是求得一个有序链表,但只能进行顺序查找,不能进行随机查找,为了实现折半查找,还需要对纪录进行重新排列。 如何在排序之后调整记录序列? 算法中使用了三个指针: 其中:p指示第i个记录的当前位置; i指示第i个记录应在的位置; q指示第i+1个记录的当前位置
void Arrange ( Elem SL[ ], int n ) { p = SL[0].next; // p指示第一个记录的当前位置 for ( i=1; i<n; ++i ) { while (p<i) p = SL[p].next; q = SL[p].next; // q指示尚未调整的表尾 if ( p!= i ) { SL[p]←→SL[i]; // 交换记录,使第i个记录到位 SL[i].next = p; // 指向被移走的记录, } p = q; // p指示尚未调整的表尾, // 为找第i+1个记录作准备 } } // Arrange
表插入排序算法分析: ① 时间开销:无需移动记录,只需修改2n次指针值。但由于比较次数没有减少,故时间效率仍为O(n2) 。 ② 空间开销:空间效率肯定低,因为增开了指针分量(但在运算过程中没有用到更多的辅助单元)。 ③ 稳定性:25和25*排序前后次序未变,稳定。
四、希尔(shell)排序(又称缩小增量排序) 基本思想:对待排记录序列先作“宏观”调整,再作“微观”调整。 所谓“宏观”调整,指的是,“跳跃式”的插入排序。 技巧:子序列的构成不是简单地“逐段分割”,而是将相隔某个增量dk的记录组成一个子序列,让增量dk逐趟缩短(例如依次取5,3,1),直到dk=1为止。 优点:让关键字值小的元素能很快前移,且序列若基本有序时,再用直接插入排序处理,时间效率会高很多。
将记录序列分成若干子序列,分别对每个子序列进行插入排序。将记录序列分成若干子序列,分别对每个子序列进行插入排序。 例如:将 n 个记录分成 d 个子序列: { R[1],R[1+d],R[1+2d],…,R[1+kd] } { R[2],R[2+d],R[2+2d],…,R[2+kd] } … { R[d],R[2d],R[3d],…,R[kd],R[(k+1)d] } 其中,d称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为 1。
13 04 49* 38 27 49 55 65 97 76 例:关键字序列 T=(49,38,65,97, 76, 13, 27, 49*,55, 04),请写出希尔排序的具体实现过程。 r[i] 初态: 第1趟 (dk=5) 49 13 38 27 49* 65 55 97 76 04 49 13 38 27 65 49* 55 97 04 76 第2趟 (dk=3) 13 13 27 04 49* 49* 55 38 04 27 49 49 55 38 65 65 97 97 76 76 第3趟 (dk=1) 04 13 27 49* 76 97 算法分析:开始时dk的值较大,子序列中的对象较少,排序速度较快;随着排序进展,dk值逐渐变小,子序列中对象个数逐渐变多,由于前面工作的基础,大多数对象已基本有序,所以排序速度仍然很快。
void ShellInsert ( SqList &L, int dk ) { for ( i=dk+1; i<=n; ++i ) if ( L.r[i].key< L.r[i-dk].key) { L.r[0] = L.r[i]; // 暂存在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]; // 插入 } // if } // ShellInsert
void ShellSort (SqList &L, int dlta[ ], int t) { // 增量为dlta[]的希尔排序 for (k=0; k<t; ++t) ShellInsert(L, dlta[k]); //一趟增量为dlta[k]的插入排序 } // ShellSort
算法分析:开始时dk的值较大,子序列中的对象较少,排序速度较快;随着排序进展,dk值逐渐变小,子序列中对象个数逐渐变多,由于前面工作的基础,大多数对象已基本有序,所以排序速度仍然很快。算法分析:开始时dk的值较大,子序列中的对象较少,排序速度较快;随着排序进展,dk值逐渐变小,子序列中对象个数逐渐变多,由于前面工作的基础,大多数对象已基本有序,所以排序速度仍然很快。 时间效率: O(n1.25)~O(1.6n1.25)——经验公式 空间效率:O(1)——因为仅占用1个缓冲单元 算法的稳定性:不稳定——因为49*排序后却到了49的前面
对特定的待排序对象序列,可以准确地估算关键码的比较次数和对象移动次数。但想要弄清关键码比较次数和对象移动次数与增量选择之间的依赖关系,并给出完整的数学分析,还没有人能够做到。对特定的待排序对象序列,可以准确地估算关键码的比较次数和对象移动次数。但想要弄清关键码比较次数和对象移动次数与增量选择之间的依赖关系,并给出完整的数学分析,还没有人能够做到。 Knuth利用大量的实验统计资料得出,当n很大时,关键码平均比较次数和对象平均移动次数大约在 n1.25到 1.6n1.25的范围内。这是在利用直接插入排序作为子序列排序方法的情况下得到的。
10.3 快速排序 1) 冒泡排序 基本思路:每趟不断将记录两两比较,并按“前小后大”(或“前大后小”)规则交换。 优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一旦下趟没有交换发生,还可以提前结束排序。 前提:顺序存储结构
void BubbleSort(Elem R[ ], int n) { while (i >1) { }// while } // BubbleSort i = n; if (R[j+1].key < R[j].key){ Swap(R[j], R[j+1]); lastExchangeIndex = j; //记下进行交换的记录位置 }//if lastExchangeIndex = 1; for (j = 1; j < i; j++) i = lastExchangeIndex; // 本趟进行过交换的 // 最后一个记录的位置
注意: 1. 起泡排序的结束条件为, 最后一趟没有进行“交换记录”。 2.一般情况下,每经过一趟“起泡”,“i 减一”,但并不是每趟都如此。 例如: 5 3 3 5 9 2 1 5 7 8 9 1 i=2 i=6 i=7 for (j = 1; j < i; j++) if (R[j+1].key < R[j].key) …
时间分析: 最好的情况(关键字在记录序列中顺序有序): 只需进行一趟起泡 0 “比较”的次数: n-1 “移动”的次数: 最坏的情况:初始排列逆序,算法要执行n-1趟起泡,第i趟(1in) 做了n- i 次关键码比较,执行了n-i 次对象交换。 “比较”的次数: “移动”的次数:
2) 快速排序 从待排序列中任取一个元素 (例如取第一个) 作为中心,所有比它小的元素一律前放,所有比它大的元素一律后放,形成左右两个子表;然后再对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个。此时便为有序序列了。 基本思想: 优点:因为每趟可以确定不止一个元素的位置,而且呈指数增加,所以特别快! 前提:顺序存储结构
例:关键字序列 T=(21,25,49,25*,16,08),快速排序的算法步骤: 21, 25, 49, 25*,16, 08 初态: 第1趟: 第2趟: 第3趟: ( ), 16,08 21 ,( ) 25,25*,49 (08),16,21, 25,(25*,49) 08,16,21,25,25*,(49) 讨论: 1. 这种不断划分子表的过程,计算机如何自动实现? 2. “快速排序”是否真的比任何排序算法都快?
1.这种不断划分子表的过程,计算机如何自动实现?1.这种不断划分子表的过程,计算机如何自动实现? 编程时: ①每一趟的子表的形成是采用从两头向中间交替式逼近法; ②由于每趟中对各子表的操作都相似,主程序可采用递归算法。 一趟快速排序算法(针对一个子表的操作) int Partition(SqList &L,int low,int high){ //一趟快排 //交换子表 r[low…high]的记录,使支点(枢轴)记录到位,并返回其位置。返回时,在支点之前的记录均不大于它,支点之后的记录均不小于它。 r[0]=r[low]; //以子表的首记录作为支点记录,放入r[0]单元 (续下页)
pivotkey=r[low].key; //取支点的关键码存入pivotkey变量 while(low < high){//从表的两端交替地向中间扫描 while(low<high && r[high].key>=pivotkey ) - -high; r[low]=r[high]; //将比支点小的记录交换到低端; while(low<high && r[low].key<=pivotkey) + +low; r[high]=r[low]; //将比支点大的记录交换到高端; } r[low]=r[0]; //支点记录到位; return low; //返回支点记录所在位置。 }//Partition
例2:关键字序列 T=(21,25,49,25*,16,08),请写出快速排序算法的一趟实现过程。 high low pivotkey=21 3 21 08 25 16 21 49 25* 16 49 08 25 ( 08,16 ) 21 ( 25* , 49, 25 ) Low=high=3,本趟停止,将支点定位并返回位置信息 25*跑到了前面,不稳定!
一趟快速排序算法流程图 i=low; j=high;r[0]=r[low]; pivot=r[low].key; i < j r[i] = r[0]; N Y i < j &&r[j].key>=pivot return ok; N Y --j; r[i] = r[j]; N i < j &&r[i].key<=pivot Y --i; r[j] = r[i]; j从高端扫描 寻找小于pivot的元素 i从低端扫描 寻找大于pivot的元素
整个快速排序的递归算法: void QSort ( SqList &L, int low, int high) { if ( low < high) { pivot = Partition ( L, low, high ); QSort ( L, low, pivot-1); QSort ( L, pivot+1, high ); } } //长度>1 //QSort 对顺序表L进行快速排序的操作函数为: void QuickSort ( SqList &L) { QSort (L,1, L.length); }
例:以关键字序列(256,301,751,129,937,863,742,694,076,438)为例,写出执行快速算法的各趟排序结束时,关键字序列的状态。例:以关键字序列(256,301,751,129,937,863,742,694,076,438)为例,写出执行快速算法的各趟排序结束时,关键字序列的状态。 原始序列:256,301,751,129,937,863,742,694,076,438 256 第1趟 第2趟 第3趟 第4趟 256,301,751,129,937,863,742,694,076,438 076 129 256 751 301 076,129,256,751,937,863,742,694,301,438 076,129,256,438,301,694,742,694,863,937 751 快速排序 076,129,256,301,301,694,742,751,863,937 076,129,256,438,301,694,742,751,863,937 438 076,129,256,301,438,694,742,751,863,937
快速排序算法分析: • 快速排序是递归的,需要有一个栈存放每层递归调用时的指针和参数(新的low和high)。 • 可以证明,函数quicksort的平均计算时间也是O(nlog2n)。实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。 • 最大递归调用层次数与递归树的深度一致,理想情况为 log2(n+1) 。因此,要求存储开销为 o(log2n)。 • 如果每次划分对一个对象定位后,该对象的左侧子序列与右侧子序列的长度相同,则下一步将是对两个长度减半的子序列进行排序,这是最理想的情况。此时,快速排序的趟数最少。 稳 定 性:不稳定 —因为可选任一元素为支点。
讨论2. “快速排序”是否真的比任何排序算法都快? ——基本上是!因为每趟可以确定的数据元素是呈指数增加的! 设每个子表的支点都在中间(比较均衡),则: 第1趟比较,可以确定1个元素的位置; 第2趟比较(2个子表),可以再确定2个元素的位置; 第3趟比较(4个子表),可以再确定4个元素的位置; 第4趟比较(8个子表),可以再确定8个元素的位置; …… 只需log2n +1趟便可排好序。 而且,每趟需要比较和移动的元素也呈指数下降,加上编程时使用了交替逼近技巧,更进一步减少了移动次数,所以速度特别快。
10.4 选择排序 基本思想:每一趟在后面n-i 个待排记录中选取关键字最小的记录作为有序序列中的第i 个记录。 选择排序有多种具体实现算法: 1) 简单选择排序 2) 锦标赛排序 3) 堆排序
1)简单选择排序 思路:每经过一趟比较就找出一个最小值,与待排序列最前面的位置互换即可。 ——首先,在n个记录中选择最小者放到r[1]位置;然后,从剩余的n-1个记录中选择最小者放到r[2]位置;…如此进行下去,直到全部有序为止。 优点:实现简单 缺点:每趟只能确定一个元素,表长为n时需要n-1趟 前提:顺序存储结构
例:关键字序列T= (21,25,49,25*,16,08),请给出简单选择排序的具体实现过程。 原始序列:21,25,49,25*,16,08 第1趟 第2趟 第3趟 第4趟 第5趟 08,25,49,25*,16,21 08,16, 49,25*,25,21 08,16, 21,25*,25,49 08,16, 21,25*,25,49 08,16, 21,25*,25,49 直接选择排序 时间效率:O(n2)——虽移动次数较少,但比较次数仍多。 空间效率:O(1)——无需任何附加单元! 算法的稳定性:不稳定——因为排序时,25*到了25的前面。
简单选择排序的算法如下: Void SelectSort(SqList &L) { for (i=1; i<L.length; ++i){ j = SelectMinKey(L,i); if( i!=j )r[i] r[j]; } } //SelectSort //对顺序表L作简单选择排序 //选择第i小的记录,并交换到位 //在r[i…L.length]中选择key最小的记录 //与第i个记录交换 讨论:能否利用(或记忆)首趟的n-1次比较所得信息,从而尽量减少后续比较次数呢? 能!——锦标赛排序和堆排序!