辅助函数介绍
首先我们为每一种排序类创建less()和exchange()这两个辅助函数用于对象的比较和位置的交换。为了让我们的排序类支持不同类型的对象,我们使用了Comparble接口。
private static boolean less(Comparable v, Comparable w) {
return (v.compareTo(w) < 0);
}
private static void exchange(Comparable[] a, int i, int j) {
Comparable swap = a[i];
a[i] = a[j];
a[j] = swap;
}
额外内存的使用
排序算法的额外内存开销和运行时间是同等重要的。排序算法可以分为两类:除了函数调用所需的栈和固定数目的实例变量之外无需额外内存的原地排序方法,以及需要额外内存空间来存储另一份数组副本的其他排序方法。
选择排序
算法流程
首先,找到数组中最小的那个元素,其次,将它和数组中的第一个元素进行交换。然后,在剩下的元素中寻找最小的元素,与数组中的第二个元素进行交换,如此反复。
算法复杂度
对于长度为N的数组,选择排序需要大约次比较和次交换。
特点
- 运行时间和输入无关。
- 数据移动是最少的。选择排序用了N次交换——交换次数和数组的大小是线性关系。
实现代码
public class Selection {
public static void sort(Comparable[] a){
for(int i = 0; i < a.length; i++){
int k = i;
for(int j = i + 1; j < a.length; j++){
if(less(a[j], a[k])) k = j;
}
exchange(a, k, i);
}
}
private static boolean less(Comparable a, Comparable b)//...见上文
private static void exchange(Comparable[] a, int i, int j) //...见上文
}
插入排序
通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。这就是插入排序。
算法复杂度
- 对于随机排列的长度为N且主键不重复的数组,平均情况下插入排序需要次比较以及次交换。最坏情况下需要次比较和次交换,最好情况下需要N-1次比较和0次交换。
- 插入排序需要的交换操作和数组中倒置的数量相同,需要的比较次数大于等于倒置的数量,小于等于倒置的数量加上数组的大小再减一。
实现代码
public class Insertion {
public static void sort(Comparable[] a){
for(int i = 0; i < a.length; i++){
for(int j = i; j > 0 && less(a[j], a[j - 1]); j--){
exchange(a, j, j - 1);
}
}
}
private static boolean less(Comparable a, Comparable b)//...见上文
private static void exchange(Comparable[] a, int i, int j) //...见上文
}
改进
- 插入排序的哨兵:在插入排序的实现中先找出最小的元素并将其置于数组的最左边,这样就能去掉内循环的判断条件j>0。这是一种常见的规避边界测试的方法,能够省略判断条件的元素通常称为哨兵。
- 不需要交换的插入排序:在插入排序的实现中使较大元素右移一位只需要访问一次数组(而不使用exchange())
实现代码:
public class InsertionX {
public static void sort(Comparable[] a){
int N = a.length;
int exchanges = 0;
for(int i = N - 1; i > 0; i--){
if(less(a[i], a[i - 1])){
exchange(a, i, i - 1);
exchanges++;
}
}
if(exchanges == 0) return;
for(int i = 2; i < N; i++){
Comparable v = a[i];
int j = i;
while (less(v, a[j - 1])){
a[j] = a[j - 1];
j--;
}
a[j] = v;
}
}
//......
}
二分插入排序
使用二分查找来快速确定插入的下标,减少比较和交换的次数。
public class BinaryInsertion {
public static void sort(Comparable[] a){
for(int i = 1; i < a.length; i++){
int lo = 0, hi = i - 1, mid;
while (lo <= hi){
mid = (lo + hi) / 2;
if(less(a[i], a[mid])){
hi = mid;
}else{
lo = mid + 1;
}
}
Comparable v = a[i];
for(int j = i; j > lo; j--){
a[j] = a[j - 1];
}
a[lo] = v;
}
}
private static boolean less(Comparable a, Comparable b)//...见上文
private static void exchange(Comparable[] a, int i, int j) //...见上文
}
希尔排序
希尔排序的思想是使数组中任意间隔为h的元素都是有序的。这样的数组被称为h有序数组。在进行排序时,如果h很大,我们就能将元素移动到很远的地方,为实现更小的h有序创造方便。用这种方式,对于任意以1结尾的h序列,我们都能够将数组排序,这就是希尔排序。
算法复杂度
希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。子数组部分有序的程度取决于递增序列的选择。彻底理解希尔排序的性能至今仍然是一项挑战。
实现代码
public class Shell {
public static void sort(Comparable[] a){
int N = a.length;
int h = 1;
while (h < N / 3) h = h * 3 + 1;
while (h >= 1){
for(int i = h; i < N; i++){
for(int j = i; j >= h && less(a[j], a[j - h]); j-= h){
exchange(a, j, j - h);
}
}
h /= 3;
}
}
private static boolean less(Comparable a, Comparable b)//...见上文
private static void exchange(Comparable[] a, int i, int j) //...见上文
}