一、基本概念和排序方法概述
1、排序的基本概念
排序 (Sorting) 是按关键字的非递减或非递增顺序对一组记录重新进行排列的操作
假设 =
(1 ≤ i ≤ n,1 ≤ j ≤ n, i ≠ j ),且在排序前的序列中
领先于
(即i<j)。若在排序后的序列中
仍领先于
,则称所用的排序方法是稳定的;反之,若可能使排序后的序列中
领先于
,则称所用的排序方法是不稳定的
排序算法的稳定性是针对所有记录而言的
根据在排序过程中记录所占用的存储设备,可将排序方法分为两大类:
- 一类是内部排序,指的是待排序记录全部存放在计算机内存中进行排序的过程
- 另一类是外部排序,指的是待排序记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中尚需对外存进行访问的排序过程
2、内部排序方法的分类
内部排序的过程是一个逐步扩大记录的有序序列长度的过程
在排序的过程中, 可以将排序记录区分为两个区域:有序序列区和无序序列区
使有序区中记录的数目增加一个或几个的操作称为一趟排序
根据逐步扩大记录有序序列长度的原则不同, 可以将内部排序分为以下几类:
- 插入类 :将无序子序列中的一个或几个记录 “插入” 到有序序列中,从而增加记录的有序子序列的长度。主要包括直接插入排序、折半插入排序和希尔排序
- 交换类 :通过 “交换” 无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。主要包括冒泡排序和快速排序
- 选择类 :从记录的无序子序列中 “选择” 关键字最小或最大的记录,并将它加入到有序子序列中, 以此方法增加记录的有序子序列的长度。主要包括简单选择排序、树形选择排序和堆排序
- 归并类 :通过 “归并” 两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。2-路归并排序是最为常见的归并排序方法
- 分配类 :是唯一一类不需要进行关键字之间比较的排序方法,排序时主要利用分配和收集两种基本操作来完成。基数排序是主要的分配类排序方法
3、待排序记录的存储方式
顺序表:记录之间的次序关系由其存储位置决定,实现排序需要移动记录
链表:记录之间的次序关系由指针指示,实现排序不需要移动记录,仅需修改指针即可。这种排序方式称为链表排序
待排序记录本身存储在一组地址连续的存储单元内,同时另设一个指示各个记录存储位置的地址向量,在排序过程中不移动记录本身,而移动地址向撒中这些记录的 “地址” ,在排序结束之后再按照地址向量中的值调整记录的存储位置。这种排序方式称为地址排序
待排序记录的数据类型定义为:
#define MAXSIZE 20 // 顺序表的最大长度
typedef int KeyType; // 定义关键字类型为整型
typedef struct{
KeyType key; // 关键字项
InfoType otherinfo; // 其他数据项
}RedType; // 记录类型
typedef struct{
RedType r[MAXSIZE+1]; // r[0]闲置或用做哨兵单元
int length; // 顺序表长度
}SqList; // 顺序表类型
4、排序算法效率的评价指标
- 执行时间
对于排序操作,时间主要消耗在关键字之间的比较和记录的移动上(这里,只考虑以顺序表方式存储待排序记录),排序算法的时间复杂度由这两个指标决定。因此可以认为,高效的排序算法的比较次数和移动次数都应该尽可能的少
- 辅助空间
空间复杂度由排序算法所需的辅助空间决定。辅助空间是除了存放待排序记录占用的空间之外,执行算法所需要的其他存储空间。理想的空间复杂度为 O(1), 即算法执行期间所需要的辅助空间与待排序的数据量无关
二、插入排序
插入排序的基本思想是:每一趟将一个待排序的记录,按其关键字的大小插入到已经排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止
1、直接插入排序
直接插入排序 (Straight Insertion Sort) 是一种最简单的排序方法,其基本操作是将一条记录插入到已排好序的有序表中,从而得到一个新的、记录数量增 1 的有序表
void InsertSort(SqList &L){
// 对顺序表L做直接插入排序
for(i = 2;i <= L.length;++i){
if(L.r[i].key < L.r[i - 1].key){ // "<",需将 r[i]插人有序子表
L.r[0] = L.r[i]; // 将待插人的记录暂存到监视哨中
L.r[i] = L.r[i-1]; // r[i-1]后移
for(j = i -2; L.r[0].key<L.r[j].key; --j) // 从后向前寻找插入位置
L.r[j+1] = L.r[j]; // 记录逐个后移,直到找到插人位置
L.r[j+1] = L.r[0]; // 将r[0]即原r[i],插人到正确位置
}
}
}
直接插入排序的时间复杂度为
直接插入排序只需要一个记录的辅助空间 r[0],所以空间复杂度为 O(1)
特点:
- 稳定排序
- 算法简便,且容易实现
- 也适用于链式存储结构,只是在单链表上无需移动记录,只需修改相应的指针
- 更适合于初始记录基本有序的情况,当初始记录无序,n较大时,此算法时间复杂度较
- 高,不宜采用
2、折半插入排序
void BInsertSort(SqList &L){
// 对顺序表L做折半插入排序
for(i = 2;i <= L.length;++i){
L.r[0] = L.r[i]; // 将待插人的记录暂存到监视哨中
low = 1;
high = i - 1; // 置查找区间初值
while(low <= high){ // 在r[low..high]中折半查找插入的位置
m = (low + high) / 2; // 折半
if(L.r[0].key < L.r[m].key)
high = m - 1; // 插入点在前一子表
else
low = m + 1; // 插入点在后一子表
}
for(j = i-1; j>=high+1; --j)
L.r[j+1] = L.r[j]; // 记录后移
L.r[high+1] = L.r[0]; // 将r[0]即原r[i], 插入到正确位置
}
}
折半插入排序的时间复杂度为
折半插入排序只需要一个记录的辅助空间 r[0],所以空间复杂度为 O(1)
特点:
- 稳定排序
- 因为要进行折半查找,所以只能用于顺序结构,不能用于链式结构
- 适合初始记录无序、n较大时的情况
3、希尔排序
希尔排序(Shell's Sort)又称 “缩小增量排序”(Diminishing Increment Sort), 是插入排序的一种
希尔排序实质上是采用分组插入的方法
先将整个待排序记录序列分割成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组。 这样当经过几次分组排序后,整个序列中的记录“基本有序” 时,再对全体记录进行一次直接插入排序
希尔对记录的分组,不是简单地“逐段分割”,而是将相隔某个“增量”的记录分成一组
void ShellInsert(SqList &L,int dk){
// 对顺序表 L 做一趟增量是 dk 的希尔插入排序
for(i = dk+1; i <= L.length; ++i){
if(L.r[i].key < L.r[i-dk].key){
L.r[0] = L.r[i];
for(j = i-dk; j>0 && L.r[0].key < L.r[j].key; j -= dk){
L.r[j+dk] = L.r[j];
}
L.r[j+dk] = L.r[0];
}
}
}
void ShellSort(SqList &l,int td[],int t){
// 按增批序列 dt[0 .. t-1] 对顺序表 L作 t 趟希尔排序
for(k=0; k<t; ++k){
ShellInsert(L,td[k]);
}
}
希尔排序的平均时间复杂度为
希尔排序只需要一个辅助空间r[0], 空间复杂度为 O(1)
特点:
- 记录跳跃式地移动导致排序方法是不稳定的
- 只能用于顺序结构,不能用于链式结构
- 增量序列可以有各种取法,但应该使增量序列中的值没有除1之外的公因子,并且最后一个增量值必须等于1
- 记录总的比较次数和移动次数都比直接插入排序要少,n越大时,效果越明显。所以适合初始记录无序、n较大时的情况
三、交换排序
交换排序的基本思想是:两两比较待排序记录的关键字,一旦发现两个记录不满足次序要求时则进行交换,直到整个序列全部满足要求为止
1、冒泡排序
冒泡排序(Bubble Sort)是一种最简单的交换排序方法,它通过两两比较相邻记录的关键字, 如果发生逆序,则进行交换,从而使关键字小的记录如气泡一般逐渐往上 "漂浮"(左移),或者使关键字大的记录如石块一样逐渐向下 "坠落”(右移)
void BubbleSort(SqList &L){
// 对顺序表L做冒泡排序
m = L.length - 1;
flag = 1; // flag用来标记某一趟排序是否发生交换
while((m>0) & (flag==1)){
flag = 0; // flag置为0, 如果本趟排序没有发生交换,则不会执行下一趟排序
for(j = 1;i <= m;j++){
if(L.r[j].key > L.r[j+1].key){
flag = 1; // flag置为1, 表示本趟排序发生了交换
t = L.r[j];
L.r[j] = L.r[j+1];
L.r[j+1] = t; // 交换前后两个记录
}
--m;
}
}
}
冒泡排序的时间复杂度为
冒泡排序只有在两个记录交换位置时需要一个辅助空间用做暂存记录,所以空间复杂度为 O(1)
特点:
- 稳定排序
- 可用于链式存储结构
- 移动举例次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n 较大时,此算法不宜采用
2、快速排序
快速排序 (Quick Sort) 是由冒泡排序改进而得的。在冒泡排序过程中,只对相邻的两个记录进行比较,因此每次交换两个相邻记录时只能消除一个逆序。如果能通过两个(不相邻)记录的一次交换,消除多个逆序,则会大大加快排序的速度。快速排序方法中的一次交换可能消除多个逆序
步骤:在待排序的 n 个记录中任取一个记录(通常取第一个记录)作为枢轴(或支点),设其关键字为 pivotkey。经过一趟排序后,把所有关键字小于 pivotkey 的记录交换到前面,把所有关键字大于 pivotkey 的记录交换到后面,结果将待排序记录分成两个子表,最后将枢轴放置在分界处的位置。然后,分别对左、右子表重复上述过程,直至每一子表只有一个记录时,排序完成
int Partition(SqList &L,int low,int high){
// 对顺序表L中的子表r[low..high]进行一趟排序,返回枢轴位置
L.r[0] = L.r[low];
pivotkey = L.r[low].key;
while(low < high){
while(low<high && L.r[high].key>=pivotkey)
--high;
L.r[low] = L.r[high];
while(low<high && L.r[low].key<=pivotkey)
++low;
L.r[high] = L.r[low];
}
L.r[low] = L.r[0];
return low;
}
void QSort(SqList &L,int low,int high){
// 调用前置初值:low=l; high=L.length;
// 对顺序表L中的子序列L.r[low .. high]做快速排序
if(low<high){
pivotloc = Partition(L,low,high);
QSort(L,low,pivotloc-1);
QSort(L,pivotloc-1,high);
}
}
void QuickSort(SqList &l){
// 对顺序表L做快速排序
Sort(L,1,L.length);
}
快速排序的平均时间复杂度为
快速排序是递归的,执行时需要有一个栈来存放相应的数据。最大递归调用次数与递归树
的深度一致,所以最好情况下的空间复杂度为 ,最坏情况下为 O(n)
特点:
- 记录非顺次的移动导致排序方法是不稳定的
- 排序过程中需要定位表的下界和上界,所以适合用于顺序结构,很难用于链式结构
- 当 n 较大时,在平均情况下快速排序是所有内部排序方法中速度最快的一种,所以其适合初始记录无序、n较大时的情况
四、选择排序
选择排序的基本思想是:每一趟从待排序的记录中选出关键字最小的记录,按顺序放在已
排序的记录序列的最后,直到全部排完为止
1、简单选择排序
简单选择排序 (SimpleSelection Sort) 也称作直接选择排序
void SelectSort(SqList &L){
// 对顺序表L做简单选择排序
for(i=1; i<L.length; ++i){ // 在L.r[i.. L.length]中选择关键字最小的记录
k=i;
for(j=i+1; j<=L.length; ++j){
if(L.r[j].key < L.r[k].key)
k=j; // k指向此趟排序中关键字最小的记录
if(k!=j){
t = L.r[i];
L.r[i] = L.r[k];
L.r[k] = t; // 交换r[i]与r[k]
}
}
}
}
简单选择排序的时间复杂度也是
简单选择排序只有在两个记录交换时需要一个辅助空间,所以空间复杂度为 O(1)
特点:
- 稳定排序
- 可用于链式存储结构
- 移动记录次数较少,当每一记录占用的空间较多时,此方法比直接插入排序快
2、树形选择排序
树形选择排序(Tree Selection Sort), 又称锦标赛排序(Tournament Sort), 是一种按照锦标赛的思想进行选择排序的方法
对 n 个记录的关键字进行两两比较,然后在其中 个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录为止
树形选择排序的平均时间复杂度为
这种排序方法尚有辅助存储空间较多、和“最大值”进行多余的比较等缺点
3、堆排序
堆排序 (Heap Sort) 是一种树形选择排序,在排序过程中,将待排序的记录 r[1..n] 看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录
堆的定义:n 个元素的序列称之为堆
可以通过构造小根堆得到一个非递增的有序序列
- 调整堆
void HeapAdjust(SqList &L,int s,int m){
// 假设r[s+l. .m]已经是堆,将r[s..m]调整为以r[s]为根的大根堆
rc = L.r[s];
for(j = 2*s;j <= m;j *= 2){ // 沿key较大的孩子结点向下筛选
if(j<m && L.r[j].key < L.r[j+1].key)
++j; // j为key较大的记录的下标
if(rc.ley >= L.r[j].key)
break; // rc应插入在位置s上
L.r[s] = rc;
}
}
- 建初堆
要将一个无序序列调整为堆,就必须将其所对应的完全二叉树中以每一结点为根的子树都调整为堆
显然,只有一个结点的树必是堆,而在完全二叉树中,所有序号大于 的结点都是叶子,因此以这些结点为根的子树均已是堆。这样,只需利用筛选法,从最后一个分支结点
开始,依次将序号为
,
,… ,1 的结点作为根的子树都调整为堆即可
void CreatHeap(SqList &L){
// 把无序序列L.r[l..n]建成大根堆
n = L.length;
for(i=n/2; i>0; --i)
HeapAdjust(L,i,n); // 反复调用HeapAdjust
}
堆排序算法的实现
根据前面堆排序算法步骤的描述,可知堆排序就是将无序序列建成初堆以后,反复进行交换和堆调整
void HeapSort(SqList &L){
// 对顺序表L进行堆排序
CreatHeap(L); // 把无序序列L.r [1 ... L.length]建成大根堆
for(i=L.length; i>1; --i){
x = L.r[1];
L.r[1] = L.r[i];
L.r[i] = x; // 将堆顶记录和当前未经排序子序列L.r[l..i]中最后一个记录互换
HeapAdjust(L,1,i-1); // 将L.r[l..i-l]重新调整为大根堆
}
}
堆排序的运行时间主要耗费在建初堆和调整堆时进行的反复 “筛选” 上
堆排序在最坏的情况下,其时间复杂度也为
仅需一个记录大小供交换用的辅助存储空间,所以空间复杂度为O(1)
特点:
- 不稳定排序
- 只能用于顺序结构,不能用于链式结构
- 初始建堆所需的比较次数较多,因此记录数较少时不宜采用。堆排序在最坏情况下时间复杂度为
, 相对于快速排序最坏情况下的
而言是一个优点,当记录较多时较为高效
五、归并排序
归并排序(Merging Sort)就是将两个或两个以上的有序表合并成一个有序表的过程
将两个有序表合并成一个有序表的过程称为 2-路归并
归并排序算法的思想是:假设初始序列含有 n 个记录,则可看成是 n 个有序的子序列,每个子序列的长度为 1,两两归并,得到
个长度为 2 或 1 的有序子序列;再两两归并,……, 如此重复,直至得到一个长度为 n 的有序序列为止
相邻两个有序子序列的归并
void Merge(RedType R[],RedType &T[],int low,int mid,int high){
// 将有序表 R[low..mid]和R[mid+l..high]归并为有序表 T[low..high]
i = low;
j = mid + 1;
k = low;
while(i <= mid && j <= high){ // 将R中记录由小到大地并入T中
if(R[i].key <= R[j].key)
T[k] = R[i+1];
else
T[k] = R[j++];
}
while(i <= mid)
T[k++] = R[i++]; // 将剩余的 R[low..mid]复制到T中
while(j <= high)
T[k++] = R[j++]; // 将剩余的 R[j..high]复制到T中
}
归并排序算法实现
void MSort(RedType R[],RedType &T[],int low,int high){
// R [low..high]归并排序后放人 T[low..high]中
if(low == high)
T[low] = R[low];
else{
mid = (low + high) / 2; // 将当前序列一分为二, 求出分裂点mid
// 对子序列R[low..mid]递归归并排序,结果放入S[low..mid]
MSort(R,S,low,mid);
// 对子序列 R[mid+l..high]递归归并排序,结果放人S[mid+1..high]
MSort(R,S,mid+1,high);
// 将S[low..mid]和S[mid+l..high]归并到T[low..high]
Merge(S,T,low,mid,high);
}
}
void MergeSort(SqList &L){
// 对顺序表 L 做归并排序
MSort(L.r,L.r,1,L.length);
}
归并排序的时间复杂度为
用顺序表实现归并排序时,需要和待排序记录个数相等的辅助存储空间,所以空间复杂度为O(n)
特点:
- 稳定排序
- 可用于链式结构, 且不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈
六、基数排序
基数排序是根据关键字中各位的值,通过对待排序记录进行若干趟“ 分配 ”与“ 收集”来实现排序的,是一种借助于多关键字排序的思想对单关键字排序的方法
基数排序(Raddd Sorting)是典型的分配类排序
1、多关键字的排序
例子:整理扑克牌(每一张牌有两个 “ 关键字”:花色和面值,且“ 花色 ” 的地位高于“ 面值" , 在比较任意两张牌面的大小时, 必须先比较 “ 花色"' 若 “ 花色 ” 相同, 则再比较面值 )
- 最高位优先法
先按不同 “ 花色 ” 分成有次序的4堆,每一堆的牌均具有相同的 “ 花色 ” ,然后分别对每一堆按 “ 面值 ” 大小整理有序
- 最低位优先法
这是一种 “ 分配 ” 与 “ 收集” 交替进行的方法
先按不同“ 面值 ” 分成13堆,然后将这13堆牌自小至大叠在一起,然后将每堆按照面值的次序收集到一起。再重新对这些牌按不同“ 花色 ” 分成4堆, 最后将这 4堆牌按花色的次序再收集到一起,此时同样得到一副满足如上次序关系的牌
2、链式基数排序
假设记录的逻辑关键字由 d 个“关键字”组成,每个关键字可能取 rd 个值。只要从最低数位关键字起,按关键字的不同值将序列中记录“分配”到 rd 个队列中后再“收集”之,如此重复 d 次完成排序。按这种方法实现排序称之为基数排序,其中“基”指的是 rd 的取值范围
相关数据类型的定义如下:
#define MAXNUM_KEY 8 // 关键字项数的最大值
#define RADIX 10 // 关键字基数,此时是十进制整数的基数
#define MAX_SPACE 100
typedef struct{
KeysType keys[MAXNUM_KEY]; // 关键字
InfoType otheritems; // 其他数据项
int next;
}SLCell; // 静态链表的结点类型
typedef struct{
SLCell r[MAX_SPACE]; // 静态链表的可利用空间, r[O]为头结点
int keynum; // 记录的当前关键字个数
int recnum; // 静态链表的当前长度
} SLList; // 静态链表类型
typedef int ArrType[RADIX]; // 指针数组类型
基数排序算法实现
void Distribute(SLCell &r,int i,ArrType &f,ArrType &e){
// 静态链表 L 的 r 域中记录已按 (keys[O],…,keys [i-1]) 有序
// 本算法按第i个关键字keys[i]建立RADIX个子表,使同一子表中记录的keys[i]相同
// f[0..RADIX-1和 e[0..RADIX-1]分别指向各子表中第一个和最后一个记录
for(j=0; j<RADIX; ++j)
f[j] = 0; // 各子表初始化为空表
for(p=r[0].next; p; p=r[p].next){
j = ord(r[p].keys[i]); // ord将记录中第i个关键字映射到[0..RADIX-1]
if(!f[j])
f[j] = p;
else
r[e[j]].next = p;
e[j] = p; // 将p所指的结点插人第j个子表中
}
}
void Collext(SLCell &r,int i,ArrType f,ArrType e){
// 本算法按keys[i]自小至大地将f[0..RADIX-1]所指各子表依次链接成一个链表
// e[0..RADIX-1]为各子表的尾指针
for(j=0; !f[j]; j=succ(j)); // 找第一个非空子表,succ为求后继函数
r[0].next = f[j]; // r[0].next指向第一个非空子表中第一个结点
t = e[j];
while(j<RADIX){
for(j=succ(j); j<RADIX-1 && !f[j]; j=succ(j)); // 找下一个非空子表
if(f[j]){
r[t].next = f[j];
t = e[j]; // 链接两个非空子表
}
}
r[t].next = 0; // t指向最后一个非空子表中的最后一个结点
}
void RadixSort(SLList &L){
// L是采用静态链表表示的顺序表
// 对L做基数排序,使得L成为按关键字自小到大的有序静态链表,L.r[0]为头结点
for(i=0; i<L.recnum; ++i)
L.r[i].next = i+1;
L.r[L.recnum].next = 0; // 将L改造为静态链表
for(i=0; i<L.keynum; ++i){ // 按最低位优先依次对各关键字进行分配和收集
Distribute(L.r,i,f,e); // 第i趟分配
Collect(L.r,i,f,e); // 第i趟收集
}
}
链式基数排序的时间复杂度为 O(d(n+ rd))
链式基数排序的空间复杂度为 O(n + rd)
特点:
- 稳定排序
- 可用于链式结构,也可用于顺序结构
- 时间复杂度可以突破基于关键字比较一类方法的下界
,达到 O(n)
- 基数排序使用条件有严格的要求:需要知道各级关键字的主次关系和各级关键字的取值范围
七、外部排序
内部排序的整个排序过程全部是在内存中完成的,并不涉及数据的内外存交换问题。但如果待排序的记录数目很大,无法一次性调入内存,整个排序过程就必须借用外存分批调入内存才能完成
1、外部排序的基本方法
外部排序基本上由两个相对独立的阶段组成
首先,按可用内存大小,将外存上含 n 个记录 的文件分成若干长度为 l 的子文件或段(segment), 依次读入内存并利用有效的内部排序方法对它们进行排序,并将排序后得到的有序子文件重新写入外存,通常称这些有序子文件为归并段或顺串(run); 然后,对这些归并段进行逐趟归并,使归并段(有序的子文件)逐渐由小至大,直至得到整个有序文件为止
2-路平衡归并
外部排序所需总时间 = 内部排序所需的时间 + 外部信息读写的时间 + 内部归并所需的时间
对同一文件而言,进行外排时所需读/写外存的次数和归并的趟数 s 成正比
在一般情况下,对 m 个初始归并段进行 k-路平衡归并时,归并的趟数
为了减少归并趟数s, 可以从以下两个方面进行改进:
- 增加归并段的个数 k
- 减少初始归并段的个数 m
2、置换-选择排序
归并的趟数 s 不仅和 K 成反比,也和 m 成反比,因此,减少 m 是减少 s 的另一条途径
m 是外部文件经过内部排序之后得到的初始归并段的个数
n 为外部文件中的记录数,l 为初始归并段中的记录数
置换选择排序 (Replacement-Selection Sorting) 是在树形选择排序的基础上得来的,它的特点是:在整个排序(得到所有初始归并段)的过程中,选择最小(或最大)关键字和输入、输出交叉或平行进行
置换-选择排序所得初始归并段的长度不等
3、最佳归并树
若对长度不等的 m 个初始归并段,构造一棵哈夫曼树作为归并树,便可使在进行外部归并时所需对外存进行读\写次数达最少,这棵归并树便称做最佳归并树
八、小结
从时间复杂度的平均情况来看,直接插入排序、折半插入排序、冒泡排序和简单选择排序的速度较慢,而其他排序方法的速度较快。从算法实现的角度来看,速度较慢的算法实现过程比较简单,称之为简单的排序方法;而速度较快的算法可以看作是对某一排序算法的改进,称之为先进的排序方法, 但这些算法实现过程比较复杂。总的来看,各种排序算法各有优缺点,没有哪一种是绝对最优的
在使用时需根据不同情况适当选用,甚至可将多种方法结合起来使用。一般综合考虑以下因素:
- 待排序的记录个数
- 记录本身的大小
- 关键字的结构及初始状态
- 对排序稳定性的要求
- 存储结构
结论:
- 当待排序的记录个数n较小时,
和
的差别不大,可选用简单的排序方法
- 而当关键字基本有序时,可选用直接插入排序或冒泡排序,排序速度很快,其中直接插入排序最为简单常用、性能最佳
- 当 n 较大时,应该选用先进的排序方法
- 对于先进的排序方法,从平均时间性能而言,快速排序最佳,是目前基于比较的排序方法中最好的方法。但在最坏情况下,即当关键字基本有序时,快速排序的递归深度为 n, 时间复杂度为
,空间复杂度为 O(n)。堆排序和归并排序不会出现快速排序的最坏情况,但归并排序的辅助空间较大
- 当 n 较大时, 具体选用的原则是:
- 当关键字分布随机,稳定性不做要求时,可采用快速排序
- 当关键字基本有序,稳定性不做要求时,可采用堆排序
- 当关键字基本有序,内存允许且要求排序稳定时,可采用归并排序
- 对于先进的排序方法,从平均时间性能而言,快速排序最佳,是目前基于比较的排序方法中最好的方法。但在最坏情况下,即当关键字基本有序时,快速排序的递归深度为 n, 时间复杂度为
- 可以将简单的排序方法和先进的排序方法结合使用
- 当 n 较大时,可以先将待排序序列划分成若干子序列,分别进行直接插入排序,然后再利用归并排序,将有序子序列合并成一个完整的有序序列
- 在快速排序中,当划分子区间的长度小于某值时,可以转而调用直接插入排序算法
- 基数排序的时间复杂度也可写成 O(d·n),它最适用于 n 值很大而关键字较小的序列
- 若关键字也很大, 而序列中大多数记录的“最高位关键字“均不同,则亦可先按“最高位关键字“不同将序列分成若干“小”的子序列, 而后进行直接插入排序
- 当关键字的取值范围为无穷集合时,则无法使用基数排序
- 从方法的稳定性来比较,基数排序是稳定的内排方法, 所有时间复杂度为
的简单排序法也是稳定的, 然而,快速排序、堆排序和希尔排序等时间性能较好的排序方法都是不稳定的
- 若记录本身信息量较大,为避免移动记录耗费大量时间,可采用链式存储结构。比如直接插入排序、归并排序都易于在链表上实现。但像折半插入排序、希尔排序、快速排序和堆排序,却难于在链表上实现
一 叶 知 秋,奥 妙 玄 心