归并即将两个有序的数组归并成一个更大的有序数组。
优点能够保证将任意长度为N的数组排序所需时间和NlogN成正比。
缺点所需的额外空间和N成正比。
原地归并的抽象方法
实现归并的一种直接了当的方法是将两个不同的有序数组归并到第三个数组中。实现方法就是创建一个适当大小的数组,将两个数组一个个从小到大放入大数组中。
但是!!!当用归并将一个大数组排序时,需要进行多次归并,每次归并都创建一个新数组就太浪费内存了。所以需要原地归并,这样就可以先将前半部分排序,再将后半部分排序。
下面的代码还是用到了辅助数组,先放进去,在把归并结果放回来。
void merge(int a[], int lo, int mid, int hi){
int i = lo, j = mid+1;
for (int k = lo; k <= hi; k++)
aux[k] = a[k];//将a复制到aux
for(int k = lo; k <= hi; k++){ //归并回到a
if(i > mid) a[k] = aux[j++];//如果左边取完,取右边
else if(j > hi) a[k] = aux[i++];//如果右边取完,取左边
else if(aux[j] < aux[i]) a[k] = aux[j++];//如果右边当前元素小于左边,取右边
else a[k] = aux[i++];
}
}
自顶向下的归并排序
class Merge{
privata:
vector<int> aux;//归并所需的辅助数组
void sort(int[] a, int lo, int hi){
if(hi <= lo) return;
int mid = lo + (hi - lo)/2;
sort(a,lo,mid);//将左半边排序
sort(a,mid+1,hi);//递归调用直到两两有序然后merge
merge(a,lo,mid,hi);
}
void merge(int a[], int lo, int mid, int hi){
int i = lo, j = mid+1;
for (int k = lo; k <= hi; k++)
aux[k] = a[k];//将a复制到aux
for(int k = lo; k <= hi; k++){ //归并回到a
if(i > mid) a[k] = aux[j++];//如果左边取完,取右边
else if(j > hi) a[k] = aux[i++];//如果右边取完,取左边
else if(aux[j] < aux[i]) a[k] = aux[j++];//如果右边当前元素小于左边,取右边
else a[k] = aux[i++];
}
}
public:
void merge_sort(int[] a){
//java版是动态分配辅助数组的内存大小,我用vector所以不用分配了
sort(a, 0, a.size() - 1);
}
}
归并排序是分治思想的典型性用。
对于长度为N的任意数组,自顶向下的归并排序需要1/2NlogN至NlgN次比较,最多需要访问数组6NlogN次
自底向下的归并排序
递归实现的归并排序是算法中分治思想的典型应用。我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。所以我们先归并那些微型数组,然后再成对归并得到的子数组,最后将整个数组归并。
这种方法比标准低柜代码量少。先22归并,再44归并…
在每一轮的归并中,最后一次归并的第二个子数组可能比第一个子数组药效,但是这对merge没问题,merge根本不要求两边一样大。
class MergeBU{
private:
vector<int> aux;
void sort(int a[]){
//进行lgN次两两归并
for(int sz = 1; sz < a.size(); sz = sz+sz)//sz子数组大小
for(int lo = 0; lo < N-sz; lo += sz+sz)//lo:子数组索引
merge(a,lo,lo+sz-1,Math.min(lo+sz+sz-1,N-1));
}
}
自底向上的归并排序会多次遍历整个数组,根据子数组大小进行两两归并。子数组的大小sz的初始值为1,每次加倍。最后一个子数组大小只有在数组大小事偶数倍的时候才会等于sz(否则它会比sz小)
自底向上的归并排序比较适合用链表组织的数据因为这种方法只需要重新组织链表链接就能将链表原地排序。
最后
归并排序和希尔排序的时间差距在常数级别之内