讲排序之前先来几个简单的函数:
交换函数:
public static void swap(Comparable[] a, int i, int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
小于函数:
public static Boolean less(Comparable v, Comparable w){
return v.compareTo(w) < 0;
}
一、选择排序
首先找到数组中的最小元素,将它和数组的第一个元素交换位置;再次,在剩下的元素中找到最小元素,将它和数组的第二个元素交换位置。如此往复,直到整个数组排序。 核心思想:不断的选择剩余元素之中的最小值。
public class SelectSort{
public static void sort(Comparable[] a){
//将a[]按升序排序
int N = a.length;
for(int i = 0; i < N - 1; i++){
int min = i; //最小元素的索引
for(int j = i + 1; j < N; j++){
if(less(a[j],a[min]) min = j;
}
swap(a, i, min);
}
}
}
二、冒泡排序
思想:越大的元素会经由交换慢慢“浮”到数列的顶端(升序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样。
public class Bubble{
public static void sort(Comparable[] a ){
int N = a.length;
for(int i = N - 1; i > 0; i--){
for(int j = 0; j < i && less(a[j+1], a[j]); j++){
swap(a, j+1, j);
}
}
}
}
三、插入排序
通常我们整理桥牌,是将每一张牌插入到其他已经有序的牌中的适当位置。
与选择排序一样,当前索引左边的所以元素都是有序的,但它们的最终位置还不确定,为了给最小元素腾出空间,它们可能会被移动。当索引到达数组的右端时,数组排序就完成了。
和选择排序的不同是,插入排序所需的时间取决于输入中元素的初始顺序。
public class Insertion{
public static void sort(Comparable[] a){
int N = a.length;
for(int i = 0; i < N; i++){
for(int j = i; j > 0 && less(a[j],a[j-1]); j--){
swap(a, j, j-1);
}
}
}
}
四、希尔排序
希尔排序为了加快速度简单改进了插入排序,交换不相邻的元素以对数组的局部排序,并最终用插入排序将局部有序的数组排序。
思想:使数组中任意间隔为 h 的元素都是有序的。相对于插入排序,只需要在插入排序的代码中移动元素的距离由 1 改为 h 即可。
public class Shell{
public static void sort(Comparable[] a){
int N = a.length;
int h = 1;
//递增序列
while(h < N/3) h = 3 * h + 1; //1,4,13,40,121,364,1093,...
while(h >= 1){
//将数组变为 h有序
for(int i = h; i < N; i++){
//将a[i]插入到a[i-h],a[i-2*h],a[i-3*h]... 之中
for(int j = i; j > h - 1 && less(a[j],a[j-h]); j=j-h){
swap(a, j, j-h);
}
}
h = h/3;
}
}
}
子数组部分有序的程度取决于递增序列的选择,而如何选择递增序列呢?回答这个问题不简单,但上图的算法性能,运行时间达不到平方级别。在最坏的情况下,其比较次数和 N3/2 成正比。
总结:对于中等大小的数组它的运行时间是可以接受的,它的代码量小,且不需要额外的内存空间。下面的几节有更高效的算法,对于很大的N,它们可能只会比希尔排序快俩倍(可能还达不到),而且更复杂。如果你需要解决一个排序问题而又没有系统排序函数可用,可以先用希尔排序,然后再考虑是否值得将它替换为更加复杂的排序算法。