目录
一. 选择排序
(1)简单选择排序
(2)堆排序
二. 归并排序
三. 基数排序
四. 各种排序方法的比较
(1)时间性能
(2)空间性能
(3)排序方法的稳定性能
(4)关于“排序方法的时间复杂度的下限”
一. 选择排序
(1)简单选择排序
基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置。
基本操作:
1.首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换。
2.再通过n-2次比较,从剩余的n-1个记录中找出关键字次小的记录,将它与第二个记录交换。
3.重复上述操作,共进行n-1趟排序后,排序结束。
不难写出算法:
void SelectSort(SqList &L){
for(i=1; i<L.length; ++i){
k=i; //第i趟从第i个元素开始
for(j=i+1; j<=L.length; j++)
if(L.r[j].key < L.r[k].key) k=j; //记录最小值位置
if(k!=i) L.r[i]←—→L.r[k]; //交换
}
}
下面我们分析时间复杂度。对移动次数来说,最好情况是0,最坏情况是3(n-1),也就是每一趟都得移动(每次移动需要移动3次)。对比较次数来说,无论待排序列处于什么状态,选择排序所需进行的"比较”次数都相同,为。
上面的算法是不稳定排序(但是可以稳定化)。具体的说用数组实现的选择排序是不稳定的,用链表实现的选择排序是稳定的。例如,给定8,5,8*,7,9;第1次:5,8,8*,7,9;第2次:5,7,8*,8,9;从而可以验证它是不稳定的。
(2)堆排序
堆的定义:若n个元素的序列满足或,则分别称该序列为小根堆和大根堆。从堆的定义可以看出,堆实质是满足如下性质的完全二叉树:二叉树中任一非叶子结点均小于(大于)它的孩子结点。
显然,大根堆的根结点是最大值,小根堆的根结点是最小值。若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列又重建成一个堆,则得到n个元素的次小值(次大值)....如此反复,便能得到一个有序序列,这个过程称之为堆排序。
那么怎么重建呢?以小(大)根堆为例:
1.输出堆顶元素之后,以堆中最后一个元素(编号最大的元素)替代之;
2.然后将根结点值与左、右子树的根结点值进行比较,并与其中小(大)者进行交换;
3.重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”。
例如,对下面的小根堆,把13输出,最后一个元素97作为根结点,它的左右孩子是38和27,27较小,所以把97和27交换。此时97的左右孩子是65和49,49较小,把49和97交换,这个时候97已经是叶子结点就不用再操作了。
写出算法如下:
void HeapAdjust(elem R[], int s, int m){
/*已知R[s..m]中记录的关键字除R[s]之外均满足堆的定义,本函数调整R[s]的关键字,使R[s..m]成为一个大根堆*/
rc = R[s];
for (j=2*s; j<=m; j *= 2){ //沿key较大的孩子结点向下筛选
if (j < m && R[j] < R[j+1]) ++j; //j为key较大的记录的下标
if (rc >= R[j]) break; //rc大于左右孩子,这个时候已经符合要求,就不用做了
R[s] = R[j]; //较大的孩子结点往上升
s = j; //rc应插入在位置s上,更新s
}//for
R[s] = rc; //插入
}//HeapAdjust
HeapAdjust函数是一个用于调整堆的函数。它接受一个数组R,以及两个整数s和m作为参数。s表示要调整的子树的根节点的位置,m表示该子树的最后一个节点的位置。
首先,将根节点的值保存在变量rc中。然后,通过一个循环来比较根节点和其子节点的值。在循环中,变量j初始化为根节点的左子节点的位置(2*s),然后每次乘以2,即可得到下一个子节点的位置。在循环中,首先判断是否存在右子节点,并且右子节点的值是否大于左子节点的值。如果满足条件,则将j加1,即将j指向右子节点。然后,判断rc的值是否大于等于R[j]的值。如果满足条件,则退出循环。如果rc的值小于R[j]的值,则将R[j]的值赋给R[s],即将较大的子节点的值上移到根节点的位置。然后,将s更新为j,即将s指向较大子节点的位置。循环结束后,将rc的值赋给R[s],即将根节点的值放到合适的位置上。这样,HeapAdjust函数完成了对以s为根节点的子树的调整,使其满足堆的性质。
可以看出:对一个无序序列反复“筛选”就可以得到一个堆。即:从一个无序序列建堆的过程就是一个反复“筛选”的过程。我们重新考察堆的定义,显然:单结点的二叉树是堆,在完全二叉树中所有以叶子结点(序号i > n/2,这里是整除向下取整)为根的子树也是堆。这样,我们只需依次将以序号为n/2,n/2 - 1,.....1的结点为根的子树均调整为堆即可。即:对应由n个元素组成的无序序列,“筛选”只需从第n/2个元素开始。
由于堆实质上是一个线形表,那么我们可以顺序存储一个堆。下面以一个实例介绍建一个小根堆的过程。例如给定关键字为49,38,65,97,76,13,27,49的一组记录,将其按关键字调整为一个小根堆:
将初始无序的R[1]到R[n]建成一个小根堆,可用以下语句实现:
for(i = n/2 ; i >= 1; i--)
HeapAdjust (R, i, n);
上面我们了解了怎么建堆。若对一个无序存列建堆,然后输出根。重复该过程就可以由一个无需序列输出有序序列。实质上,堆排序就是利用完全二叉树中父结点与孩子结点之间的内在关系来排序的。
void HeapSort(elem R[]){ //对R[1]到R[n]进行堆排序
int i;
for (i = n/2; i>= 1; i--)
HeapAdjust(R, i, n);//建初始堆
for (i = n; i > 1; i--){ //进行n-1趟排序
Swap(R[1], R[i]); //根与最后一个元素交换,也就是把根结点输出并放在最后一个位置
HeapAdjust(R, 1, i-1); //对R[1]到R[i-1]重新建堆}
}//HeapSort
最后我们来研究时间复杂度。初始堆化所需时间不超过O(n),排序阶段(不含初始堆化)每次重新堆化所需时间不超过O(logn),则n-1次循环所需时间不超过O(nlogn)。因此:
Tw(n)=O(n)+ O(nlogn)= O(nlogn)
堆排序的时间主要耗费在建初始堆和调整建新堆时进行的反复筛选上。堆排序在最坏情况下,其时间复杂度也为O(nlog2n),这是堆排序的最大优点。无论待排序列中的记录是正序还是逆序排列,都不会使堆排序处于"最好"或"最坏"的状态。另外,堆排序仅需一个记录大小供交换用的辅助存储空间。
然而堆排序是一种不稳定的排序方法,它不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的。
二. 归并排序
基本思想:将两个或两个以上的有序子序列“归并”为一个有序予列。在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的有序子序列R[1..m]和R[m+1..n]归并为一个有序序列R[1..n]。
这种树称为归并树。n个元素归并排序只需要趟。下面讨论怎么把两个有序序列合并成一个有序序列。这里可以参考线性表的合并算法。设R[low]-R[mid]和R[mid+1]-R[high]为相邻,归并成一个有序序列R1[low] - R1[high].
若SR[i].key<=SR[j].key,则TR[k]=RS[i];k++;i++; 否则,TR[k]=SR[j];k++;j++;
归并排序的时间效率是O(nlog2n),空间效率是O(n),因为需要一个与原始序列同样大小的辅助序列(TR)。这正是此算法的缺点。归并排序算法是稳定的算法。
三. 基数排序
基本思想:分配+收集
基数排序也叫桶排序或箱排序:设置若干个箱子,将关键字为k的记录放入第k个箱子,然后在按序号将非空的连接。基数排序的数字是有范围的,均由0-9这十个数字组成,则只需设置十个箱子,相继按个、十、百...进行排序。例:给定待排序序列(614,738,921,485,637,101,215,530,790,306)。这里每一个箱子都是一个队列,遵循先进先出的原则:
至此排序完成!基数排序的时间效率:O(k*(n+m)),其中k:关键字个数(上面有3个关键字),m:关键字取值范围为m个值(上面为10),n:元素个数。这里,每一趟分配n个元素,收集m个桶,总共需要k遍。
空间效率:这里需要放置m个桶,回收的时候回收n个元素,则空间复杂度是O(n+m)。基数排序是稳定的。
四. 各种排序方法的比较
(1)时间性能
1.按平均的时间性能来分,有三类排序方法:
- 时间复杂度为O(nlogn)的方法有:快速排序、堆排序和归并排序,其中以快速排序为最好;
- 时间复杂度为O(n^2)的有:直接插入排序、冒泡排序和简单选择排序,其中以直接插入为最好,特别是对那些对关键字近似有序的记录序列尤为如此;
- 时间复杂度为O(n)的排序方法只有:基数排序。
2.当待排记录序列按关键字顺序有序时,直接插入排序和冒泡排序能达到到O(n)的时间复杂度;而对于快速排序而言,这是最不好的情况,此时的时间性能退化为O(n^2),因此是应该尽量避免的情况。
3.简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。
(2)空间性能
指的是排序过程中所需的辅助空间大小.
1.所有的简单排序方法(包括:直接插入、冒泡和简单选择)和堆排序的空间复杂度为O(1)
2.快速排序为O(logn),为栈所需的辅助空间
3.归并排序所需辅助空间最多,其空间复杂度为O(n)
4.链式基数排序需附设队列首尾指针,则空间复杂度为O(rd)
(3)排序方法的稳定性能
稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。
- 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
- 对于不稳定的排序方法,只要能举出一个实例说明即可。
- 快速排序和堆排序是不稳定的排序方法。
(4)关于“排序方法的时间复杂度的下限”
本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法,可以证明,这类排序法可能达到的最快的时间复杂度为O(nlogn)。(基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)。
可以用一棵判定树来描述这类基于“比较关键字”进行排序的排序方法。