这篇文章主要介绍常规的排序算法,包括选择排序、插入排序、冒泡排序、希尔排序等。
关于快速排序的详解可参见:快速排序算法Java详解
关于堆排序的详解可以参见:堆排序算法Java详解
关于归并排序的详解可参见:归并排序算法Java详解
关于基数排序的详解可参见:基数排序算法Java详解
0 辅助函数
在所有排序中可能都要用到的辅助函数在这里首先列出来。
//交换
private void exchange(int[] items, int a, int b) {
int t;
t = items[a];
items[a] = items[b];
items[b] = t;
}
//比较,然后交换。如果items[b]<items[a],则交换这两个值
private void compexch(int[] items, int a, int b) {
if(items[b] < items[a])
exchange(items, a, b);
}
1 选择排序
选择排序是最简单的排序算法。首先,选出数组中最小的元素,将它与数组中第一个元素进行交换;然后找出次小的元素,并将它与数组中第二个元素进行交换。按照这种方法一直进行下去,知道整个数组完成排序。
例如如下的数组,前4个已经在正确的位置上了,后4个暂时无序:
当i=l+4时,启动内循环,找到了最小的一个元素15。然后将15和22调换,得到如下的数组。此时前5个事有序且处于正确位置的,后三个是无序的。
重复上述步骤,直到全部循环完成,整个数组也就变成有序的了。选择排序算法的Java实现如下:
public void selectionSort(int[] items, int l, int r) {
for(int i=l; i<r; i++) {
int min = i;
for(int j=i+1; j<=r; j++) //找到从i+1到r中最小的一个值,该值的下标为min
if(items[j] < items[min])
min = j;
exchange(items, i, min); //交换
}
}
选择排序有一个缺点,它运行的时间对文件中已有序的部分依赖较少,即没有很好的利用已经有序的部分。所以其最坏和最好的时间复杂度都是O(n^2)。
2 插入排序
我们在打牌时候使用的排序方式通常就是插入排序,摸牌时每次只考虑一张牌,将牌插入已经排好了序的牌的适当位置,插入后手中的牌还是有序的。插入排序的基本操作就是将一个记录插入已经排好序的有序表中,从而得到一个新的、记录数增1 的有序表。
例如如下的数组,前4个已经有序,后4个暂时无序:
在对14进行插入排序后,数组的情况如下,此时前5个是有序的,后3个是无序的。
插入排序的Java代码实现如下:
public void insertionSort(int[] items, int l, int r) {
for(int i=r; i>l; i--) compexch(items, i-1, i);
//上面是:将最小的元素挪到最前面的位置,将它作为“观察哨关键字”,这样可以使后面的循环更快
for(int i=l+2; i<=r; i++) {
int j = i;
int v = items[i];
while(v < items[j-1]) { //元素一直后移
items[j] = items[j-1];
j--;
}
items[j] = v;
}
}
由上述代码可以看到,在数组基本有序或完全有序时可以保证时间复杂度为O(N)。而在最坏的情况下(数组逆序),时间复杂度为O(N^2)。其平均时间复杂度为O(N^2)。
3 冒泡排序
冒泡排序的思路非常简单:遍历文件,如果近邻的两个元素大小顺序不对,就将两者交换,重复这样的操作指导整个文件排好序。
对于l到r-1之间的值,在内部循环过程中,逐渐将较小的值“冒”到前面。在外循环遍历的过程中,前i个元素已经处在了正确的位置上了。
例如如下的数组,前4个已经在正确的位置上了,后4个暂时无序:
当i=l+4时,启动内循环,j=r。先15和16判断,15<16,不用调换位置;然后j--,20和15判断,20>15,调换位置:
再j--,15和22判断;22>15,调换位置:
当再j--时,j与i重合,内部循环结束,15已经“冒”到了前端,此时前5个元素的位置是正确的,后三个无序。
重复上述循环,即可完成冒泡排序。冒泡排序的Java实现代码如下。
public void bubbleSort(int[] items, int l, int r) {
for(int i=l; i<r; i++) {
for(int j=r; j>i; j--)
compexch(items, j-1, j);
}
}
虽然算法思路简单,但是冒泡排序的执行速度要比插入排序或选择排序要慢,所以在实际应用中并不是非常多。
4 希尔排序
希尔排序又称“缩小增量排序”,它也是属于插入排序类的方法,但是在效率上较插入排序有较大改进。对于直接插入排序,其时间复杂度为O(n^2),但是当待排序记录的顺序正确时,其时间复杂度可提高至O(n)。希尔排序的基本思想是:先将整个待排序记录序列分隔成若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
例如如下的数组,一开始时分成以5为间隔划分子序列{R0,R5},{R1,R6},...。然后对每个子序列进行插入排序,得到一趟排序结果。然后在对一趟排序结果以3为间隔划分子序列,并对每个子序列进行插入排序。最后以1为间隔划分子序列(以1为间隔即不划分)进行一次插入排序。
希尔排序的Java实现代码如下。
public void shellSort(int[] items) { //时间复杂度:O(n^1.3)
//依次将数组分隔成5个子序列、3个子序列和1个子序列(1个子序列其实就是不划分)
int dlka[] = {5, 3, 1};
for(int i=0; i<dlka.length; i++)
shellInsert(items, dlka[i]); //增量为dlka[i]的希尔插入排序
}
//对数组items进行一趟希尔插入排序。该排序方式与直接插入排序相比,不同之处在于:
//前后记录位置的增量不是1,而是dk
private void shellInsert(int[] items, int dk) {
int i, j, t;
for(i=dk; i< items.length; i++) {
if(items[i] < items[i - dk]) {
t = items[i]; //暂存
for(j=i-dk; j>=0; j-=dk) {
if(items[j] > t)
items[j + dk] = items[j]; //记录后移
else
break;
}
items[j + dk] = t; //插入
}
}
}
希尔排序的时间复杂度为O(n^1.3)(根据《数据结构(C语言版)》)。需要注意的是,增量dlka中最后一个值必须是1,且所有的增量必须都是质数.
5 各类排序的比较
下面以表格形式从平均时间、最坏情况、辅助存储和稳定性的角度,对各种内部排序方法进行比较。
上表中:
关于快速排序的详解可参见:快速排序算法Java详解
关于堆排序的详解可以参见:堆排序算法Java详解
关于归并排序的详解可参见:归并排序算法Java详解
关于基数排序的详解可参见: 基数排序算法Java详解
全文完。转载请注明出处。
参考文献
1. 严蔚敏,数据结构(C语言版),清华大学出版社.
2.Robert Sedgewick,算法:C语言实现,机械工业出版社.