一般来说,排序算法有10种,今天先说4种,并给出基本的代码。如有错误,欢迎大家指正。如果大家比较忙,忙着去泡妞的话,可以直接跳到最后看我的小结部分。
为了便于后面的讨论和理解,这里先做一个约定,就是把你要排序的元素假想为大小不一的一些球,这些球都放在一个容器里。排序的过程是把这些球拿出来按照一个指定的顺序的摆放(这里指定的顺序我们先定为升序,倒序是一回事)。同时预先定义两个方法,假定我们要比较的元素是整型数字,其实要改成通用的类型,也是很容易的一件事。
public static boolean less(int a,int b){
return a<b;
}
public static void exch(int [] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
/**
这里给大家说一个不需要临时变量就交换两个数的技巧,异或就可以了。如下:
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];
arr[i]=arr[i]^arr[j];
**/
}
1. 选择排序
顾名思义,不断地把容器中最小的球选择出来,依次摆放在前面已经拿出来的球后面。直至容器中没有球为止。大家看一下代码:
/**
** arr 要排序的数组
** start 排序数组的开始位置
** end 排序数组的结束位置
**/
public static void sort(int [ ] arr,int start,int end){
int i,j,min;
for(i=start;i<end-1;i++){
min=i;
for(j=start+1;j<=end;j++){
if(less(arr[j],arr[min]) min=j;
}
exch(arr,i,min);
}
}
现在来分析一下分析一下这个算法的性能,排序要完成,第一次选出最小的数,需要N-1比较,第二次需要N-2,…,一直到1;那么总共的比较是(N-1)+(N-2)+(N-3)+…+1=N(N-1)/2,时间复杂度O(n^2);交换次数为N-1次,这是显而易见的。
2.插入排序
插入排序,我们从容器拿球出来时,并不要求拿最大的还是最小,依次拿出来就可以了。拿出来以后,放到已经拿出来的球中的适当位置,大家想想在排队时,要把个子矮的人调整到适当的位置,都是和他前面的比较,如果比他前面的人矮,就插入到前面去,直到他前面的人都比他个子矮,这样最后就可以形成一个有序的队伍。下面是代码:
public static void sort(int [ ] arr,int start,int end){
int i,j,temp;
/**
这里是找出元素中最小的一个,避免后面插入时要判断是否已经到达数组
第一个位置。
**/
for(i=end;i>start;i--) if(less(arr[i],arr[i-1])) exch(arr,i,i-1);
for(i=start+2;i<=end;i++){
temp=arr[i];
j=i;
/**
注意,这里在处理插入时,采用赋值操作的实现当然要比交换操作快。
**/
while(less(temp,arr[j-1]){
arr[j]=arr[j-1];
j--;
}
arr[j]=temp;
}
}
现在来分析一下插入排序,通过内部循环大家可以看到,比较的次数和移动的次数是一样的。在最坏情况下,插入第i个数时,需要比较i-1和移动i-1次,因此最坏情况下,需要N(N-1)/2次比较和移动,平均情况是N(N-1)/4次比较和移动。
3.冒泡程序
冒泡,大家想想,水里气泡是不是从底向上组件冒到上面的。我们对容器里的小球采用冒泡排序的话,我们也是不断地把最小的球浮到上面来。最后形成一个有序的小球队列。还是先看代码
public static void sort(int [ ] arr,int start,int end){
int i,j;
//最多只需要N-1趟就可以排好序了,因为一次把一个小的球浮上来。
for(i=start;i<end;i++){
for(j=end;j>i;j--){
if(less(arr[j],arr[j-1])) exch(arr,j,j-1);
}
}
}
其实上面的程序还可以进一步改进,如果第i趟时,没有发生交换,就说明已经排好序了。和前面的分析一样,第i趟冒泡排序需要N-i次比较和交换。总的来说,平均和最坏情况下都需要N^2/2次比较和交换操作(理论证明有点复杂,但是牛人已经证明过了)。
4.希尔排序
希尔排序本质上就是插入排序,大家有没有注意到,如果在一个数组里,最小的一个元素在最后一个位置,插入排序时,我们是不是需要移动N-1次才能放到具体的位置?于是,shell这个人就想,我们是不是可以以一定的步长把数组里的元素分成几个序列?分别对这些子序列使用希尔排序,最后步长减小为1时再使用插入排序搞定。Ok,这就是希尔排序了。当然了,希尔排序的难点就是步长的选择,到底多少合适呢?说实话,算法大牛们到现在为止都没有证明出哪一个步长序列是最好的。我这里就是用广泛采用的步长序列吧,1,4,13,40,121…(其实就是3x+1形成的序列),这个步长下,时间复杂度是O(N^(3/2));代码如下:
public static void sort(int [ ] arr,int start,int end){
int i,j,temp,h;
//计算步长
for(h=1;h<=(end-start)/9;h=3*h+1);
//开始以不同步长进行插入排序
for(;h>0;h/=3){
//注意,这里后面i++,而不是i+=h
for(i=start+h;i<=end;i++){
j=i;temp=arr[i];
while(j>=start+h&&less(temp,arr[j-h]){
arr[j]=arr[j-h];
j-=h;
}
arr[j]=temp;
}
}
}
小结:选择排序,插入排序和冒泡排序都是O(n^2)的,希尔排序低于O(n^2)。实际应用过程中,对于数据量较小时(一般来说,小于1w),一般可以使用选择排序,插入排序和冒泡排序,中等数据量(小于100w)可以采用希尔排序,可以取得较好的效果。即使对于小数据量,还可以进一步分析,选择排序是不管输入数据的形态怎样(也就是说,不管输入的数据是随机的,还是几乎有序的,还是倒序的),时间复杂度都是O(N^2),然而如果数据几乎是有序的时候,采用插入排序和冒泡排序都可以在线性时间内完成排序。那么选择排序有没有作为首选的时候呢?答案是肯定的,如果我们要排序的文件比较大时,移动文件耗费的时间占据了很多时间,这个时候,比较操作是微不足道的,根据前面我们的分析,选择排序需要的移动(即交换)次数是最少的,这时选择排序当然是我们的首选了。
接下来,我准备说一下快速排序,归并排序和堆排序。