数据结构——排序(一)
排序大致分为两类,内部排序和外排序。内部排序指要排序的数据量很小,可以将全部数据读入内存,然后在其中进行排序。外部排序指数据量很大,数据无法全部读入内存,必须在磁盘或者磁带上完成的排序。首先来研究内排序,有几种简单的排序算法如插入排序,冒泡排序,选择排序时间复杂度为O(N*N),另外有一种希尔排序(shellsort),编程非常简单,时间复杂度也为O(N*N),但是在实践中很有效。另外有些稍微复杂些的算法可以做到时间复杂度为O(lNogN)。任何通用的排序算法,时间复杂度的下界为O(NlogN)。下面介绍几个典型的排序算法。
需要注意的是,这里描述的排序算法都是可以互换的,算法都接受一个数组以及数组的大小N来进行排序。另外数组中的元素都是可以进行比较大小的,这种排序称为基于比较的排序。
插入排序
插入排序是一种简单的排序算法,它由N-1趟(pass)排序组成。利用P代表排序的趟数,则P从1开始到N-1。插入排序利用了这样的事实,在进行第P趟排序时,从0到P-1的位置都是排过序的。则第P趟排序的任务就是将位置P处的元素插入到从0到P处的序列中的合适位置。因为位置P前的序列都是排好序的,因此在找到一个小于x(x为开始时p处的元素)的元素后,就可以将x放置到找到的位置的后面,比x大的元素都会向后移动一个位置。这种实现方式与在二叉堆中所用到的技巧(上滤,下滤)相同。插入排序例程如下:
void InsertionSort(ElementType A[],int N)
{
int j,P;
ElementType Tmp;
for(p=1;p<N;p++)
{
Tmp=A[P];
for(j=p;j>0&&A[j-1]<Tmp;j--)
A[j]=A[j-1];
A[j]=Tmp;
}
}
插入排序的时间复杂度比较好分析,对于每一趟排序,P处的元素最多要与前面所有的元素(P个元素,数组小标从0开始)都进行一次比较,同时为了保证不超过数组下界,每次都需要与0比较,因此在进行第P趟排序时,执行的比较次数为P+1,总共需要进行N-1趟,则最坏情况下的时间复杂度为O(N*N)(即(N-1)*(1+2+…..+N-1))。最好情况下,就是数组是已排序的,于是每次比较都是在最开始处比较就停止了,每一趟比较次数为2次,总共为2(N-1)次比较,则最好情况下时间复杂度为O(N)。现在问题在于平均情况下的时间复杂度,实际是也为O(N*N)。下面分析几个简单排序算法(只进行相邻元素比较的排序算法)的时间复杂度。
一些简单排序算法的下界
首先引出逆序的概念,数组的一个逆序是指数组中具有性质i<j,但是A[i]>A[j]的序偶(A[i],A[j])。一个数组中逆序的数目正好是简单排序算法中执行交换的数目。在一个已排序的数组中,逆序的数目为0。对一些简单排序算法而言,算法所需要花费的时间就是交换逆序的时间I与其他的操作O(N)。因此简单排序算法的时间复杂度为O(I+N)。其中I即为一个排列中逆序的数目。可以证明,由N个互异数组成的数组的平均逆序数是N(N-1)/4。由此可知,通过交换相邻元素进行排序的任何算法平均需要O(N*N)时间。
这个结论对使用非显式地实施相邻元素交换的插入排序有效,同时对另外的简单排序算法如冒泡排序和选择排序均有效。事实上,它对一整类只进行相邻元素交换的排序算法都是有效的,包括没有发现的算法。这个下界告诉我们,要想使一个排序算法以亚二次元时间运行,必须要比较,同时要对距离较远的元素交换,这样才可以在一次交换中删除不止一个逆序。
希尔排序
希尔排序是第一批冲破二次时间屏障的算法,它通过比较相距一定间隔的元素来工作,各趟比较使用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止,希尔排序也叫做缩小增量排序。希尔排序使用一个序列h1,h2,….ht,叫做增量序列。只要h1=1,任何增量序列都是可行的。在使用增量hk的一趟排序之后,对于每一个i我们有A[i]<A[i+hk],所有相隔hk的元素都被排序,称为hk排序的。hk排序的一般做法是,对于hk,hk+1,…..,N-1中的每一个位置i,把其上的元素放到i,i-hk,i-2hk…..中间的正确位置上。增量排序的一种流行的选择是使用shell建议的序列,ht=N/2的下界,hk=h((k+1)/2的下界,但是这种序列不好。希尔排序的例程如下:
void Shellsort(ElementType A[],int N)
{
int i,j,Increment;
ElementType Tmp;
for(Increment=N/2;Incrment>0;Increment/=2)
{
//这个循环内实际上进行了一次插入排序
for(i=Increment;i<N;i++)
{
Tmp=A[i];
//这个循环相当于执行一趟插入排序
For(j=i;j>=Increment;j-=Increment)
If(Tmp<A[j-Increment])
A[j]=A[j-Increment];
Else
Break;
A[j]=Tmp;
}
}
}
堆排序
堆排序使用优先队列的实现中应用到的典型的数据结构二叉堆,因此称为堆排序,利用优先队列每次都删除最小元素的性质,可以首先利用N次插入操作建立一棵二叉堆,时间复杂度为O(N)。然后执行N次删除最小元操作,就可以得到一个从小到大的元素排列(最小堆的情况)。每次删除最小元操作的时间复杂度为O(logN),则N次删除操作的时间复杂度为O(NlogN)。则综合时间复杂度为O(NlogN)。利用堆排序需要额外的数组来保存删除的最小元素,解决方法是利用保存堆的数组来保存删除后获得的最小元素。将删除的最小元素放到保存堆的数组的最后,将原来在堆最后位置的元素尝试放到数组的最开始位置,如果无法将其放到数组头部,则使用下滤的思想将其放到合适的位置。堆排序的例程如下所示(这里使用数组中位置为0的地方存储数据,与优先队列中二叉堆的实现不同):
#define LeftChild(i) (2*(i)+1)
void PerDown(ElementType A[],int I,int N)
{
int Child;
ElementType Tmp;
For(Tmp=A[i];LeftChild(i)<N;i=Child)
{
Child=LeftChild(i);
If(Child!=N-1&&A[Child+1]>A[Child])
Child++;
If(Tmp<A[Child])
A[i]=A[Child];
Else
Break;
}
A[i]=Tmp;
}
void HeapSort(ElementType A[],int N)
{
int I;
for(i=N/2;i>=0;i++) PerDown(A,i,N);
for(i=N-1;i>0;i--)
{ Swap(&A[0],&A[i]);PercDown(A,0,i);}