排序
排序(sorting)又称为分类。概要地说,排序就是将待排序文件中的记录按排序码不减(或不增)的次序排列起来。
其确切定义为:
设{R1,R2,…,Rn}是由n个记录组成的文件,{K1,K2,…,Kn}是相应的排序码的集合。
所谓排序,就是确定1,2,…,n的一种排列p1,p2,…,pn,使得各排序码满足如下的非递减(或非递增)关系。
如果待排序文件中,存在多个排序码相同的记录,经过排序后这些记录仍保持它们原来的相对次序,则称这种排序算法是“稳定的”;
否则称该排序算法是“不稳定的”。
在排序过程中,整个文件都放在内存中处理的排序方法称为内排;
在排序过程中,不仅需要使用内存,而且还要使用外存的排序方法称为外排序。
按所采用的策略不同,排序方法可分为五类:插入排序、交换排序、选择排序、归并排序和基数排序。
排序的过程就是对待排序文件的处理。其处理方式与存储结构相关,主要有三种。
(1)对记录本身进行物理重排,经过比较和判断,把记录移到合适的位置;
(2)对文件的辅助表(例如由排序码和指向记录的指针组成的目录表)的表目进行物理地重排,只移动辅助表的表目,而不移动记录本身。
(3)既不移动记录本身,也不移动辅助表的表目,而是在记录或辅助表的表目之间增加一条链,排序过程只改变这条链接上的指针,用以表示被排的顺序。
-
插入排序(insertion sort)
每次将一个待排序的记录,按其排序码值的大小插到前面已经排序的子文件中的适当位置,直到全部记录插入完为止。本节主要讲述直接插入排序、希尔(Shell)排序和其他插入排序(包括折半插入排序和表插入排序)。
-
直接插入排序(straight insertion sort)
每步完成一个记录的插入,对于 n个记录的排序问题需要从 2 到 n 共 n -1 趟。当要插入第 i (2≤i≤n)个记录时,文件已分为两部分:其中,前i-1个记录已排好序(称为有序子文件)。 这时Ki与,,…,逐个进行比较,找到应该插入的位置,从该位置的记录到第i-1个记录,都往后顺移一个位置,然后将Ri插入。 这一趟的处理结果为其中前 i 个记录已为有序。 从上述过程可以看出,每趟完成一个记录的插入,而每插入一个记录,前边有序子文件的记录个数就增1, 因此,经过有限步之后,n个记录则都进入到有序子文件之中而达到全部有序。
该算法的附加空间为O(1),算法的平均时间复杂度也为O(n2)。
-
希尔排序
希尔(Shell)排序又称为缩小增量排序. Shell排序的平均时间复杂度为O(n1.3)首先取一个正整数 d1(d1< n),把文件的全部记录分成 d1个组; 所有距离为 d1倍数的记录都放在同一组中,在各组内进行排序; 然后取正整数d2<d1,重复上述的分组和排序过程; 直到取dlast=1,即所有记录都放在同一组中排序为止。 由于开始时d1的取值较大,每组中的记录个数较少,排序速度较快; 待到排序的后期,di的取值逐渐变小,每组中的记录个数逐渐变多,但由于有前面工作的基础, 大多数记录已基本有序,所以排序速度仍然很快。
-
折半插入排序
直接插入排序中,在插入Ri时,,…,已是排好序的。因此在插入Ri时,可改用折半比较的方法来寻找 Ri的插入位置。按这种方法进行的插入排序称为折半插入排序(binaryinsertion sort),也称为二分法插入排序。
时间复杂度为O(nlog2n)。 -
表插入排序
表插入排序(list insertion sort)的基本思想是:在每个结点中增加一个指针字段,在插入Ri(i=2,…,n)时,R1到Ri-1已经用指针按排序码值不减的次序链结起来,这时循链顺序比较,找到Ri应该插入(链入)的位置,然后做链表的插入,这样就得到从R1到Ri的一个通过链接指针排好序的链表。如此重复处理,直到把Rn插入链表中排好序为止。执行表插入排序可以用数组 R 存放待排序的文件,记录之间的链接关系用next 字段表示,next字段取值为数组元素的下标,头指针就放在R[0].next字段中。
这种形式的链表称作静态链表。
-
-
交换排序
交换排序(exchange sorting)的基本思想是:两两比较待排序记录的排序码,并交换不满足顺序要求(反序)的那些偶对,直到不再存在这样的偶对为止。本节介绍两种交换排序:冒泡排序和快速排序。-
冒泡排序
冒泡排序(bubble sort)又称为起泡排序,它是一种简单的交换排序方法。其具体做法是:设n个记录的待排序文件存放在数组R[1‥n] 中,首先比较Kn-1和Kn,如果Kn-1> Kn(发生逆序),则交换Rn-1和Rn;
然后Kn-2和Kn-1(可能是刚交换来的)做同样的处理;重复此过程直到处理完K1和 K2。上述的比较和交换的处理过程称为一次起泡。第一趟结果是将排序码最小的记录交换到文件第一个记录R1的位置(也是最终排序的位置),其他的记录大多数都向着最终排序的位置移动。但也可能出现个别的记录向着相反的方向移动的情况。第二趟再对R[2‥n] 部分重复上述处理过程,这一趟的结果是将排序码次最小的记录交换到文件第二个记录R2的位置。如此一趟一趟地进行下去……至多经过n-1趟(n-1次起泡)就可达到全部有序。冒泡排序算法的平均时间复杂度为O(n2),空间复杂度为O(1)。
-
快速排序
快速排序(quick sort)又称划分交换排序。其基本思想是:在待排序文件的n个记录中任取一个记录(例如就取第一个记录)作为基准, 将其余的记录分成两组,第一组中所有记录的排序码都小于或等于基准记录的排序码; 第二组中所有记录的排序码都大于或等于基准记录的排序码, 基准记录则排在这两组的中间(这也是该记录最终排序的位置); 然后分别对这两组记录重复上述的处理,直到所有的记录都排到相应的位置为止。
快速排序的时间复杂度为O(nlog2n)。
-
-
选择排序
选择排序(selection sort)基本思想是:第一趟在有n个记录的待排序文件中,选出排序码最小(大)的记录, 并把它与剩余的n-1个记录分开;然后在剩余的n-1个记录再选出排序码最小(大)的记录, 并把它与剩余的n-2个记录分开;依次重复下去,……,一般第i趟(i = 1,2,…,n-1)在当前剩余的 n- i+1 个待排序记录中选出排序码最小(大)的记录, 作为有序子文件第 i个记录。 待到第n-1趟结束时,剩余的待排序文件中只剩下一个记录, 它就是整个待排序文件中的排序码最大(小)的记录,至此排序已完成。 本节将介绍直接选择排序、树形选择排序和堆排序三种选择排序的方法。
-
直接选择排序
直接选择排序(straight selection sort)又称为简单选择排序(simple selection sort),它是一种简单的排序方法。
其做法是:首先在所有记录中选出排序码最小(大)的记录, 把它与第一个记录交换,然后在其余的记录中再选出排序码最小(大)的记录与 第二个记录交换,以此类推,直到所有记录排序完成。
-
树形选择排序
树形选择排序(tree selection sort),又称锦标赛排序(tournament sort)。 树形选择排序总的时间开销为O(nlog2n),总的附加空间量为2n-1。直接选择排序时,为了从n个排序码中找出最小的排序码,需要进行n-1次比较, 然后为在n-1个排序码中找出次最小的排序码需要进行n-2次比较。 树形选择排序的具体做法是:把n个排序码两两进行比较,取出⎡n/2⎤ 个较小的排序码作为第一步比较的结果保存下来,再把这 ⎡n/2⎤ 个排序码两两分组并进行比较,…, 如此重复,直到选出一个排序码最小的记录为止。 在选择次最小排序码时, 只要将结点中最小排序码改成+∞(实现时用机器最大数来代替), 重新进行比较,这时,实际上只要修改从树根到刚刚改为+∞的叶结点这条路径上的各结点的值, 其他结点均保持不变。如此反复,直到排序完成。
-
堆排序
堆排序(heap sort)是由它的发现者J.W.J.Williams 于1964 年命名的,是对树形选择排序的进一步改进。
使得时间开销与树形选择排序相同,也为O(nlog2n)。
同时又不需增加像树形选择排序那么多的附加存储空间,堆排序的附加存储空间仅为O(1)。堆排序的基本思想是: (1)将待排序文件的n个记录,利用堆的调整算法FilterDown( )建成初始堆; (2)输出堆顶记录; (3)对剩余的记录重新调整成堆;
-
-
归并排序
将已有序的子文件进行合并,得到完全有序的文件。 合并时只要比较各有序子文件的第一个记录的排序码,排序码最小的那个记录就是排序后的第一个记录; 取出这个记录,然后继续比较各个子文件的第一个记录,便可找出排序后的第二个记录; 如此继续下去,只要经过一趟扫描就可以得到最终的排序结果。 对于排序码任意排列的待排序文件进行归并排序时,可以把文件中的n个记录看成n个子文件。 每个子文件只包含一个记录,显然对于个子文件来说是有序的。 但是,要想只经过一趟扫描就将n个子文件全部归并成一个有序的文件显然是困难的。 通常,可以采用两两归并的方法,即每次将两个子文件归并成一个较大的有序子文件。 第一趟归并后,得到个长度为2的有序子文件(最后一个子文件长度可能为1); 在此基础上,再进行以后各趟的归并,每经过一趟后,子文件的个数约减少一半,而每个子文件的长度约增加一倍。 如此反复,直到最后一趟将两个有序子文件归并到一个文件中,这时整个排序工作就完成了。
时间代价为O(nlog2n),空间复杂度为O(n)。
-
基数排序
基数排序(radix sort)又称为桶排序(bucket sort), 是一种采用“分配”和“收集”的办法, 借助于多排序码排序的思想来实现对单排序码进行排序的方法。 是利用“分配”和“收集”两种操作对单排序码进行排序的一种内部排序的方法。 所以总的时间复杂度为 O(d(n+radix))。 算法所需要的附加存储空间是为每个记录增设的指针字段, 以及每一个队列的队头和队尾指针,总共为n+2radix。
-
外排序
外排序过程主要分为两个阶段: (1)根据为外排序所用的内存缓冲区的大小,将外存上含有n个记录的待排序文件划分成若干个子文件或段(segment), 依次读入内存并用有效的内排序方法对各段进行排序。然后将这些经过排序的段回写到外存,通常称这些经过排序的段(即有序子文件)为归并段或顺串(run)。 (2)对这些归并段进行逐趟归并,使归并段的长度逐趟增大, 直到最后归并成一个大归并段(即整个有序文件)为止。
-
2路平衡归并
由于内、外存在读写时间上存在着很大的差异,因此提高外排序速度的关键是减少对数据的扫描的遍数(即对顺串归并的趟数)。 因此,增大归并路数,可减少归并趟数,从而减少总的读写外存的次数d。 从表8-1中可以看出,采用6路归并比2路归并可减少近一半的读写外存的次数。 一般地,对m个顺串,做k路平衡归并,归并树可用正则k叉树(即只有度为0和度为k的结点的k叉树 )来表示。
-
k路平衡归并与败者树
败者树(tree of loser)实际上是一棵完全二叉树,它是树形选择排序的一种变型。 败者树就是在比赛(选择)树中,每个非叶结点均存放其两个子女结点中的败者,而让胜者去参加上一层的比赛。 叶结点指向对应缓冲区中的当前第一个记录。 此外,在根结点之上还增加进一个双亲结点,它为比赛的“冠军”。
-
最佳归并树
-