第二章 排序
2.1初级排序算法
2.1.1 辅助函数
public static voidexch(Comparable[] a, inti, intj){
//用于交换a[i]和a[j]
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
public static boolean less(Comparable v, Comparable w){
//如果v<w返回true,v>w返回false
return v.compareTo(w) <0;
}
2.1.2 选择排序
算法描述:不断在剩余元素中选择最小的
1. 找到数组中最小的那个元素,将它和数组的第一个数交换(如果第一个元素就是最小的元素,就和自己交换)
2. 在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置
3. 如此往复,直到将整个数组排序
核心代码:
public classSelection {
public static void sort(Comparable[] a){
//将数组a升序排序
int N = a.length;
for(inti = 0; i < N; i++){
int min =i;
for(intj = i; j < N; j++){
if(less(a[j],a[min])){
min = j;
}
exch(a, i, min);
}
}
}
}
时间复杂度:
对于长度为N的数组,需要大约N(N-1)/2次比较和N次交换,时间复杂度是N^2
2.1.3 插入排序
算法描述:当前元素的左边元素都是有序的,但最终位置还不确定;将当前元素它前面的元素比较,插入到合适的位置。
核心代码:
public classInsertion {
public static void sort(Comparable[] a){
//将数组a升序排序
int N = a.length;
for(inti = 0; i < N; i++){
//将a[j]与它左边的a[j-1]a[j-2]a[j-2]...比较,插入到合适的位置
for(intj = i; j > 0&&less(a[j],a[j-1]); j--){
exch(a, j, j-1);
}
}
}
}
时间复杂度:
最坏的情况需要N(N-1)/2次比较,N(N-1)/2次交换;最好情况下需要N-1次比较,0次交换;平均情况下需要N(N-1)/4次比较,N(N-1)/4次交换
插入排序适合的情况:
数组部分有序:逆序的数量 小于数组大小的某个倍数
典型的部分有序数组:
1. 数组中每个元素距离它的最终位置都不远;
2. 一个有序的大数组接一个小数组
3. 数组中只有几个元素的位置不正确
插入排序适合部分有序数组(当逆序数量很少),小规模数组时
关于插入排序的命题:
插入排序需要的交换操作 = 数组中逆序的数量
逆序的数量 <= 需要的比较次数 <= 逆序的数量 + 数组的大小 – 1
(每次交换对应这一次比较,在i没有到达最左端时,每个i需要一次额外的比较)
2.1.4 选择排序 vs 插入排序
算法比较:
选择排序只访问右侧元素;插入排序只访问左侧元素
哪个更快:
对于随机排序的无重复主键的数组,插入排序和选择排序的运行时间是平方级的,两者之比应该是一个较小的常数
这个小常量没有准确描述,是因为不同的情况下结果不同
实验验证
2.1.5 希尔排序
插入排序,在大规模乱序数组的情况下,效率很差,因为它只能交换相邻的元素
算法描述:
h有序数组:数组中任意间隔为h的元素都是有序的,如下图所示:
h从一个很大的数开始递减至1,比较和交换都以h为间隔(当h很大时可以将元素移动到很远的地方,未更小的h有序创造条件,当h=1时可以实现整个数组排序)
核心代码:
public classShell {
public static void sort(Comparable[] a) {
//将数组a升序排序
int N = a.length;
int h =1;
while (h< N /3) {
h = 3 * h +1;
}//1,4,13,40,121......
while (h>=1){
for (inti = h; i < N; i++) {
//将a[i]插入到a[j-h],a[j-2h],a[j-3h]...
for (intj = i; j >= h && less(a[j],a[j - h]); j -= h) {
exch(a, j, j -h);
}
h = h / 3;
}
}
}
}
时间复杂度:
希尔排序权衡了子数组的规模和有序性;
排序之初各个子数组都很短,排序之后子数组是部分有序的;这两种情况都是插入排序擅长的情况
算法比较:
实验证明,希尔排序比插入和选择排序快得多,数组越大,优势越大