一、排序的基本概念
使序列成为一个按关键字有序的序列的操作称为排序。
1、排序的稳定性
排序不仅是针对主关键字,对于次关键字,由于待排序的记录序列中可能存在两个或两个以上的关键字相等的记录,排序结果可能会存在不唯一的情况。
例如:设:Ri.key==Rj.key
假设排序前Ri的位置排列在Rj之前;
经排序后若Ri的位置仍然排列在Rj之前,则是稳定排序算法;
若不能保证这一点则是非稳定排序算法。
2、内排序与外排序
内排序是在排序的整个过程中,待排序的所有记录全部被放在内存中。
外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
对于内排序,排序算法的性能主要受3个方面影响:
1、时间性能
排序是数据处理中经常执行的操作,属于系统的核心部分,排序算法的时间开销是衡量其好坏的重要的标志。内排序中主要进行比较和移动两种操作。高效率的内排序算法应是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。
2、辅助空间
辅助存储空间是除了存放排序所占用的存储空间之外,执行算法所需要的其他存储空间。
3、算法的复杂度
即算法本身的复杂度。根据排序过程中借助的主要操作,将内排序分为:插入排序、交换排序、选择排序和归并排序。
排序用到的结构与函数:
#define MAXSIZE 100
typedef struct{
int data[MAXSIZE];//用于存储要排序数组,r[0]用作哨兵或临时变量
int length;//记录书序表长度
}SqList;
/*数组中两元素交换*/
int swap(SqList L, int i, int j){
int temp = L.data[i];
L.data[i] = L.data[j];
L.data[j] = temp;
return OK;
}
二、冒泡排序
冒泡排序(Bubble Sort)是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。
//冒泡排序
int BubbleSort(SqList &L){
int i,j;
int flag = TRUE;//作标记,用于优化冒泡排序
for(i=1; i<L.length && flag; i++){
flag = FALSE;
for(j=L.length-1; j>=i; j--){
if(L.data[j] > L.data[j+1]){
swap(L,j,j+1);
flag = TRUE;
}
}
}
return OK;
}
冒泡排序复杂度
最好的情况是当排序的表本身是有序的,由以上代码可知,将进行n-1次比较,没有数据交换,时间复杂度为O(n);
最坏的情况是当排序表示逆序的,此时需要比较 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)次,并做等数量级的记录移动。
因此,总的时间复杂度为O(n2)。
三、简单选择排序
简单选择排序法(Simple Selection Sort)就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1 ≤ \leq ≤i ≤ \leq ≤n)个记录交换
//对顺序表L作简单选择排序
int SelectSort(SqList &L){
int i,j,min;
for(i=1; i<L.length; i++){
min = i;
for(j=i+1; j<L.length; j++){
if(L.data[min] > L.data[j])
min = j;
}
if(i != min)
swap(L,i,min);
}
return OK;
}
简单选择排序复杂度
简单选择排序最大的特点就是交换移动数据次数少,这样也就节约了相应的时间。
无论最好最坏情况,其比较次数都是一样多,第i趟排序需要n-i次关键字的比较,此时需要比较 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)次。对于交换次数而言,最好的时候交换0次,最坏的时候交换n-1次,最终的排序时间是比较与交换的次数总和。
总的时间复杂度为O(n2)。与冒泡排序相同,但简单选择排序性能上略优于冒泡排序。
四、直接插入排序
直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。
//对顺序表L作直接插入排序
int InsertSort(SqList &L){
int i,j;
for(i=2; i<=L.length; i++){
if(L.data[i] < L.data[i-1]){
L.data[0] = L.data[i];//设置哨兵
for(j=i-1; L.data[j] > L.data[0]; j--)
L.data[j+1] = L.data[j];
L.data[j+1] = L.data[0];
}
}
return OK;
}
直接插入排序复杂度
从空间上看,它只需要一个记录的辅助空间,因此主要看它的时间复杂度
最好的情况即排序本身有序,共比较n-1次,没有移动记录,时间复杂度为O(n)。最坏的情况即逆序,需要比较 ( n + 2 ) ( n − 1 ) 2 \frac{(n+2)(n-1)}{2} 2(n+2)(n−1)次,移动次数也达到最大值 ( n + 4 ) ( n − 1 ) 2 \frac{(n+4)(n-1)}{2} 2(n+4)(n−1)次。
如果排序记录是随机的,根据概率相同的原则,平均比较和移动次数约为 n 2 4 {n^2}\over{4} 4n2次,因此,直接插入排序法的时间复杂度为O(n2)。同样的O(n2)时间复杂度的,直接插入算法比冒泡和简单选择排序的性能要好一些。
五、希尔排序
希尔排序(Shell Sort),希尔排序是D.L.Shell于1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是O(n2),希尔排序算法是突破这个时间复杂度的第一批算法之一。
六、堆排序
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:
堆排序(Heap Sort)是对简单选择排序进行的一种改进,是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是对顶的根结点。将它移走(就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行就能得到一个有序序列了。
//堆调整
int HeapAdjust(SqList &L, int s, int m){
int temp,j;
temp = L.data[s];
for(j=2*s; j<=m; j*=2){//沿关键字较大的孩子结点向下筛选
if(j<m && L.data[j] < L.data[j+1])
++j;
if(temp >= L.data[j])
break;
L.data[s] = L.data[j];
s = j;
}
L.data[s] = temp;
return OK;
}
//对顺序表L进行堆排序
int HeapSort(SqList &L){
int i;
for(i=L.length/2; i>0; i--){//把L中的数组构建成一个大顶堆
HeapAdjust(L, i, L.length);
}
for(i=L.length; i>1; i--){
swap(L,1,i);//将堆顶记录和当前未经排序子序列的最后一个记录交换
HeapAdjust(L,1,i-1);//将L中的数组重新调整为大顶堆
}
return OK;
}
堆排序复杂度
它的运行时间主要是消耗在初始构建堆和在重建堆时的反复筛选上。
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较,若有必要则互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n).
在正式排序时,第i次取堆顶记录重建堆需要用O( log i \log i logi)的时间(完全二叉树的某个结点到根结点的距离为 ⌊ log 2 i ⌋ \lfloor{\log_2i} \rfloor ⌊log2i⌋+1),并需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(n log n \log n logn)。
总体而言,堆排序的时间复杂度为O(n log n \log n logn)。由于堆排序堆原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(n log n \log n logn)。
空间复杂度上,它只有一个用来交换的暂存单元。但由于记录的比较与交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。