@排序

  1. 第八章 排序技术

本章的基本内容是:
排序的基本概念
插入排序
交换排序
选择排序

  • 排序:给定一组记录的集合{r1, r2, ……, rn},其相应的关键码分别为{k1, k2, ……,
    kn},排序是将这些记录排列成顺序为{rs1, rs2, ……, rsn}的一个序列,使得相应的关键码满足
    ks1≤ks2≤……≤ksn(称为升序)或 ks1≥ks2≥……≥ksn(称为降序)。 正序:待排序序列中的记录已按关键码排好序。
    逆序(反序):待排序序列中记录的排列顺序与排好序的顺序正好相反。 排序的基本概念
    排序算法的稳定性:假定在待排序的记录集中,存在多个具有相同键值的记录,若经过排序,这些记录的相对次序仍然保持不变,即在原序列中,ki=kj且ri在rj
    之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

学号 姓名 高数 英语 思想品德
0001 王军 85 68 88
0002 李明 64 72 92
0003 汤晓影 85 78 86
… … … … …
排序的基本概念
单键排序:根据一个关键码进行的排序;多键排序:根据多个关键码进行的排序。
学号 姓名 高数 英语 思想品德
0001 王军 85 68 88
0002 李明 64 72 92
0003 汤晓影 85 78 86
… … … … …
按学号排序——单键排序按成绩(高数+英语+思想品德)排序——多键排序

  1. 排序的基本概念

设关键码分别为k1, k2, …, km,多键排序有两种方法: ⑴依次对记录进行m次排序,第一次按k1排序,第二次按k2排序,依此类推。这种方法要求各趟排序所用的算法是稳定的;
⑵将关键码k1, k2, …, km分别视为字符串依次首尾连接在一起,形成一个新的字符串,然后,对记录序列按新形成的字符串排序。
多键排序 单键排序

  1. 排序的基本概念

排序的分类

  1. 内排序:在排序的整个过程中,待排序的所有记录全部被放置在内存中
  2. 外排序:由于待排序的记录个数太多,不能同时放置在内存,而需要将一部分记录放置在内存,另一部分记录放置在外存上,整个排序过程需要在内外存之间多次交换数据才能得到排序的结果。
    排序的基本概念
    排序的分类
  3. 基于比较:基本操作——关键码的比较和记录的移动,其最差时间下限已经被证明为Ω(nlog2n)。
  4. 不基于比较:根据关键码的分布特征。
    基于比较的内排序
  5. 插入排序
  6. 交换排序
  7. 选择排序
  8. 归并排序

- 基本操作。内排序在排序过程中的基本操作: ⑴比较:关键码之间的比较; ⑵移动:记录从一个位置移动到另一个位置。
9. 辅助存储空间。 辅助存储空间是指在数据规模一定的条件下,除了存放待排序记录占用的存储空间之外,执行算法所需要的其他存储空间。
算法本身的复杂程度。
10. 从操作角度看,排序是线性结构的一种操作,待排序记录可以用顺序存储结构或链接存储结构存储。
假定1:采用顺序存储结构,关键码为整型,且记录只有关键码一个数据项。 int r[n+1];
//待排序记录存储在r[1]~r[n],r[0]留做他用 假定2:将待排序的记录序列排序为升序序列。
插入排序的主要操作是插入,其基本思想是:每次将一个待排序的记录按其关键码的大小插入到一个已经排好序的有序序列中,直到全部记录排好序为止。
基本思想:在插入第 i(i>1)个记录时,前面的 i-1 个记录已经排好序。

有序序列	无序序列
r'1	r'2 …… r'i-1	r'i	ri+1 ……  

基本思想:在插入第 i(i>1)个记录时,前面的 i-1 个记录已经排好序。
需解决的关键问题?
(1) 如何构造初始的有序序列?
(2) 如何查找待插入记录的插入位置?
直接插入排序过程示例
r 0 1 2 3 4 5 6 
关键问题(1)如何构造初始的有序序列?
解决方法:
将第1个记录看成是初始有序表,然后从第2个记录起依次插入到这个有序表中,直到将第n个记录插入。
算法描述:
for (i=2; i<=n; i++)
{
插入第i个记录,即第i趟直接插入排序;
}
关键问题(2)如何查找待插入记录的插入位置?
解决方法:
r[0]有两个作用:
11. 进入循环之前暂存了r[i] 的值,使得不致于因记录的后移而丢失r[i]的内容;
12. 在查找插入位置的循环中充当哨兵。
在i-1个记录的有序区r[1] ~ r[i-1]中插入记录r[i],首先顺序查找r[i]的正确插入位置,然后将r[i]插入到相应位置。算法描述:
r[0]=r[i]; j=i-1; while (r[0]<r[j])
{ r[j+1]=r[j]; j–;
}
直接插入排序算法
void insertSort (int r[ ], int n)
{ for (i=2; i<=n; i++)
{ r[0]=r[i]; j=i-1; while (r[0]<r[j])
{ r[j+1]=r[j];
j=j-1;
} r[j+1]=r[0];
}
}

如果r[i]>=r[i-1],内层循环会出现什么情况?

直接插入排序算法性能分析
最好情况下(正序): 5 4 3 2 1 比较次数:n-1 4 4 5 3 2 1 移动次数:2(n-1) 3 3 4 5 2 1 时间复杂度为O(n)。 2 2 3 4 5 1 最坏情况下(逆序或反序): 1 1 2 3 4 5
n (n+2)(n−1)
比较次数:∑i =
i =2 2
移动次数:∑n(i +1)= (n+4)(n−1) i=2 2 时间复杂度为O(n2)。
直接插入排序算法性能分析
平均情况下(随机排列):
n i = (n+2)(n−1) 比较次数:i∑=2 2 4
n(i +1)= (n+4)(n−1) 移动次数:i=∑2 2 4 时间复杂度为O(n2)。
直接插入排序算法性能分析
空间性能:需要一个记录的辅助空间。
直接插入排序算法是一种稳定的排序算法。
直接插入排序算法简单、容易实现,适用于待排序记录基本有序或待排序记录较小时。
当待排序的记录个数较多时,大量的比较和移动操作使直接插入排序算法的效率降低。 如何改进直接插入排序?
注意到,在插入第 i(i>1)个记录时,前面的 i-1 个记录已经排好序,则在寻找插入位置时,可以用折半查找来代替顺序查找,从而较少比较次数。
请同学们写出这个改进的直接插入排序算法,并分析时间性能。

改进的着眼点:
(1) 若待排序记录按关键码基本有序时,直接插入排序的效率可以大大提高;
(2) 由于直接插入排序算法简单,则在待排序记录数量n较小时效率也很高。

基本思想:将整个待排序记录分割成若干个子序列,在子序列内分别进行直接插入排序,待整个序列中的记录基本有序时,对全体记录进行直接插入排序。
需解决的关键问题?
(1) 应如何分割待排序记录,才能保证整个序列逐步向基本有序发展?
(2) 子序列内如何进行直接插入排序? 分割待排序记录的目的?
13. 减少待排序记录个数;
14. 使整个序列向基本有序发展。
基本有序:接近正序,例如{1, 2, 8, 4, 5, 6, 7, 3, 9};局部有序:部分有序,例如{6, 7, 8, 9, 1, 2, 3, 4, 5}。
局部有序不能提高直接插入排序算法的时间性能。
启示?
子序列的构成不能是简单地“逐段分割”,而是将相距某个“增量”的记录组成一个子序列。
希尔插入排序过程示例
1 2 3 4 5 6 7 8 9 初始序列 40 25 49 25* 16 21 08 30 13 d = 4 40 25 49 25* 16 21 08 30 13
13 21 08 25* 16 25 49 30 40 d = 2 13 21 08 25* 16 25 49 30 40
08 21 13 25* 16 25 40 30 49 d = 1 08 21 13 25* 16 25 40 30 49
08 13 16 21 25* 25 30 40 49
关键问题(1)应如何分割待排序记录?
解决方法:
将相隔某个“增量”的记录组成一个子序列。
增量应如何取?
希尔最早提出的方法是d1=n/2,di+1=di/2。
算法描述:
for (d=n/2; d>=1; d=d/2)
{
以d为增量,进行组内直接插入排序;
} 关键问题(2)子序列内如何进行直接插入排序?
解决方法:
在插入记录r[i]时,自r[i-d]起往前跳跃式(跳跃幅度为d)搜索待插入位置,并且r[0]只是暂存单元,不是哨兵。当搜索位置<0,表示插入位置已找到。
在搜索过程中,记录后移也是跳跃d个位置。
在整个序列中,前d个记录分别是d个子序列中的第一个记录,所以从第d+1个记录开始进行插入。关键问题(2)子序列内如何进行直接插入排序?算法描述:
for (i=d+1; i<=n; i++) //将r[i]插入到所属的子序列中
{
r[0]=r[i]; //暂存待插入记录
j=i-d; //j指向所属子序列的最后一个记录
while (j>0 && r[0]<r[j])
{
r[j+d]=r[j]; //记录后移d个位置
j=j-d; //比较同一子序列的前一个记录
}
r[j+d]=r[0];
}

希尔排序算法的时间性能
 希尔排序开始时增量较大,每个子序列中的记录个数较少,从而排序速度较快;当增量较小时,虽然每个子序列中记录个数较多,但整个序列已基本有序,排序速度也较快。
 希尔排序算法的时间性能是所取增量的函数,而到目前为止尚未有人求得一种最好的增量序列。
 研究表明,希尔排序的时间性能在O(n2)和O(nlog2n)
 之间。当n在某个特定范围内,希尔排序所需的比较次数和记录的移动次数约为O(n1.3 )。

交换排序的主要操作是交换,其主要思想是:在待排序列中选两个记录,将它们的关键码相比较,如果反序(即排列顺序与排序后的次序正好相反),则交换它们的存储位置。

基本思想:两两比较相邻记录的关键码,如果反序则交换,直到没有反序的记录为止。
rj反序则交换rj+1 ri+1≤……≤rn-1≤rn
无序区 有序区
1≤j≤i-1 已经位于最终位置

⑴ 在一趟起泡排序中,若有多个记录位于最终位置,应如何记载?
⑵ 如何确定起泡排序的范围,使得已经位于最终位置的记录不参与下一趟排序?
⑶如何判别起泡排序的结束?关键问题⑴:如何记载一趟排序过程中交换的多个记录?解决方法:
设变量exchange记载记录交换的位置,则一趟排序后, exchange记载的一定是这一趟排序中记录的最后一次交换的位置,且从此位置以后的所有记录均已经有序。
交换 交换 交换

**关键问题⑴:**如何记载一趟排序过程中交换的多个记录?解决方法:
设变量exchange记载记录交换的位置,则一趟排序后, exchange记载的一定是这一趟排序中记录的最后一次交换的位置,且从此位置以后的所有记录均已经有序。
算法描述:
if (r[j]>r[j+1]){ r[j]←→r[j+1]; exchange=j;
}
**关键问题⑵:**如何确定起泡排序的范围?解决方法:
设bound位置的记录是无序区的最后一个记录,则每趟起泡排序的范围是r[1] ~ r[bound]。
在一趟排序后,从exchange位置之后的记录一定是有序的,所以bound=exchange。交换 交换 交换

**关键问题⑵:**如何确定起泡排序的范围?
解决方法:设bound位置的记录是无序区的最后一个记录,则每趟起泡排序的范围是r[1] ~ r[bound]。
在一趟排序后,从exchange位置之后的记录一定是有序的,所以bound=exchange。算法描述:
bound=exchange; for (j=1; j<bound; j++) if (r[j]>r[j+1]){ r[j]<==>r[j+1];
exchange=j;
}
**关键问题⑶:**如何判别起泡排序的结束?解决方法:
在每一趟起泡排序之前,令exchange的初值为0,在以后的排序过程中,只要有记录交换,exchange的值就会大于0。这样,在一趟比较完毕,就可以通过 exchange的值是否为0来判别是否有记录交换,从而判别整个起泡排序的结束。

**关键问题⑶:**如何判别起泡排序的结束?解决方法:
在每一趟起泡排序之前,令exchange的初值为0,在以后的排序过程中,只要有记录交换,exchange的值就会大于0。这样,在一趟比较完毕,就可以通过 exchange的值是否为0来判别是否有记录交换,从而判别整个起泡排序的结束。
算法描述:
while (exchange)
{
执行一趟起泡排序;
}
起泡排序算法
void BubbleSort(int r[ ], int n)
{ exchange=n;
while (exchange)
{
bound=exchange; exchange=0; for (j=1; j<bound; j++) if (r[j]>r[j+1]) {
r[j]←→r[j+1];
exchange=j;
}
}
}

起泡排序的时间性能分析最好情况(正序):
比较次数:n-1 5 4 3 2 1 移动次数:0 4 3 2 1 5 时间复杂度为O(n); 3 2 1 4 5
最坏情况(反序):比较次数:n∑-1(n-i) = n (n−1) 21 12 33 44 55 i =1 2
n-1 3n(n−1) 移动次数:∑ 3(n-i)=
i =1 2 时间复杂度为O(n2)。平均情况:时间复杂度为O(n2)。需扫描1趟需扫描n-1趟需扫描2趟需扫描n-2趟 造成不对称的原因是什么? 如何改变不对称性?
在排序过程中交替改变扫描方向——双向起泡排序

  1. 改进的着眼点:在起泡排序中,记录的比较和移动是在相邻单元中进行的,记录每次交换只能上移或下移一个单元,因而总的比较次数和移动次数较多。
    减少总的比较次数和移动次数 增大记录的比较和移动距离 较大记录从前面直接移动到后面较小记录从后面直接移动到前面
    快速排序的基本思想
    首先选一个轴值(即比较的基准),通过一趟排序将待排序记录分割成独立的两部分,前一部分记录的关键码均小于或等于轴值,后一部分记录的关键码均大于或等于轴值,然后分别对这两部分重复上述方法,直到整个序列有序。
    需解决的关键问题?

⑴如何选择轴值?
⑵如何实现分割(称一次划分)?
⑶如何处理分割得到的两个待排序子序列?
⑷如何判别快速排序的结束?关键问题⑴:如何选择轴值?选择轴值的方法:
1.使用第一个记录的关键码;
2.选取序列中间记录的关键码;
3.比较序列中第一个记录、最后一个记录和中间记录的关键码,取关键码居中的作为轴值并调换到第一个记录的位置;
4.随机选取轴值。
选取不同轴值的后果:
决定两个子序列的长度,子序列的长度最好相等。关键问题⑵:如何实现一次划分?

关键问题⑵:如何实现一次划分?
解决方法:
设待划分的序列是r[s] ~ r[t],设参数i,j分别指向子序列左、右两端的下标s和t,令r[s]为轴值,
(1) j从后向前扫描,直到r[j]<r[i],将r[j]移动到r[i] 的位置,使关键码小(同轴值相比)的记录移动到前面去;
(2) i从前向后扫描,直到r[i]>r[j],将r[i]移动到r[j] 的位置,使关键码大(同轴值比较)的记录移动到后面去;
(3) 重复上述过程,直到i=j。关键问题⑵:如何实现一次划分?算法描述:
int Partition(int r[ ], int first, int end)
{ i=first; j=end; //初始化 while (i<j)
{
while (i<j && r[i]<= r[j]) j–; //右侧扫描 if (i<j) {
r[i]←→r[j]; i++; //将较小记录交换到前面
}
while (i<j && r[i]<= r[j]) i++; //左侧扫描 if (i<j) {
r[j]←→r[i]; j–; //将较大记录交换到后面
}
}
retutn i; //i为轴值记录的最终位置
}
**关键问题⑶:**如何处理分割得到的两个待排序子序列?
解决方法:
对分割得到的两个子序列递归地执行快速排序。

**关键问题⑶:**如何处理分割得到的两个待排序子序列?算法描述:
void QuickSort (int r[ ], int first, int end )
{ pivotpos = Partition (r, first, end ); //一次划分 QuickSort (r, first, pivotpos-1);
//对前一个子序列进行快速排序
QuickSort (r, pivotpos+1, end );
//对后一个子序列进行快速排序
}
**关键问题⑷:**如何判别快速排序的结束?解决方法:
若待排序列中只有一个记录,显然已有序,否则进行一次划分后,再分别对分割所得的两个子序列进行快速排序(即递归处理)。关键问题⑷:如何判别快速排序的结束?
算法描述:
void QuickSort (int r[ ], int first, int end )
{//在序列 first~end中递归地进行快速排序 if (first < end) { pivotpos = Partition (r, first, end ); QuickSort (r, first, pivotpos-1);
QuickSort (r, pivotpos+1, end ); }
}
快速排序的时间性能分析
快速排序的递归执行过程可以用递归树描述。
例:{38, 27, 55, 50, 13, 49, 65}的快速排序递归树如下:

快速排序的时间性能分析
每次划分轴值的选取

快速排序递归的深度

快速排序的时间性能
快速排序的时间性能分析
最好情况:
每一次划分对一个记录定位后,该记录的左侧子表与右侧子表的长度相同,为O(nlog2n)。
T(n)≤2T(n/2)+n
≤2(2T(n/4)+n/2)+n=4T(n/4)+2n
≤4(2T(n/8)+n/4)+2n=8T(n/8)+3n
… … …
≤nT(1)+nlog2n=O(nlog2n)
快速排序的时间性能分析
最好情况:
每一次划分对一个记录定位后,该记录的左侧子表与右侧子表的长度相同,为O(nlog2n)。
最坏情况:
每次划分只得到一个比上一次划分少一个记录的子序列(另一个子序列为空),为 O(n2)。
n
i =1 2
平均情况:为O(nlog2n)。

选择排序的主要操作是选择,其主要思想是:每趟排序在当前待排序序列中选出关键码最小的记录,添加到有序序列中。
有序序列 无序序列

最小记录
…… … r …

基本思想:第i 趟在n-i+1(i=1,2,…,n-1)个记录中选取关键码最小的记录作为有序序列中的第i个记录。
需解决的关键问题?
⑴如何在待排序序列中选出关键码最小的记录? ⑵如何确定待排序序列中关键码最小的记录在有序序列中的位置?
简单选择排序示例

简单选择排序示例

**关键问题⑴:**如何在无序区中选出关键码最小的记录?
解决方法:
设置一个整型变量index,用于记录在一趟比较的过程中关键码最小的记录位置。

index	index index

**关键问题⑴:**如何在无序区中选出关键码最小的记录?
解决方法:
设置一个整型变量index,用于记录在一趟比较的过程中关键码最小的记录位置。
算法描述:
index=i;
for (j=i+1; j<=n; j++) if (r[j]<r[index]) index=j; 关键问题⑵:如何确定最小记录的最终位置?解决方法:
第i趟简单选择排序的待排序区间是r[i] ~ r[n],则r[i] 是无序区第一个记录,所以,将index所记载的关键码最小的记录与r[i]交换。
算法描述:
if (index!=i) r[i]←→r[index];
简单选择排序算法
void selectSort ( int r[ ], int n)
{ for ( i=1; i<n; i++)
{ index=i; for (j=i+1; j<=n; j++)
if (r[j]<r[index]) index=j;
if (index!=i) r[i]<==>r[index];
} }
简单选择排序算法的性能分析
移动次数:最好情况(正序):0次 
简单选择排序算法的性能分析
移动次数:
最好情况(正序):0次最坏情况:3(n-1)次比较次数: n
简单(i=1)选择排序(2)的时间复杂度为O(n2)。
空间性能:需一个辅助空间。
稳定性:是一种稳定的排序算法。
堆排序
改进的着眼点:如何减少关键码间的比较次数。若能利用每趟比较后的结果,也就是在找出键值最小记录的同时,也找出键值较小的记录,则可减少后面的选择中所用的比较次数,从而提高整个排序过程的效率。
减少关键码间的比较次数

查找最小值的同时,找出较小值
堆的定义堆是具有下列性质的完全二叉树:
每个结点的值都小于或等于其左右孩子结点的值
(称为小根堆),或
每个结点的值都大于或等于其左右孩子结点的值

  1. 小根堆的根结点是所有结点的最小者。 2. 较小结点靠近根结点,但不绝对。

堆的定义堆是具有下列性质的完全二叉树:
每个结点的值都小于或等于其左右孩子结点的值
(称为小根堆),或
每个结点的值都大于或等于其左右孩子结点的值

  1. 大根堆的根结点是所有结点的最大者。 2. 较大结点靠近根结点,但不绝对。
    (称为大根堆)。
    堆和序列的关系
    采用顺序存储
    1 2 3 4 5 6 7 8 9 10
    50 38 45 32 36 40 28 20 18 28
    将堆用顺序存储结构来存储,则堆对应一组序列。
    堆排序
    基本思想:首先将待排序的记录序列构造成一个堆,此时,选出了堆中所有记录的最大者,然后将它从堆中移走,并将剩余的记录再调整成堆,这样又找出了次小的记录,以此类推,直到堆中只有一个记录。
    需解决的关键问题?
    ⑴如何由一个无序序列建成一个堆(即初始建堆)?
    ⑵如何处理堆顶记录?
    ⑶如何调整剩余记录,成为一个新堆(即重建堆)?
    堆调整
    堆调整:在一棵完全二叉树中,根结点的左右子树均是堆,如何调整根结点,使整个完全二叉树成为一个堆?

堆调整——算法描述:
void sift ( int r[ ], int k, int m )
{//要筛选结点的编号为k,堆中最后一个结点的编号为m i=k; j=2i; temp=r[i]; //将筛选记录暂存
while (j<=m ) //筛选还没有进行到叶子
{
if (j<m && r[j]<r[j+1]) j++; //左右孩子中取较大者 if (r[i]>r[j]) break; else { r[i] ←→ r[j]; i=j; j=2
i;
}
}
r[i]=temp; //将筛选记录移到正确位置
}
**关键问题⑴:**如何由一个无序序列建成一个堆?

**关键问题⑴:**如何由一个无序序列建成一个堆?
算法描述:
for (i=n/2; i>=1; i–) sift(r, i, n) ;
最后一个结点(叶子)的序号是n,
则最后一个分支结点即为结点n的双亲,其序号是n/2。关键问题⑵:如何处理堆顶记录?

1      2      3     4      5      6	1      2      3     4      5      6

36 28 32 25 18 16

16 28 32 25 18 36

**关键问题⑵:**如何处理堆顶记录?
解决方法:
第 i 次处理堆顶是将堆顶记录r[1]与序列中第n-i+1个记录r[n-i+1]交换。
算法描述:
r[1]←→r[n-i+1]; 关键问题⑶:如何调整剩余记录,成为一个新堆?

**关键问题⑶:*如何调整剩余记录,成为一个新堆?
解决方法:
第 i 次调整剩余记录,此时,剩余记录有n-i个,调整根结点至第n-i个记录。
算法描述:
sift(r, 1, n-i);
堆排序算法
void HeapSort ( int r[], int n)
{
for (i=n/2; i>=1; i–) //初建堆 sift(r, i, n) ;
for (i=1; i>n; i++ )
{
r[1]←→r[n-i+1]; //移走堆顶 sift(r, 1, n-i); //重建堆
} }
堆排序算法的性能分析
第1个for循环是初始建堆,需要O(n)时间;
第2个for循环是输出堆顶重建堆,共需要取n-1次堆顶记录,第 i 次取堆顶记录重建堆需要O(log2i)时间,需要O(nlog2n)时间;
因此整个时间复杂度为O(nlog2n),这是堆排序的最好、最坏和平均的时间代价。
练习:对于无序序列:49,38,65,97,76,13,27,
49
,写出堆排序过程。

归并排序
归并排序的主要操作是归并,其主要思想是:将若干有序序列逐步归并,最终得到一个有序序列。
归并:将两个或两个以上的有序序列合并成一个有序序列的过程。
二路归并排序
基本思想:将一个具有n个待排序记录的序列看成是n个长度为1的有序序列,进行两两归并,得到 n/2个长度为2的有序序列,再进行两两归并,得到 n/4个长度为4的有序序列,……,直至得到一个长度为n的有序序列为止。
需解决的关键问题?
⑴如何将两个有序序列合成一个有序序列?
⑵怎样完成一趟归并?
⑶如何控制二路归并的结束?关键问题⑴:如何将两个有序序列合成一个有序序列?

关键问题⑴:如何将两个有序序列合成一个有序序列?

关键问题⑴:如何将两个有序序列合成一个有序序列?
60 20 31 5 44 55 65
60 20 31 5 44 55 65
20 60 5 31 44 55 65 i i j j j
在归并过程中,可能会破坏原来的有序序列,所以,将归并的结果存入另外一个数组中。
5 20 31 60 k 关键问题⑴:如何将两个有序序列合成一个有序序列?

**关键问题⑴:**如何将两个有序序列合成一个有序序列?

**关键问题⑴:**如何将两个有序序列合成一个有序序列?设相邻的有序序列为r[s] ~ r[m]和r[m+1] ~ r[t],归并成一个有序序列r1[s] ~ r1[t]

**关键问题⑴:**如何将两个有序序列合成一个有序序列?
算法描述:
void Merge (int r[ ], int r1[ ], int s, int m, int t )
{
i=s; j=m+1; k=s;
while (i<=m && j<=t)
{ if (r[i]<=r[j]) r1[k++]=r[i++]; else r1[k++]=r[j++];
}
if (i<=m) while (i<=m) //收尾处理
r1[k++]=r[i++]; //前一个子序列
else while (j<=t)
r1[k++]=r[j++]; //后一个子序列
}
关键问题⑵:怎样完成一趟归并?
60 20 31 5 44 55 65
60 20 31 5 44 55 65
20 60 5 31 44 55 65
5 20 31 60 44 55 65
在一趟归并中,除最后一个有序序列外,其它有序序列中记录的个数相同,用长度h表示。关键问题⑵:怎样完成一趟归并?
设参数i指向待归并序列的第一个记录,归并的步长是2h,在归并过程中,有以下三种情况:
①若i≤n-2h+1,则相邻两个有序表的长度均为h,执行一次归并,完成后i加2h,准备进行下一次归并;
h
i=1
5 31 44 55 65 n-2h+1=4 i n-2h+1 关键问题⑵:怎样完成一趟归并?
设参数i指向待归并序列的第一个记录,归并的步长是
2h,在归并过程中,有以下三种情况:
①若i≤n-2h+1,则相邻两个有序表的长度均为h,执行一次归并,完成后i加2h,准备进行下一次归并;算法描述:
while (i≤n-2h+1)
{
Merge (r, r1, i, i+h-1, i+2h-1); i+=2h;
}
关键问题⑵:怎样完成一趟归并?
设参数i指向待归并序列的第一个记录,归并的步长是
2h,在归并过程中,有以下三种情况:
②若i<n-h+1,则表示仍有两个相邻有序表,一个长度为h,另一个长度小于h,则执行两个有序表的归并,
完成后退出一趟归并。
h <h
i=4
20 60 5 31 44 55 65 n-2h+1=4 n-h+1=6 n-2h+1 i n-h+1 关键问题⑵:怎样完成一趟归并?
设参数i指向待归并序列的第一个记录,归并的步长是
2h,在归并过程中,有以下三种情况:
②若i<n-h+1,则表示仍有两个相邻有序表,一个长度为h,另一个长度小于h,则执行两个有序表的归并,完成后退出一趟归并。
算法描述:
if (i<n-h+1) Merge (r, r1, i, i+h-1, n); 关键问题⑵:怎样完成一趟归并?
设参数i指向待归并序列的第一个记录,归并的步长是
2h,在归并过程中,有以下三种情况:
③若i≥n-h+1,则表明只剩下一个有序表,直接将该有序表送到r1的相应位置,完成后退出一趟归并。
h i=9
n-h+1=8
n-h+1 i
关键问题⑵:怎样完成一趟归并?
设参数i指向待归并序列的第一个记录,归并的步长是
2h,在归并过程中,有以下三种情况:
③若i≥n-h+1,则表明只剩下一个有序表,直接将该有序表送到r1的相应位置,完成后退出一趟归并。
算法描述: if (i>=n-h+1)
for (k=i; k<=n; k++) r1[k]=r[k];
一趟归并排序算法
void MergePass (int r[ ], int r1[ ], int n, int h)
{ i=1;
while (i≤n-2h+1) //情况1
{
Merge (r, r1, i, i+h-1, i+2h-1); i+=2h;
}
if (i<n-h+1) Merge (r, r1, i, i+h-1, n); //情况2 else for (k=i; k<=n; k++) //情况3
r1[k]=r[k];
}
关键问题⑶:如何控制二路归并的结束?解决方法:
开始时,有序序列的长度h=1,结束时,有序序列的长度h=n,用有序序列的长度来控制排序的结束。 
关键问题⑶:如何控制二路归并的结束?算法描述:
void MergeSort (int r[ ], int r1[ ], int n )
{ h=1; while (h<n)
{
MergePass (r, r1, n, h); h=2h;
MergePass (r1, r, n, h);
h=2
h;
}
}
二路归并排序算法的性能分析
时间性能:
一趟归并操作是将r[1]r[n]中相邻的长度为h的有序序列进行两两归并,并把结果存放到r1[1]r1[n]中,这需要O(n)时间。整个归并排序需要进行log2n趟,因此,总的时间代价是O(nlog2n)。这是归并排序算法的最好、最坏、平均的时间性能。
空间性能:
算法在执行时,需要占用与原始记录序列同样数量的存储空间,因此空间复杂度为O(n)。

对排序算法应该从以下几个方面综合考虑:
⑴时间复杂性;
⑵空间复杂性;
⑶稳定性;
⑷算法简单性;
⑸待排序记录个数n的大小;
⑹记录本身信息量的大小;
⑺关键码的分布情况。

时间复杂度比较

排序方法 平均情况 最好情况 最坏情况
直接插入排序 O(n2) O(n) O(n2)
希 尔 排 序 O(nlog2n) O(n1.3) O(n2)
起 泡 排 序 O(n2) O (n) O(n2)
快 速 排 序 O(nlog2n) O(nlog2n) O(n2)
简单选择排序 O(n2) O(n2) O(n2)
堆 排 序 O(nlog2n) O(nlog2n) O (nlog2n)
归 并 排 序 O(nlog2n) O(nlog2n) O(nlog2n)
空间复杂度比较
排序方法 辅助空间
直 接 插 入 排 序 O(1)
希 尔 排 序 O(1)
起 泡 排 序 O(1)
快 速 排 序 O(log2n) ~O(n)
简 单 选 择 排 序 O(1)
堆 排 序 O(1)
归 并 排 序 O(n)
稳定性比较
所有排序方法可分为两类,
(1) 一类是稳定的,包括直接插入排序、起泡排序、直接选择排序和归并排序;
(2) 另一类是不稳定的,包括希尔排序、快速排序和堆排序。
算法简单性比较
从算法简单性看,
(1) 一类是简单算法,包括直接插入排序、直接选择排序和起泡排序,
(2) 另一类是改进后的算法,包括希尔排序、堆排序、快速排序和归并排序,这些算法都很复杂。
待排序的记录个数比较
从待排序的记录个数n的大小看,n越小,采用简单排序方法越合适,n越大,采用改进的排序方法越
合适。因为n越小,O(n2)同O(nlog2n)的差距越小,并且输入和调试简单算法比输入和调试改进算法要少用许多时间。

  1. 记录本身信息量比较

记录本身信息量越大,移动记录所花费的时间就越多,所以对记录的移动次数较多的算法不利。
排序方法 最好情况 最坏情况 平均情况
直接插入排序 O(n) O(n2) O(n2)
起泡排序 0 O(n2) O(n2)
直接选择排序 0 O(n) O(n)
关键码的分布情况比较
当待排序记录按关键码有序时:
•插入排序和起泡排序能达到O(n)的时间复杂度;
•对于快速排序而言,这是最坏的情况,此时的时间性能蜕化为O(n2);
•选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值