目录
概述
-
排序的定义
将一组杂乱无章的数据按一定的规律顺次排列起来。
定义:设有记录序列:{R1、R2 … Rn}
其相应的关键字序列为:{K1、K2 … Kn};
若存在一种确定的关系:Kx<=Ky<=…<=Kz, 将记录序列{R1、R2 … Rn} 排成按该关键字有序的序列:{Rx、Ry… Rz}, 这样的操作称之为排序。
-
排序的分类
内部排序:若待排序记录都在内存中,称为内部排序
外部排序:若待排序记录一部分在内存,一部分在外存,则称为外部排序
稳定排序:假设在待排序的文件中,存在两个具有相同关键字的记录R(i)与R(j),其中R(i)位于R(j)之前。在用某种排序法排序之后, R(i) 仍位于R(j)之前,则称这种排序方法是稳定的
不稳定排序:反之,若R(j)领先于R(i),则称这种排序方法是不稳定的。
-
内部排序的算法分类
插入排序(希尔排序)
交换排序(快速排序)
选择排序(堆排序)
归并排序
基数排序
大多数排序算法都采用顺序结构,其类型结构定义如下
插入排序
主要思想:每次将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
插入排序有多种具体实现算法:
直接插入排序
折半插入排序
希尔排序
-
直接插入排序
直接插入排序(InsertionSort)是所有排序方法中最简单的一种排序方法。
基本原理:
(1) 顺次地从无序表中取出记录Ri(1≤i≤n),与有序表中记录的关键字逐个进行比较,找出其应该插入的位置
(2)将此位置及其之后的所有记录依次向后顺移一个位置
(3)将记录Ri插入到空出的位置
假设将n个待排序的记录顺序存放在长度为n+1的数组R[1]~R[n]中。R[0]作为辅助空间,用来暂时存储需要插入的记录,起监视哨的作用。直接插入排序算法如下:
直接插入排序的算法分析
时间性能:整个算法执行for循环n-1次,每次循环中的基本操作是比较和移动,其总次数取决于数据表的初始特性,可能有以下几种情况:
(1)当初始记录序列的关键字已是递增排列时,这是最好的情况。 算法中第二个for语句的循环体执行次数为0,因此,在一趟排序中关键字的比较次数为1,即R[0]的关键字与R[j]的关键字比较次数为1,而移动次数为2,即R[i]移动到R[0]中,R[0]移动到R[j+1]中。
所以整个排序过程中的比较次数和移动次数分别为(n-1)和2×(n-1),因而其时间复杂度为O(n)。
(2)当初始数据序列的关键字序列是递减排列时,这是最坏的情况。在第i次排序时,for语句内的循环体执行次数为i。
因此,关键字的比较次数为i,而移动次数为i+1。所以,整个排序过程中的比较次数和移动次数分别为:
(3)一般情况下,可认为出现各种排列的概率相同,因此取上述两种情况的平均值,作为直接插入排序关键字的比较次数和记录移动次数,约为n^2/4。所以其时间复杂度为O(n^2)。
总结:根据上述分析知,当原始序列越接近有序时,该算法的执行效率就越高。
空间性能:该算法仅需要一个记录的辅助存储空间,空间复杂度为O(1)。
稳定性:由于该算法在搜索插入位置时遇到关键字值相等的记录就停止操作,不会把关键字值相等的两个数据交换位置,所以该算法是稳定的。
-
折半插入排序
主要思想:就是在插入Ri时(此时R1,R2,…,Ri-1已排序),取R⎣i/2⎦的关键字K ⎣i/2⎦与Ki进行比较(⎣i/2⎦表示取不大于i/2 的最大整数),
如果Ki<K⎣i/2⎦,Ri的插入位置只能在R1和R⎣i/2⎦之间,则在R1和 R⎣i/2⎦-1之间继续进行折半查找,
否则在R⎣i/2⎦+1和Ri-1之间进行折半查找。如此反复直到最后确定插入位置为止。
优点:比较的次数大大减少,全部元素比较次数仅为O(nlog2n)。
时间效率:虽然比较次数大大减少,可惜移动次数并未减少,所以排序效率仍为O(n^2)。
空间效率:O(1)
稳定性:稳定
讨论:若记录是链表结构,用直接插入排序行否?折半插入排序呢?
答:直接插入不仅可行,而且还无需移动元素,时间效率更高!
但是链表无法“折半”!
-
希尔排序
希尔排序的基本思想是:先将整个待排记录序列分割成若干小组(子序列),分别在组内进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。希尔排序的具体步骤如下:
(1)首先取一个整数d1<n,称之为增量,将待排序的记录分成 d1个组,凡是距离为d1倍数的记录都放在同一个组,在各组 内进行直接插入排序,这样的一次分组和排序过程称为一趟希尔排序。
(2)再设置另一个新的增量d2<d1,采用与上述相同的方法继续进行分组和排序过程。
(3)继续取di+1<di,重复步骤(2),直到增量d=1,即所有记录都放在同一个组中。
分析:开始时dk的值较大,子序列中的对象较少,排序速度较快;随着排序进展,dk值逐渐变小,子序列中对象个数逐渐变多,由于前面工作的基础,大多数对象已基本有序,所以排序速度仍然很快。
希尔排序开始时增量较大,分组较多,每组的记录数较少,故各组内直接插入过程较快。随着每一趟中增量di逐渐缩小,分组数逐渐减少,虽各组的记录数目逐渐增多,但由于已经按di-1 作为增量排序,使序列表较接近有序状态,所以新的一趟排序过程也较快。
因此希尔排序在效率上较直接插入排序有较大的改进。希尔排序的时间复杂度约为O(n^(1..3)),它实际所需的时间取决于各次排序时增量的取值。大量研究证明,若增量序列取值较合理,希尔排序时关键字比较次数和记录移动次数约为O(nlog2n)。由 于其时间复杂度分析较复杂,在此不做讨论。
希尔排序会使关键字相同的记录交换相对位置,所以希尔排序 是不稳定的。
交换排序
主要思想:两两比较待排序记录的关键码,如果发生逆序(即排列顺序与排序后的次序正好相反),则交换之,直到所有记录都排好序为止。
交换排序主要的实现算法:
冒泡排序
快速排序
-
冒泡排序
基本思路:每趟不断将记录两两比较,并按前小后大 (或“前大后小”)规则交换
优点:每趟结束时,不仅能挤出一个大值到最后面位置, 还能同时部分理顺其他元素;一旦下趟没有交换发生,还可以提前结束排序。
前提:顺序存储结构
冒泡排序的算法分析
最好情况:初始排列已经有序,只执行一趟起泡,做n-1次关键码比较,不移动对象。
最坏情形:初始排列逆序,算法要执行n-1趟起泡,第i趟(1≤i<n) 做了n-i次关键码比较,执行了n-i次对象交换。
此时的总比较次数KCN和记录移动次数RMN为:
时间效率:O(n^2)——因为要考虑最坏情况
空间效率:O(1)——只在交换时用到一个缓冲单元
稳定性:稳定
-
快速排序
快速排序的基本思想是:
从待排记录序列中任取一个记录Ri作为基准(通常取序列中的第一个记录),将所有记录分成两个序列分组, 使排在Ri之前的序列分组的记录关键字都小于等于基准记录的关键字值Ri.key,排在Ri之后的序列分组的记录关键字都大于Ri.key,形成以Ri为分界的两个分组,此时基准记录Ri的位置就是它的最终排序位置。此趟排序称为第一趟快速排序。
然后分别对两个序列分组重复上述过程,直到所有记录排在相应的位置上为止。
选取基准
在快速排序中,选取基准常用的方法有:
(1)选取序列中第一个记录的关键字值作为基准关键字。 这种选择方法简单。但是当序列中的记录已基本有序时, 这种选择往往使两个序列分组的长度不均匀,不能改进排序的时间性能。
(2)选取序列中间位置记录的关键字值作为基准关键字。
(3)比较序列中始端、终端及中间位置上记录的关键字值, 并取这三个值中居中的一个作为基准关键字。
为了叙述方便,在下面的快速排序中,选取第一个记录的关键字作为基准关键字。
快速排序的实现
算法中记录的比较和交换是从待排记录序列的两端向中间进行的。设置两个变量low和high,其初值分别是n个待排序记录中第一个记录的位置号和最后一个记录的位置号。在扫描过程中,变量low,high的值始终表示当前所扫描分组序列的第一个和最后一个记录的位置号。设枢轴记录的关键字为pivotkey,并把该位置保存在R[0]中,然后每趟快速排序,进行如下操作:
(1)从序列最后位置的记录Rj开始依次往前扫描,若存在pivotkey≤Rj.key,则令high前移一个,即high--, 如此直到pivotkey>Rj.key或high=low为止。若low<high,将比枢轴记录小的记录移到低端。
(2)从序列最前位置的记录Ri开始依次往后扫描,若存在pivotkey≥R[i].key,则令low后移一个位置,即low++, 如此比较直到pivotkey<Ri.key或high=low为止。若low<high,将比枢轴记录大的记录移到高端。
重复(1)(2)直到low>=high的时候停止,此时,将枢轴记录到位,返回枢轴位置。
按照快速排序的基本思想,在一趟快速排序之后,需要重复(1),(2),直到找到所有记录的相应位置。显然, 快速排序是一个递归的过程。
快速排序算法的执行时间取决于基准记录的选择。一趟快速排序算法的时间复杂度为O(n)。下面分几种情况讨论整个快速排序算法需要排序的趟数:
(1)在理想情况下,每次排序时所选取的记录关键字值都是当前待排序列中的“中值”记录,那么该记录的排序终止位置应在 该序列的中间,这样就把原来的子序列分解成了两个长度大致相等的更小的子序列,在这种情况下,排序的速度最快。设完成n个记录待排序列所需的比较次数为C(n),则有:
C(n)≤n+2C(n/2)≤2n+4C(n/4)≤kn+nC (1)(k是序列的分解次数)
若n为2的幂次值且每次分解都是等长的,则分解过程可用一棵满二叉树描述,分解次数等于树的深度k=log2n,因此有:
C(n)≤nlog2n+nC(1)=O(nlog2n)
整个算法的时间复杂度为O(nlog2n)。
(2)在极端情况下,即每次选取的“基准”都是当前分组序列中关键字最小(或最大)的值,划分的结果是基准的前边(或右边)为空, 即把原来的分组序列分解成一个空序列和一个长度为原来序列长度减1的子序列。总的比较次数达到最大值:
如果初始记录序列已为升序或降序排列,并且选取的基准记录又是该序列中的最大或最小值,这时的快速排序就变成了“慢速排序”。整个算法的时间复杂度为O(n^2)。
为了避免这种情况的发生,可修改上面的排序算法,在每趟排序之前比较当前序列的第一、最后和中间记录的关键字,取关 键字居中的一个记录作为基准值调换到第一个记录的位置。
(3)一般情况下,序列中各记录关键字的分布是随机的,因而可以认为快速排序算法的平均时间复杂度为O(nlog2n)。实验证明,当n较大时,快速排序是目前被认为最好的一种内部排序方法。
在算法实现中需设置一个栈的存贮空间来实现递归,栈的大小取决于递归深度,最多不会超过n。若每次都选较长的分组序列进栈, 而处理较短的分组序列,则递归深度多不会超过log2n,因此快速排序需要的辅助存贮空间为O(log2n)。
快速排序算法是不稳定排序,对于有相同关键字的记录,排序后有可能颠倒位置。
选择排序
选择排序(Selection Sort)的基本思想是:不断从待排记录序列中选出关键字最小的记录插入已排序记录序列的后面,直到n个记录全部插入已排序记录序列中。
选择排序主要的实现算法:
简单选择排序
树形选择排序
堆排序
-
简单选择排序
简单选择排序(SimpleSelectionSort)也称直接选择排序,是选择排序中最简单直观的一种方法。其基本操作思想:、
①每次从待排记录序列中选出关键字最小的记录;
②将它与待排记录序列第一位置的记录交换后,再将其“插入”已排序记录序列(初始为空);
不断重复过程(1)和(2),直到待排记录序列为空
算法分析
1、简单选择排序算法关键字比较次数与记录的初始排列无关。假定整个序列表有n个记录,总共需要n-1趟的选择;第i趟选择具有最小关键字记录所需要的比较次数是n-i-1 次,总的关键字比较次数为:(n-1)+(n-2)+…+1=n(n-1)/2
而记录的移动次数与其初始排列有关。当这组记录的初始状态是按关键字从小到大有序时,每一趟选择后都不需要进行交换,记录的总移动次数为0,这是最好的情况;而最坏的情况是每一趟选择后都要进行交换,一趟交换需要移动记录3次。总的记录移动次数为3(n-1)。
所以,简单选择排序的时间复杂度为O(n^2)。
2、简单选择排序算法只需要一个临时单元用作交换,因此空间复杂度为O(1)。
3、由于在直接选择排序过程中存在不相邻记录之间的互换, 可能会改变具有相同关键字记录的相对位置,所以该算法是不稳定排序。
-
树形排序
树形选择排序(Tree Selection Sort)又称锦标赛排序(Tour nament Sort),是一种按照锦标赛的思想进行选择排序的方 法。
主要思想:首先对n个记录的关键进行两两比较,然后在其中的n/2个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录为止。
-
堆排序
堆排序方法是由J. Williams和Floyd提出的一种对直接排序的改进方法,它在选择当前最小关键字记录的同时, 还保存了本次排序过程所产生的比较信息。
堆排序(Heap Sort)是借助于完全二叉树结构进行的排序, 是一种树型选择排序。
其特点是:在排序过程中,将R[1..n]看成是一棵完全二叉树的顺序存储结构,利用完全二叉树双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大 (或最小)的记录。
堆的定义
n个元素序列{k1,k2,…,kn},当且仅当满足如下性质称为堆。
(1)这些元素是一棵完全二叉树中的结点,且对于i=1, 2,…, n,ki是该完全二叉树中编号为i的结点;
(2) ki≤k2i (或ki≥k2i )(1 ≤ i≤ ⎣n /2⎦)
(3)ki≤k2i+1(或ki≥k2i+l)(1≤i≤⎣n/2 ⎦)
根据堆的定义,可以推出堆的两个性质:
①堆的根结点是堆中元素值最小(或最大)的结点,称为堆顶元素。
②从根结点到每个叶结点的路径上,元素的排序序列都是递减(或递增)有序的。
堆排序的基本思想是:对一组待排序记录,首先把它们的关键字按堆定义排列成一个序列(称为初始建堆),堆顶元素为最小(或大)关键字的记录,将堆顶元素输出;然后对剩余的记录再建堆,得到次最小(或大)关键字记录;如此反复进行,直到全部记录有序为止,这个过程称为堆排序。
如何将一个无序序列建成一个堆?
具体做法是:把待排序记录存放在数组R[1..n]之中,将R看作一棵二叉树,每个结点表示一个记录,将第一个记录[R1]作为二叉树的根,以下各记录R[2..n]依次逐层从左到右顺序排列,构成一棵完全二叉树,任意结点R[i]的左孩子是R[2i],右孩子是R[2i+1],双亲是R[i/2]。
将待排序的所有记录放到一棵完全二叉树的各个结点中(注意:这时的完全二叉树并不具备堆的特征)。此时所有i >⎣n/2」的结点R[i]是叶子结点,因此以R[i]为根的子树已经是堆。
从i= ⎣n/2」的结点R[i]开始,比较根结点与左、右孩子的关键字值,若根结点的值大于左、右孩子中的较小者,则交换根结点和值较小孩子的位置,即把根结点下移,然后根结点继续和新的孩子结点比较,如此一层一层地递归下去,直到根结点下移到某一位置时,它的左、右子结点的值都大于它的值或者已成为叶子结点。这个过程称为“筛选”。
从一个无序序列建堆的过程就是一个反复“筛选”的过 程,“筛选”需要从i=⎣n/2」的结点R[i]开始,直至结点R[1] 结束。
对于已建好的堆,可以采用下面两个步骤进行排序:
(1)输出堆顶元素:将堆顶元素(第一个记录)与当前堆的最后一个记录对调。
(2)调整堆:将输出根结点之后的新完全二叉树调整为堆。
不断地输出堆顶元素,又不断地把剩余的元素建成新堆, 直到所有的记录都变成堆顶元素输出。
对堆排序算法主要由建立初始堆和反复重建堆两部分构成,它们均通过调用Sift()实现。假设具有n个记录的初始序列对 应的完全二叉树的深度为 h=⎣log2n⎦+1,则在建立初始堆时, 对每一个非叶子结点都要从上到下做“筛选”,则建立初始堆的总比较次数C1为:C1(n)≤4n,其时间复杂度为O(n)。
n个结点完全二叉树的深度为 ⎣log2n⎦+1,n-1次建新堆的总比较次数C2: C2(n)≤2(⎣log2n⎦+⎣log2(n-1)⎦+...+log2(2)≤2n*log2n
堆排序所需的关键字比较的总次数是: C1(n)+C2(n)= O(nlog2n)
类似地,可求出堆排序所需的记录移动的总次数为:O(nlog2n),因此堆排序的最坏时间复杂度为O(nlog2n)。堆排序算法 一般适合于待排序记录数比较多的情况。
堆排序需要一个辅助空间,所以空间复杂度为O(1)。
堆排序也是不稳定排序。
归并排序
归并排序(Merge Sort)是一种常用的排序方法,“归并” 的含义是将两个或两个以上的有序表合并成一个新的有序表。
二路归并排序的基本思想是:将有n个记录的待排序列看作n个有序子表,每个有序子表的长度为1,然后从第一个有序子表开始,把相邻的两个有序子表两两合并,得到n/2个长度为2或1的有序子表(当有序子表的个数为奇数时,最后一组合并得到的有序子表长度为1),这一过程称为一趟归并排序。
再将有序子表两两归并,如此反复,直到得到一个长度为n的有序表为止。上述每趟归并排序都需要将相邻的两个有序子表两两合并成一个有序表,这种归并方法称为二路归并排序。
1、两个有序表的合并算法Merge()
设线性表R[low..m],和R[m+1..high]是两个已排序的有序表,存放在同一数组中相邻的位置上,将它们合并到一个数组R1中,合并过程如下:
(1)比较线性表R[lowm]与R[m+1high]的第一个记录,将其中关键字值较小的记录移入表R1(如果关键字值相同,可将R[low..m]的第一个记录移入R1中)。
(2)将关键字值较小的记录所在线性表的长度减1,并将其后继记录作为该线性表的第一个记录。
反复执行上述过程,直到线性表R[low..m]或R[m+1..high] 之一成为空表,然后将非空表中剩余的记录移入R中,此时R1成为一个有序表。
2、一趟归并排序的算法MergePass()
一趟归并排序的算法调用n/(2*length)次归并算法merge(), 将R[1..n]中前后相邻且长度为length的有序子表进行两两归并, 得到前后相邻且长度为2*length的有序表,并存放在R1[1..n] 中。
如果n不是2*length的整数倍,则可出现两种情况:一种情况是,剩下一个长度为length的有序子表和一个长度小于 length的子表,合并之后其有序表的长度小于2*length;另一种情况是,只剩下一个子表,其长度小于等于length,此时不调用算法merge(),只需将其直接放入数组R1中,准备进行下 一趟归并排序。
显然,n个记录进行二路归并排序时,归并的趟数为O(log2n), 每趟归并中,关键字的比较次数不超过n,因此,二路归并排序的时间复杂度为O(nlog2n)。对序列进行归并排序时,除采用二路归并排序外还可以采用多路归并排序方法。
归并排序需要的辅助空间R1与待排序记录的数量相等,因此二路归并排序的空间复杂度为O(n)这,是常用的排序方法中空间复杂度最差的一种排序方法。
另外,从排序的稳定性看,二路归并排序是一种稳定的排序方法。
基数排序
基数排序是和前面所述各类排序方法完全不同的一种排序方法。基数排序(Radix Sort)是一种借助于多关键字排序的思想对单逻辑关键字进行排序的方法,即先将关键字分解成若干部分,然后通过对各部分关键字的分别排序,最终完成对全部记录的排序。
多关键字的排序
链式基数排序
-
多关键字的排序
多关键字的排序方法
最高位优先法(MSD):先对最高位关键字k1(如花色) 排序,将序列分成若干子序列,每个子序列有相同的k1值;然后让每个子序列对次关键字k2(如面值)排序, 又分成若干更小的子序列;依次重复,直至就每个子序列对最低位关键字kd排序;最后将所有子序列依次连接在一起成为一个有序序列。
最低位优先法(LSD):从最低位关键字kd起进行排序, 然后再对高一位的关键字排序,……依次重复,直至对最高位关键字k1排序后,便成为一个有序序列。
-
链式基数排序
基数排序:借助“分配”和“收集”对单逻辑关键字进行排序的一种方法。
链式基数排序:用链表作存储结构的基数排序。
链式基数排序算法分析
时间复杂度
– 分配:T(n)=O(n)
– 收集:T(n)=O(rd)
– T(n)=O(d(n+rd))
– 其中:n——记录数,
d——关键字数,
rd——关键字取值范围(如十进制为10)。
空间复杂度:
–S(n)=2rd个队列指针+n个指针域空间。
排序算法小结