《算法》(第四版)------------排序
1、初级排序
基础API
public static void sort(Comparable[] a); //排序方法
public static boolean less(Comparable v,Comparable w);//比较,v小于w
public static void exch(Comparabl[] a, int i, int j);//交换元素
选择排序
思路
将数组分为已排序(前)、(后)未排序两段。后段找到数组中最小元素,使之与后段第一个元素交换位置(交换后该位置也会成为前段中的一员)。然后后段按此重复。当全部元素都成为已排序段成员则循环结束,排序结束。
成本
N2。时间成本与输入情况无关,无论输入的数组是随机排序还是已排序或者全为相同元素,所需时间一样。
代码
public static void sort(Comparable a[]){
int N = a.length;
for(int i = 0;i < N;i++){
int min = i;
for(int j = i+1;j < N;j++){
if(less(j,min) min = j;
}
exch(a,i,min);
}
}
插入排序
思路
将数组分为已排序(前)、(后)未排序两段。后段选取第一个元素加入到前段,然后该元素寻找自己能插入的位置,进行比较发现比前一个元素小则向前移动,找到后则会进入下一个循环。当全部元素都成为已排序段成员则循环结束,排序结束。
成本
N2。若是输入为已排序或者全为相同元素,则经过线性时间(N次比较,0次交换)就能运算完毕。
代码
public static void sort(Comparable[] a){
//将每一个元素插入到合适的位置,位置不合适则前面的元素向后推
int N = a.length;
for(int i = 0;i < N;i++){
int now = i,num = a[i];
while(now>0&&less(a,now,now-1)){
now--;
a[now] = a[now-1];
}
a[now] = num;
}
}
优化
提高插入排序的方法很简单。从交换这个步骤入手,在对某元素选择需要插入的位置时,记录下该元素,然后再比较时将较大的元素右移即可,这样访问数组的次数能减半。
希尔排序
概念
基于插入排序的一种快速排序算法。思想是使数组中任意间隔为h的元素都有序。这样的数组称为h有序数组。下图为示意图。
思路
1、选取一个合适的步长h
2、外循环:逐渐减小h到1
3、内循环:没个h都进行从a[h]开始,往后遍历,每个元素都插入到a[i-h],a[i-2*h],a[i-3*h]中。
2、归并排序
概念
分治的典型算法。将数组分成两半,分别排序,然后再将两个数组归并起来。
归并:左右分组各维护一个索引,不断向右遍历,取两边索引的较小值线性排序。
自顶向下归并排序
递归排序,从大数组不断递归到左右数组的排序。从树底下开始往上归并。
private static Comparable aux[]; //归并需要使用的辅助数组
public static void sort(Comparable a[], int lo, int hi){
if(hi<=lo){
return;
}
int mid = (lo+hi)/2;
sort(a,lo,mid);
sort(a,mid+1,hi);
merge(a,lo,mid,hi);
}
public static void merge(Comparable a[], int lo, int mid, int hi){
//维护左右数组下标
int i = lo,j = mid+1;
//得到辅助数组
for(int k=0;k<=hi;k++)
aux[k]=a[k];
for(int k=0;k<=hi;k++)
//左右边界检测
if (i>mid) a[k]=aux[j++];
else if(j>hi) a[k]=aux[i++];
//选取
else if(less(aux[i],aux[j])) a[k]=aux[i++];
else a[k]=aux[j++];
}
成本
对于长度为N的任意数组。树有n层,对于0<=k<=n-1,第k层有2k个子数组,每个数组长度为2n-k,最多需要2n-k次比较。因此每层比较次数都为2k*2n-k=2n,n层则一共为n2n=NlgN。归并排序最多需要访问数组6NlgN次,每次归并最多6N次访问数组(2N次用来复制,2N次用来将排序好的元素移回去,比较时最多2N次)。时间成本:NlgN。
自底向上归并排序
与自顶向下不同,自底向上归并排序维护一个sz值(当前要归并分组的大小)。sz从1开始组件翻倍递增,数组中的子数组两两配对归并,最终归并成完全排序数组。
public static void sort(Comparable a[], int lo, int hi){
int N = a.length;
int sz = 1;
//外循环增大每次需要归并长度
for(sz = 1;sz<N;sz*=2)
//内循环将数组中各个子数组两两配对
for(int i=0;i<N-sz;i+=2*sz)
//最后一组若落单则渠道下一轮等待归并,最后一组可能长度小于sz,此时会出现N-1<i+2*sz-1
merge(a,i,i+sz-1,Math.min(i+2*sz-1,N-1));
}
概念
快排也是一种分治算法。数组被切分成两个子数组,切分位置(k)左边的子数组均<=k,右边的子数组均>=k。当左右子数组均有序,此时整个数组有序。
快排思路
切分:数组的两端维护各维护一个索引,从两头往中间靠拢,当左边遇到>v,右边遇到<v,则可以退出循环进行一次交换两个索引对应的元素。
private static void sort(Comparable a[], int lo, int hi){
if(hi<=lo) return;
int j = partition(a,lo,hi);
sort(a,lo,j-1);
sort(a,j+1,hi);
}
private static int partition(Comparable a[], int lo, int hi){
Comparable v = a[lo];
int i=lo,j=hi+1;
//i,j向中间靠拢,要注意边界检测
while(less(a[++i],v)) if(i==hi) break;
while(less(v,a[--j])) if(j==lo) break;
//数组遍历完
if(i>=j) break;
exch(a,i,j);
//将切分位置元素换到适合的位置(j必定走到左子数组的最后一个元素:a[j]<v且j<=i)
exch(a,lo,j);
return j;
}
算法改进
切换到插入排序
对于小数组插入排序比较快。
//将sort()中
if(hi<=lo) return;
//替换为
if(hi<= lo+M){
//插入排序
Insertion.sort(a,lo,hi);
return;
}
三取样切分
取中位数来做切分对象,但是需要额外的计算中位数的成本。
三项切分
分成<v,=v,>v三部分。
/**
维护指针,lt,gt,i
[lo..lt-1]:<v
[lt..i-1]:=v
[i..gt]:?未知部分
[gt+1..hi]:>v
**/
/**
i从lo+1开始向前遍历:
a[i]<v:交换(lt,i),lt++,i++
a[i]>v:交换(i,gt),gt--
a[i]=v:i++
**/
private static void sort(Comparable a[], int lo, int hi){
if(lo<=hi) return;
Comparable v = a[lo];
int lt=lo,gt=hi,i=lo+1;
//i向前直到遇上gt(仍然有未知的部分,级没有排序好的元素)
while(i<=gt){
int cmp = a[i].compareTo(v);
if(cmp<0) exch(a,lt,i);i++,lt++;
else if(cmp>0) exch(a,gt,i);gt--;
else i++;
}
sort(a,lo,lt-1);//<v部分排序
sort(a,gt+1,hi);//>v部分排序
}
3、堆排序
概念
堆有序:二叉树每个结点都>=它的两个子结点。
二叉堆:一组能用堆有序的完全二叉树排序的元素,并在数组中按照层级储存(不使用第一个位置0)
上浮(swim):由下至上的堆有序化。将v上浮即,使v与父结点p比较,若是v>p则v与p交换位置,v层级上升。不断循环比较上升直至v<=p或者上升至根结点。
private void swim(int k){
// k/2为父结点
while(k>1&&less(k/2,k)){
exch(k/2,k);
k/=2;
}
}
下沉(sink):由上至下的堆有序化。将v下沉即,使v与子结点c比较,若是v<c则v与c交换位置,v的层级下降。不断循环比较下降直至v>=c或者下降至叶子结点。
private void sink(int k){
while(k<=N/2){
int j = 2*k;
//max(子结点1,子结点2)
if(j<N&&less(j,j+1)) j+=1;
if(!less(k,j)) break;
exch(j,k);
k=j;
}
}
思路
public static void sort(Comparable[] a){
int N = a.length();
//构造二叉堆:从数组n-1层级开始下沉(最后一层无需下沉,则此操作的对象数量为N/2)
for(int i = N/2;i>=1;i--) sink(a,i,N);
//将根结点(最大元素)与最后一个元素交换,然后取出最大元素,并且sink(1):重新使堆有序化
while(N>1){
exch(a,1,N--);
sink(a,1,N);
}
}