写在前面的话
我们将讨论对数组和链表的排序。我们关心的性能指标是排序算法的运行时间和所消耗的额外内存空间。
定义:
稳定——如果排序后文件中具有相同关键字的元素的相对位置保持不变,称一个排序算法是稳定的。
逆序——文件中一对顺序不对的元素称为一个逆序。
1. 选择排序
是思路非常简单的一种排序算法,工作过程如下:首先,选出数组中最小的元素,将它与数组中第一个元素进行交换。然后找出次小的元素,将它与数组中第二个元素进行交换……直到整个数组排好序。
代码实现如下:
void selection(Item a[],int l,int r) {
int i,j;
for(i=l;i<r;i++) {
int min=i;
for (j=i+1;j<=r;j++)
if(a[j]<a[min]) min=j;
exch(a[i],a[min]);
}
}
选择排序的缺点是:它的运行时间对文件中已有序的部分依赖较少。
2. 插入排序
原理:从序列中第二个数a[1]开始,每一步都将一个待排数据a[i]按其大小插入到已排好序的数据中的适当位置,直到全部插入完毕。
代码实现如下:
如果没有设置哨兵,那么
void insertion(Item a[],int l,int r) {
for (int i=l+1;i<=r;i++) {
int j=i;
Item v=a[i];
while (a[j-1]>v && j>l) { //如果某元素a[j-1]大于该元素v,就把a[j-1]后移到a[j]的位置
a[j]=a[j-1];
j--;
}
a[j]=v; //最后将该元素v放到正确的位置
}
}
在这里,“j>l”通常是为真,是无用的,它只在待插入元素是最小的且要插入到数组最前端时才为假。为了省略该判断条件,将数组中最小元素提前放在第1个,并从l+2位开始插入:
void insertion(Item a[],int l,int r) {
for (int i=r;i>l;i--) //设置哨兵,把数组中最小的元素放在第1个
compexch(a[i-1],a[i]);
for (i=l+2;i<=r;i++) {
int j=i;
Item v=a[i];
while(v<a[j-1]) { //如果某元素a[j-1]大于该元素v,就把a[j-1]后移到a[j]的位置
a[j]=a[j-1];
j--;
}
a[j]=v;
}
}
注意,这里设置最小元素为哨兵的目的,是为了省略while循环中的判断条件“j>l”。
3. 冒泡排序
原理:遍历数组,如果近邻的两个元素大小顺序不对,就将两者交换,重复这样的操作直到整个文件排好序。假设从右到左进行,第一遍将最小的元素移到了最左边,第二遍将第二小的元素移到了左边第二位……
冒泡排序实际上是一种选择排序。
代码实现如下:
void bubble(Item a[],int l,int r) {
int i,j;
for (i=l;i<r;i++)
for(j=r;j>i;j--) {
if(a[j-1]>a[j]) {//如果前一个比当前的大,就交换它们
Item temp=a[j];
a[j]=a[j-1];
a[j-1]=temp;
}
}
}
基本排序算法的性能特性
选择、插入、冒泡是3种基本排序算法。它们有以下一些性能:
- 选择排序执行大约(N^2)/2次比较操作和N次交换操作,且对输入数据不敏感。
- 平均情况下,插入排序执行大约(N^2)/4次比较操作和(N^2)/4次交换操作,最坏情况下,则是两倍的数量。
- 平均情况和最坏情况下,冒泡排序差别不大,都执行大约(N^2)/2次比较操作和(N^2)/2次交换操作。
4. 希尔排序
希尔排序是插入排序的改进版本,实质就是分组插入排序,也叫作缩小步长排序。
原理:先将整个文件分割成若干个子文件(由相隔某个步长的元素组成),分别进行直接插入排序,然后依次缩减步长再进行排序,待整个序列中的元素基本有序(步长足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况)效率是很高的,因此希尔排序在时间效率上比插入排序有较大提高。
如何选用步长是个很难回答的问题,通常使用以几何级别减少的序列。
如果在插入排序中不使用哨兵,并且用h代替1,就得到了h-排序的文件。增加一个外部循环来改变步长就实现了希尔排序。代码实现如下:
void shell(Item a[],int l,int r)
{
int i,j,h;
for (h=1;h<=(r-l)/9;h=3*h+1);
//先通过for循环确定最大步长h
for(;h>0;h/=3)
//逐渐减小步长,每次循环执行直接插入排序
for(i=l+h;i<=r;i++)
{
int j=i;Item v=a[i];
while (j>= l+h && less(v,a[j-h]))
{
a[j]=a[j-h];
j -= h;
}
a[j]=v;
}
}
注意,这里的步长间的比例大概是1/3,方法是从1开始,通过乘3加1得到下一个步长。于是步长序列是1、4、13、40、121、364……