1、第9章 排序,排序,能使杂乱的无序数据变为有序。在将数据按某种规则排列有序后,就能大大提高对其查找和处理的效率。,本章主要介绍以下几个方面的内容: 排序的基本概念; 基本的插入排序(直接插入排序、折半插入排序、表插入排序)算法; 基本的交换排序(冒泡排序、快速排序)算法; 基本的选择排序(直接选择排序、堆排序)算法。,9.1 排序的基本概念,给定一组记录:r1、r2、rn,对应的关键字分别为:k1、k2、kn。将这些记录重新排列成顺序为:rs1、rs2、 rsn,使得对应的关键字满足: ks1ks2ksn的升序条件。这种重排 一组记录、使其关键字值具有非递减顺序的过程,就称为“排序”。让关键字
2、排序后表现出一种非递增关系也是可以的,也是排序,让关键字排序后表现出一种非递增关系也是可以的,也是排序。假定待排序记录中存在有相同关键字。若经某种排序之后,那些有相同关键字值的记录间的相对位置保持不变,那么称这种排序方法是“稳定的”,否则就是“不稳定的”。,所谓“内排序”,是指待排记录序列全部存放在内存,整个排序过程也都在内存里完成;所谓“外排序”,是指内存中容纳不下所有待排记录序列,排序过程中需要不断地与外存进行数据交换。本章只介绍有关内排序的算法。,9.2 插 入 排 序,插入排序的基本思想是:一趟一个地将待排记录按照关键字大小,插入到已经排好序的部分记录的适当位置中,使其成为一个新的有序
3、序列,直到所有待排记录全部插入完毕。,直接插入排序的基本思想是:初始时认可第1个记录已排好序,从第2个记录开始,总是用第i(2in)个记录与前面排好序的i1个记录的子序列进行顺序比较,插入到它的相应位置,得到i个排好序的子序列。这一过程直进行到最后一个记录时结束。,9.2.1 直接插入排序,例9-1 已知待排序记录的关键字为: 77,44,99,66,33,55,88,22利用直接插入排序完成对它们的排序,给出最终的排序结果。解:把所有待排记录存放在一个一维数组A里,如图9-1的“初始状态”所示。,图9-1 直接插入排序的过程,图9-2 数据元素的存储结构,一维数组元素的存储结构应该如图9-2
4、所示:,算法9-1 直接插入排序算法。已知有n个记录的待排序列存放在一维数组Ar里,该数组元素的存储结构如图9-2所示,要求对其进行直接插入排序。算法名为Ins_Sort(),参数为Ar、n。直接插入排序是一种稳定排序算法。,Ins_Sort(Ar, n) for (i=2; i=1) )Arj+1 = Arj;j-;Arj+1 = temp; /* 完成插入 */ ,图9-3 直接插入排序过程的局部,例9-2 有待排序的关键字序列如下: 138,219,365,513,491,412,953,276,977,738以图示的方法完成对它的直接插入排序。,图9-4 直接插入排序图示,例9-3 有
5、待排记录关键字序列: 49,66,35,76,25,85,76,32 利用直接插入排序算法进行排序,考察它的稳定性。,图9-5 直接插入排序算法的稳定性,折半插入排序在确定插入位置时,采用的比较方法是折半查找法(因为已排好序的子序列是一个有序表,可以对它实施折半查找),有时也称为“二分法插入排序”。,9.2.2 折半插入排序,算法9-2 折半插入排序算法。已知有n个记录的待排序列存放在一个一维数组Ar里,该数组元素的存储结构如图9-2所示,要求对其进行折半插入排序。算法名为Bin_Sort(),参数为Ar、n。,Bin_Sort(Ar, n) for (i=2; iArmid.key)low
6、= mid+1;elsehigh = mid -1;for (j = i-1; j=high+1; j-)Arj+1 = Arj;Arhigh+1 = temp; ,待排序的7个记录的关键字为:55、23、72、68、36、84、41,现在前6个关键字已经排好序。使用折半插入排序算法,来观察第7个关键字41的插入排序过程。 折半插入排序算法只适用于顺序存储,它是一种稳定的排序方法。,图9-6 折半插入排序的局部过程,表插入排序的基本思想,仍是不断地用被考察关键字与已排好序的子序列(比较范围)进行比较。只是是在记录的存储结构里增加了一个指针域,通过指针域的链接,让记录按照关键字的大小加以排列,使
7、得排序中没有了记录的移动。,9.2.3 表插入排序,例9-4 有关键字序列:55、44、72、68、36、84、41,采用表插入排序方法,完成它们的排序。图9-7 表插入排序时的结点存储结构,解:把记录存放在一个一维数组Ar里,其元素的存储结构如图9-7所示。数组初始时所有元素的next域都为1,如图9-8(a)所示。head是一个指针,它总是包含数组里关键字最小的那个记录的下标,初始时指向数组的第1个元素。,图9-6 折半插入排序的局部过程 v,算法9-3 表插入排序算法。已知有n个记录的待排序列存放在一个一维数组Ar里,该数组元素的存储结构如图9-6所示。要求对其进行表插入排序。算法名为L
8、in_Sort(),参数为Ar、n。表插入排序是一个稳定的排序算法。,Lin_Sort(Ar, n) head=1; /* head初始化 */for (i=1; i=Arptr.key) /* 通过比较,寻找插入位置 */qtr = ptr;ptr = Arptr.next;Ari.next = ptr; /* 完成与后面记录的链接 */if (qtr = -1) /* 如果插入位置在表头,则修改head */head = i;elseArqtr.next = i; /* 是其他位置时,完成前面的链接 */ ,9.3 交 换 排 序,交换排序的基本思想是:不断地对欲排序列中的两两记录做关键字
9、比较,若发现它们的大小次序相反,就进行交换,直到所有记录的关键字都满足排序要求时为止。,实施冒泡排序时,要对n个记录的关键字序列进行n1次扫描。每次扫描时,都从下到上对相邻的两个关键字进行比较,如果不符合由小到大的顺序,就将它们交换位置。,9.3.1 冒泡排序,这样,经过第1次扫描,就能从n1对相邻关键字的比较中,把关键字序列里最大的元素渐渐地移动到序列的最上边;经过第2次扫描,就能从n2对相邻关键字的比较中,把关键字序列里次大的元素渐渐地排到序列的次上位置;如此最多经过n1次扫描,n个关键字都排到了自己应该位于的位置。,例9-5 有待排的记录关键字序列:84、30、73、26、51,把它们存
10、放在一个一维数组A里,然后对它们进行冒泡排序,最后给出排序结果。,图9-9 冒泡排序过程示例,算法9-4 冒泡排序算法。已知有n个记录的无序序列,存放在一个一维数组Ar里。对它进行冒泡排序,并最后得到排序结果。算法名为Bub_Sort(),参数为Ar、n。冒泡排序是一种稳定排序算法。,Bub_Sort(Ar, n) for (i=1; iArj+1.key) /* 通过temp进行交换 */temp = Arj+1.key;Arj .key = Arj+1 .key;Arj+1 .key = temp;flag = 1;if (flag = 0) /* 若没有发生交换,就结束算法 */brea
11、k; ,例9-6 有待排记录的关键字序列:77、44、99、66、33、55、88、22,把它们存放在一个一维数组A里,然后对它们进行冒泡排序,最后给出排序结果。,快速排序的基本思想是:在待排序的n个关键字序列里,选择一个基准元素x,称其为“枢轴(Pivot)”。通常,把序列的第1个元素选为枢轴,也可以把位于序列中间位置的元素选为枢轴。然后把所有小于等于x的关键字调整到x的左边,把大于x的关键字调整到x的右边。这被称为是快速排序的一次划分。,9.3.2 快速排序,继续对左、右两个部分重复进行相同的这种划分过程,每次划分后就会让作为枢轴的关键字位于它最终应该在的位置上,直到最后分割的每一部分都只
12、有一个关键字时,才结束整个排序过程。,图9-11 快速排序的一次划分示意,算法9-5 快速排序一次划分算法。已知待排序的关键字序列存放在一维数组Ar里。现在要排序的子序列起始于第s个元素,终止于第t个元素。要对ArsArt里的关键字序列做快速排序的一次划分。算法名为Qukpass_Sort(),参数为Ar、s、t。,Qukpass_Sort(Ar, s, t) low = s; /* low为划分时序列的左边界 */high = t; /* high为划分时序列的右边界 */temp = Ars; /* 把枢轴值暂存于temp */while (lowlow) /* 把枢轴值存入正确位置 */
13、 ,例9-7 有关键字序列:70、85、69、35、93、23、71、68、55,存放在一个一维数组A中。试应用快速排序一次划分算法,完成对它们的一次划分。,图9-12 快速排序一次划分的过程,解:,对该关键字序列快速排序一次划分的结果为:55,68,69,35,23,70,71,93,85对关键字序列进行一次划分之后,枢轴到了最终应该在的位置上,其他的关键字被分列在左、右两个子序列里。对它们继续进行划分,直到每个子序列里只有一个关键字时止,排序就结束。,算法9-6 快速排序算法。 Quk_Sort(Ar, s, t) if (st)i = Qukpass_Sort(Ar, s, t);Quk
14、_Sort(Ar, s, i-1);Quk_Sort(Ar, i+1, t); 快速排序不是一种稳定的排序算法。,例9-8 有待排关键字序列:72、6、57、88、60、42、83、73、48、85,用图示说明对它实施快速排序算法的整个过程。解:对该待排关键字序列实施快速排序算法的整个过程,如图9-13所示。,图9-13 快速排序全过程图示,9.4 选 择 排 序,选择排序的基本思想是:第1趟从n个待排序的关键字序列中选出最小的,第2趟从n1个待排序的关键字序列中选出次小的,如此反复进行下去,直到整个排序结束。,直接选择排序的基本思想是:对由n个关键字组成的待排序列进行n1趟扫描,在每趟扫描过
15、程中,总是从未排序的部分里通过不断比较挑选小的关键字,记住它的位置。在一趟扫描结束、找到了最小关键字后才进行一次位置的交换操作,把最小关键字放置到它的正确位置上。,9.4.1 直接选择排序,例9-9 有待排序的关键字序列:15、23、14、28、13、17、20、42,对其进行直接选择排序,给出排序结果。解:经过7趟扫描,得到排好序的关键字序列,如图9-14里的“最终结果”所示。,图9-14 直接选择排序示例,算法9-7 直接选择排序算法。已知有n个记录的待排关键字序列被存放在一个一维数组Ar里,要对它实施直接选择排序,并得出最终排序结果。算法名为Sel_Sort(),参数为Ar、n。 直接选
16、择排序算法是不稳定的。,Sel_sort(Ar, n) for (i=1; i=n-1; i+) /* i控制n1趟扫描 */ small = i; /* 用变量small记住当前最小关键字的位置 */for (j = i+1; j=n; j+) /* j控制这趟扫描的比较范围 */if (Arj.keyArsmall.key) /* 如果发现更小者,随时修改small的值 */small = j; if (small != i) /* small与比较范围首元素下标不同,则交换 */temp = Ari; /* 交换是利用临时变量temp进行的 */Ari = Arsmall;Arsmall
17、 = temp; ,所谓“堆”,是一棵完全二叉树,且各结点的记录关键字值满足条件:根结点和任何分支结点的关键字值,均小于或等于其左、右孩子结点(如果有的话)的关键字值。,9.4.2 堆排序,1堆的定义,有n个记录的关键字序列:k1、k2、kn,若它们之间满足条件: kik2i,并且kik2i+1(i=1,2,n/2,且2i+1n),那么该序列被称为是一个“堆”。这时,若采用一个一维数组存放堆,那么它就是一棵完全二叉树的顺序存储,可以把关键字序列的每个ki看作是这棵有n个结点的完全二叉树的第i个结点,其中k1是该树的根结点。,例9-10 有关键字序列:10、23、18、68、94、72、71、8
18、3,存储在一个一维数组A里,如图9-15(a)所示。,图9-15 满足堆性质的完全二叉树,与图9-15(a)数组相对应的完全二叉树如图9-15(b)所示(结点旁的数字是相应数组元素的下标)。可以看出,该完全二叉树上结点的关键字之间,满足堆的条件,即有:10236883, 102394, 101871, 101872所以,图9-15(b)所示的这棵完全二叉树是一个堆。,对于堆,要注意如下三点: 在一个堆里,k1(即完全二叉树的根结点)是堆中最小的关键字值; 堆的任何一棵子树本身也是一个堆; 堆中任一结点的关键字值都不大于左、右 两个孩子的关键字值(如果有的话),但在左、右孩子的关键字值之间没有大
19、小关系存在。,堆里根结点的关键字值k1是堆中最小的,首先输出根结点k1,然后通过一定的规则对剩余的结点进行调整,使它们之间又成为一个堆,这样再输出新的根结点,如此下去,最终就可以达到由小到大的排序目的。这就是堆排序的基本思想。,2堆排序过程中的筛选,例9-11 关键字序列:10、23、18、68、94、72、71、83是一个堆,把它存储在一个一维数组A里,相应的完全二叉树如图9-16(a)所示,结点旁的数字是数组元素的下标。对它实施堆排序。,解:实施堆排序的过程如图9-16(b)图9-16(q)所示。输出堆顶元素10。为维护堆的性质,必须按照各关键字进行自上而下的适当调整,这种调整过程,被称为
20、“筛选”。一次筛选的目的,就是将不满足堆性质的完全二叉树根结点,调整到其适当的位置上,以便构成一个新堆。,图9-16 堆排序的完整过程图示,算法9-8 堆排序过程中的一次筛选算法。设一维数组Ar的元素Ars、Ars+1、Art,是以Ars为根结点的一棵完全二叉树。除Ars外,这些记录的关键字都满足堆的性质。要求通过筛选,使序列Ars、Ars+1、Art成为一个堆。算法名为Sift_Ar(),参数为Ar、s、t。,Sift_Ar(Ar, s, t) i = s; /* 变量i记住当前筛选根结点在数组的下标号 */temp = Ars; /* 临时存放原根结点的记录内容 */for (j=2*i;
21、 jArj+1.key) /* 左孩子关键字大,应该选择右孩子 */j+;if (temp.keyArj.key)break;Ari=Arj; /* 用选中的小的孩子结点代替根结点 */i=j; /* 继续往下进行筛选 */Ari = temp; /* 最终让原根结点进入自己的正确位置 */ ,创建堆的基本思想是:先把无序的关键字序列存放在一维数组里,并得到该数组对应的完全二叉树。对于这棵完全二叉树的每一个叶结点,以它们为根结点的子树显然已经满足堆的条件。从最后一个分支结点开始往前,不断地利用筛选算法,将一棵棵子树调整成为一个堆,直进行到完全二叉树的根结点为止。这样,一个堆就建立起来了。,3创
22、建一个堆,例9-12 有无序关键字序列:75、79、71、68、94、16、11、28,创建相对应的堆。解:将无序关键字序列存放于一个一维数组中,用它构造一个完全二叉树,如图9-17(a)所示。有了一次筛选算法,有了创建堆的算法,就可以给出堆排序的完整算法了。,图9-17 创建一个堆的过程图示,算法9-9 堆排序算法。已知有n个关键字的无序序列,存放在一个一维数Ar里。通过建堆、筛选,实现对它的堆排序。算法名为Heap_Sort(),参数为Ar、n。,Heap_Sort(Ar, n) for (i=n/2; i0; i-) /* 利用Sift_Ar(),将Ar1Arn建成堆 */Sift_Ar
23、(Ar, i, n);for (i=n; i1; i-) /* 堆顶元素Ar1与堆底元素Ari交换 */temp = Ar1;Ar1 = Ari;Ari = temp;Sift_Ar(Ar, 1, i-1); /* 利用Sift_Ar(),将Ar1Ari-1筛选成堆 */for (i=n; i=1; i-) /* 打印输出Ar1Arn的关键字值 */printf (“%d “, Ari.key); ,例9-13 已知记录的关键字序列:11、28、16、68、94、75、71、79,是一个堆,相应的完全二叉树和数组如图9-18(a)所示。对它进行堆排序。,图9-18 堆排序的部分过程图示,解:,也可以这样来定义堆。有n个记录的关键字序列:k1、k2、kn,若它们之间满足条件:kik2i,并且kik2i+1(i=1, 2, , n/2, 且2i+1n)那么该序列被称为是一个“堆”。前面定义的是所谓的“小根堆”,即堆的上面小,底下大;这里定义的是所谓的“大根堆”,即堆的上面大,下面小。堆排序是一种不稳定的排序算法。,