1.32k likes | 1.49k Vues
第 8 章 排 序. 8.1 排序技术概述. 8.2 插入排序. 8.3 选择排序. 8.4 交换排序. 8.5 归并排序. 8.6 基数排序. 8.7 外部排序概述. 8.8 本章小结. 8.1 排序技术概述. 从操作角度看,排序是线性结构的一种操作。 为了提高排序效率,人们已对排序进行了许多研究,提出了许多方法。. 排序就是按照某种规则,为一组给定的对象排列次序。 排序的 主要目的 是:在排好序的集合中能够快速查找(检索)一个元素。.
E N D
第8章 排 序 8.1 排序技术概述 8.2 插入排序 8.3 选择排序 8.4 交换排序 8.5 归并排序 8.6 基数排序 8.7 外部排序概述 8.8 本章小结
8.1 排序技术概述 从操作角度看,排序是线性结构的一种操作。 为了提高排序效率,人们已对排序进行了许多研究,提出了许多方法。
排序就是按照某种规则,为一组给定的对象排列次序。 排序的主要目的是:在排好序的集合中能够快速查找(检索)一个元素。
所谓“内部”排序,就是指整个排序过程都是在内存中进行的。所谓“内部”排序,就是指整个排序过程都是在内存中进行的。 如果排序的数据项很多,内存不足以存放得下全部数据项时,排序过程就需要对外存进行存取访问,也就是“外部”排序。 本章的内容以内部排序为主,对外部排序只进行简单地介绍。
我们把查找时关注或使用的数据叫做关键字(key),它可以是数据信息当中的一个属性,也可以是几个属性的组合。我们把查找时关注或使用的数据叫做关键字(key),它可以是数据信息当中的一个属性,也可以是几个属性的组合。 关键字可以代表其所在的那项数据信息。在这项数据信息当中,关键字所取的值叫做键值。
在本章中,为了突出排序算法本身的内容,我们简化各项数据的属性个数。在本章中,为了突出排序算法本身的内容,我们简化各项数据的属性个数。 假设待排序的数据都只有一个属性,这个属性就是关键字,并且关键字的类型是整型。
我们可以把排序看成是一种定义在某种数据集合上的操作。我们可以把排序看成是一种定义在某种数据集合上的操作。 本章所讲的各种内部排序,都可以认为是在一维数组这种线性数据结构上定义的操作。其功能是将一组任一排列的数据元素重新排列成一个按键值有序的序列。
对排序更为确切的定义: 假设{D1,D2,…,DN}为含有N项数据的序列,其中Di(1≤i≤N)表示序列中第i项数据,N项数据对应的键值序列为{K1,K2,…,KN}。 排序操作是将这些数据重新排列成一个按键值有序的新序列{Rp1,Rp2,…,RpN},使得相应的键值满足条件p1≤p2≤…≤pN(此时新序列成“升序”)或p1≥p2≥…≥pN(此时新序列成“降序”)。
注意:在上面定义叙述中所用到的≤或≥这两个比较符号,是通用意义上的关系比较符。注意:在上面定义叙述中所用到的≤或≥这两个比较符号,是通用意义上的关系比较符。 对于数值型信息,它是表示关系的小或大;对于字符串来说,它是指在字典中所排列位置的前或后。 对于不同类型的数据,我们可以规定不同的比较规则来反映≤或≥的含义。
如果在数据序列中,有多个具有相同键值的数据,在排序前后,这些数据之间的相对次序保持不变,这样的排序方法被称作是稳定的,否则被称为是不稳定的。如果在数据序列中,有多个具有相同键值的数据,在排序前后,这些数据之间的相对次序保持不变,这样的排序方法被称作是稳定的,否则被称为是不稳定的。
分析算法效率: 1. 从空间角度进行:主要是看 执行算法时所需附加空间的数量; 2. 从时间角度进行:从两种操作入手进行分析。 键值的比较次数 数据移动的次数
插入排序是指在待排序的序列中,分成两部分:前面为已经排好序的部分,后面为仍未排好序的部分。插入排序是指在待排序的序列中,分成两部分:前面为已经排好序的部分,后面为仍未排好序的部分。 每一轮都从没有排好序部分取出首元素按照大小将其插到前面已经排好序部分中的合适位置上。 这样前面已排序部分就多了一个元素,后面未排序部分同时少了一个元素。该排序过程不断进行,直到未排序部分的元素数目为零。
插入排序 将待排序的序列分成两部分:前面为已经排好序的部分,后面为仍未排好序的部分。 每一轮都从没有排好序部分取出首元素按照大小将其插到前面已经排好序部分中的合适位置上。 这样前面已排序部分就多了一个元素,后面未排序部分同时少了一个元素。该排序过程不断进行,直到未排序部分的元素数目为零。
排序过程如图8.1所示。 图(a)为该轮排序前的状态, 图(b)为该轮排序后的状态, 阴影表示本轮待排序的元素,在图(b)中它已经插入到已排好序部分的合适位置上。
注意: 在排序过程开始时,直接把序列中第一个元素认为是已排好序部分。另外,用整型数组代表排序的数据元素序列,数组下标为零的元素我们当作辅助空间,所以从数组下标是1的空间开始存储元素序列。
在把待插入元素从未排序部分移入已排好序部分时,有多种方法,下面逐一介绍。在把待插入元素从未排序部分移入已排好序部分时,有多种方法,下面逐一介绍。 8.2.1 直接插入排序 8.2.2 折半插入排序 8.2.3 2_路插入排序 8.2.4 表插入排序 8.2.5 希尔排序
8.2.1 直接插入排序 直接插入排序 又称简单插入排序。排序过程中,待插入元素先放在临时空间hold中,依次让它和前面的元素作比较,直到发现其应插入的位置,将其插入。
//不带哨兵的直接插入排序。 //数组rec为待排序的序列, //count为排序元素的个数 void insertSortSimple(int rec[], int count) { int curr , hold; for(int inx=2; inx<=count; inx++) //从第二个元素起依次做插入排序 { hold=rec[inx]; curr=inx-1;
//从已排好序部分的尾元素开始向前 //查找待插入位置 while(curr>=1 && hold<rec[curr]) { rec[curr+1]=rec[curr]; curr--; } rec[curr+1]=hold; //将待插入元素插到合适的位置 } }
算法中,while语句的条件表达式由两部分组成,curr>=1保证数组下标不会越界(curr不会被减到低于零,rec[curr]保证有意义),但这样做就会增加一次条件判断。算法中,while语句的条件表达式由两部分组成,curr>=1保证数组下标不会越界(curr不会被减到低于零,rec[curr]保证有意义),但这样做就会增加一次条件判断。 为了提高效率,我们把待插入元素先放在临时空间rec[0]中,依次让它和前面的元素作比较,直到发现其应插入的位置,将其插入。 注意rec[0]起到了“哨兵”的作用。因为while语句的条件表达式使得curr不会被减到低于零。
//改进后的直接插入排序 //数组rec为待排序的序列,count为排序元 //素的个数 insertSortSimple(int rec[], int count) { int curr; for(int inx=2; inx<=count; inx++) //从第二个元素起依次做插入排序 { rec[0]=rec[inx]; curr=inx-1;
//从已排好序部分的尾元素开始向前 //查找待插入位置 while(rec[0]<rec[curr]) { rec[curr+1]=rec[curr]; curr--; } rec[curr+1]=rec[0]; //将待插入元素插到合适的位置 } } 从while语句的条件表达式得知直接插入排序是稳定的排序算法 。
分析算法中元素比较与移动的次数 如果每轮待插入的元素都是在已排好序部分中的最大元素,那么每轮比较次数只有一次(只和已排好序部分的尾元比较一次),而且不用移动元素。N个元素的序列只做了N-1轮的操作,所以标准操作次数Tmin=(N-1)*1+(N-1)*0=N-1=O(N),显然这种方式对应的情况是原序列已经是升序。
如果原序列是降序,而最终结果应该是升序,故每一轮中待插入元素要和所有已排好序的元素进行比较(包括哨兵在内),而且这些元素都要依次后移(哨兵不后移),以便将首元位置空出,让待插元素插到最前面的位置。这样,如果原序列是降序,而最终结果应该是升序,故每一轮中待插入元素要和所有已排好序的元素进行比较(包括哨兵在内),而且这些元素都要依次后移(哨兵不后移),以便将首元位置空出,让待插元素插到最前面的位置。这样, 比较次数为: Tcompare=2+3+…+N=(N+2)*(N-1)/2 移动次数为: Tshift=1+2+…+(N-1)=N*(N-1)/2 所以标准操作次数: Tmax=Tcompare+Tshift=O(N2)。
注意: 上面计算移动记录次数的过程中,并没有考虑以下两个赋值操作:给哨兵赋值的操作和将待插元素插到正确位置的赋值操作。 如果将这两项计算在内,那么移动次数都应再加上2(N-1)的值。考虑到排序序列中各元素值是随机取值的,所以它们之间大小排列也是随机的,因此各种排列概率相同,我们取最差时间效率与最好时间效率的平均值做为直接插入排序的平均时间复杂度,为O(N2)。在算法中我们多开了一个辅助空间,故空间复杂度为O(1)。
8.2.2 折半插入排序 在向有序表中插入元素的过程中,直接插入排序采用的方法是:从后向前依次与有序表中元素作比较。没有充分利用“前部分已经有序”的特性,这种方法效率较低。
考虑改进的方法: 设定有序表长度为L,那么让待插元素x和处于有序表中间位置的元素y作比较: 如果待插元素x大于或等于y,则说明插入的位置一定在有序表的后半部分; 如果待插元素x较小,则说明插入的位置一定在有序表的前半部分。 无论哪种情况,我们都可以将可能插入的位置所在的空间长度降至原来长度的一半,故称“折半”插入排序。
对于长度为L的有序表,大约需要log2L次的比较就可以确定出待插元素的位置。因此对于N个元素的序列来说,进行N-1轮的所有比较次数为O(N*log2N),显然优于直接插入法中平均情况下的比较次数O(N2)。但是在确定了待插位置之后,元素移动的次数并没有降下来,所以时间复杂度仍为有O(N2)。对于长度为L的有序表,大约需要log2L次的比较就可以确定出待插元素的位置。因此对于N个元素的序列来说,进行N-1轮的所有比较次数为O(N*log2N),显然优于直接插入法中平均情况下的比较次数O(N2)。但是在确定了待插位置之后,元素移动的次数并没有降下来,所以时间复杂度仍为有O(N2)。 算法中仍旧多开了一个辅助空间,故空间复杂度为O(1),保持不变。
void insertSortBinary(int rec[], int count) { int hold; //当前正处理的元素 int btm,top,mid; for(int inx=2; inx<=count; inx++) //从第二个元素起依次做插入排序 { hold=rec[inx]; btm=1;top=inx-1; //从已排好序部分的尾元素开始向前 //查找待插入位置 while(btm<=top)
{ mid=(btm+top)/2; if(rec[mid]<hold) btm=mid+1; else top=mid-1; } for(int i=inx-1;i>=btm;i--) rec[i+1]=rec[i]; rec[btm]=hold; //将待插入元素插到合适的位置 } }
8.2.3 2_路插入排序 在折半插入排序的基础上,再针对数据元素的移动进行算法的改进,就得到2_路插入排序算法。
我们开设一个辅助数组temp,其大小为待排序元素的个数,把它当作是循环数组。我们开设一个辅助数组temp,其大小为待排序元素的个数,把它当作是循环数组。 将第一个元素放在数组temp[0]中,并把它当作是有序表中处于中间位置的元素。 从第二个元素开始,每个元素在做插入的时候,都先与temp[0]进行比较。 若是小于temp[0],则往它前面的有序表中插;否则,往其后面的有序表中插。 有序表首元素和尾元素的下标分别用first与last表示。显然,开始状态下,有序表中仅有temp[0],所以first=last=0。 由于多开了辅助数组,所以该算法的空间复杂度为O(N)。
//2_路插入排序 //数组rec为待排序的序列,count为排序元素的个数 void insertSortBinary(int rec[], int count) { int *temp=new int[count]; //分配辅助空间 temp[0]=rec[1]; //第一个元素直接放入辅助数组,形成初始的有序表 int first=last=0; //有序表首元和尾元的下标 for(int inx=2; inx<=count; inx++) //从第二个元素起依次做插入排序 { if(rec[inx]<temp[0]) { //新插元素比有序表中间元素还小 for(int i=first; temp[i]<=rec[inx]; i=(i+1)%count ) temp[(i-1+count)%count]=temp[i]; temp[(i-1+count)%count]=rec[inx]; //插入待插元素 first=(first-1+count)%count;
}else { //新插元素比有序表表中间元素还大 for(int i=last;temp[i]>rec[inx];i--) temp[i+1]=temp[i]; temp[i+1]=rec[inx]; //插入待插元素 last++; } } for(int inx=0; inx<count; inx++) rec[inx+1]=temp[inx]; //复制回原数组 delete[] temp; //释放空间 }
在上面的算法中,确实在一定程度上减少了数据移动的次数,但仍旧没有完全解决数据移动的问题。由第二章线性表中的知识,我们知道:要想彻底提高数据移动的效率,就应该使用链表结构的存储方式。下面就介绍用数组实现的静态链表完成的插入排序。在上面的算法中,确实在一定程度上减少了数据移动的次数,但仍旧没有完全解决数据移动的问题。由第二章线性表中的知识,我们知道:要想彻底提高数据移动的效率,就应该使用链表结构的存储方式。下面就介绍用数组实现的静态链表完成的插入排序。
8.2.4 表插入排序 当希望在排序过程中不移动记录时,我们可以借助链表增删节点时的思想,用数组来实现链表的特征。在链表中,每个节点都由数据域和地址域两部分组成。类似的,让数组元素也由数据域和地址域组成。一个元素地址域中记录的是逻辑上该元素直接后继的下标值。也就说,数组中物理地址相邻的两个元素在逻辑上不一定前后相接。
//静态链表节点(数组元素)的类型声明 struct SLNode { int value; //假定数据为整型 int next; //记录下一元素的地址下标 }; //静态链表插入排序。 //数组rec为待排序的序列,count为排序元素的个数 void insertSortStaticTable(int rec[], int count) { SLNode tmp=new SLNode[count+1]; //数据元素从1号下标开始存储 int inx; for(inx=1; inx<=count; inx++) tmp[inx].value=rec[inx]; tmp[0].next=1; //有序表中仅有一个元素,该元素下标为1, //tmp[0].next为头指针 tmp[1].next =0; //地址域为零表示该元素为尾元
int curr,pre;//存数组下标的变量,起指针变量的作用 for(inx=2; inx<=count; inx++) { pre=0; curr=tmp[0].next; //curr指向首元,pre为首元的前驱,用0表示空 while(curr!=0&& tmp[curr].value<=tmp[inx]) //有序表中找待插位置 { pre=curr; curr=tmp[curr].next;} tmp[inx].next=curr; tmp[pre].next=inx; //将当前元素插到静态链表中的合适位置 } for(inx=1,curr=tmp[0].next;inx<=count;inx++) { //遍历链表依次将元素移回 rec[inx]=tmp[curr].value; curr=tmp[curr].next; } delete[] tmp; }
上面的算法中,由于引入了静态链表这个辅助空间,故空间复杂度为O(N)。上面的算法中,由于引入了静态链表这个辅助空间,故空间复杂度为O(N)。 移动次数就是原数组与辅助数组之间的两次互相赋值,故为2N; 在关键字比较的操作上,效率与前述算法一致,故时间复杂度为O(N2)。
8.2.5 希尔排序 在分析直接插入排序的时间复杂度时,可以看到:如果数据序列本身已是升序的,那么时间复杂度为O(N)。因此,如果能够保证在使用直接插入排序之前,数据序列已经基本有序,则时间复杂度就会基本接近O(N)。借助于这种思路,我们希望一个乱序的数据序列在每一轮操作中,较大的数据尽快地跳到序列较后的部分,而较小的数据尽快地跃到序列较前的部分。这样经过几轮之后,序列已经基本有序,在此基础上再使用一次直接插入排序即可。
希尔排序(Shell Sort)又称“缩小增量排序”正是基于上述这种思想进行的排序操作。 该排序方法将序列中的元素分成小组,同组元素之间的跨度称作“增量”。对于同组的元素进行直接插入排序,每一轮对各组元素都进行一遍直接插入排序。每轮之后,都会缩小增量,减少组数。当经过几轮之后,数据各元素已经基本有序,这时候将增量缩小至1,也就是说所有元素都成了一组,在最后这一轮操作中,使用直接插入排序对所有元素进行排序即可。
由上面排序的实例,可以看出希尔排序不是稳定的排序方法。由上面排序的实例,可以看出希尔排序不是稳定的排序方法。 如果在N个元素进行排序时,增量inc按照规则 (inc0=N/3,inci+1=inci/2) 取值,那么希尔算法的实现如下:
//希尔插入排序 //数组rec为待排序的序列,count为排序元素的个数 void insertSortShell(int rec[], int count) { int hold,curr; //可以在下面的算法中用rec[0]作临时变量,代替hold for(int inc=count/3; inc>=1; inc/=2) //当增量为1时,作最后一次插入操作 for(int inx=inc+1; inx<=count; inx++) { hold=rec[inx]; curr=inx-inc; while(curr>=1 && hold<rec[curr]) { rec[curr+inc]=rec[curr]; curr-=inc; } rec[curr+inc]=hold; //将待插入元素插到合适的位置 } } }