排序算法之归并排序
归并算法的基础是,将两个有序的数组归并成一个更大的有序数组。
自然而然想到的就是,创建一个新的数组,将两个不同的有序数组归并到第三个数组中,然后结果返回一个数组。
Java的确可以以数组作为返回值,但是每次归并都创建一个新数组,会带来新的时间和空间上的花销。
比如每次归并都初始化一个新的数组,这是一个时间的花销。至于空间的花销,每次归并创建的数组长度加起来,肯定会比N大。
所以可以直接在排序的开始阶段,直接创建一个长度为N的数组,将排序的部分元素放到该辅助数组中,然后将排序结果放回原数组。
一:一次归并
就是比较两个数组当前元素的大小,小的进入数组。
如果一个数组已经全部进入,那么另一个数组可以直接全部依次进入,因为是有序的。
public static void merge(int []a,int lo,int mid,int hi){ int i = lo; int j = mid+1; //将lo----mid和mid+1----hi进行归并 //复制数组到aux里面,方便原地归并 for(int k=lo;k<=hi;k++) aux[k]=a[k]; //k是元素的总数,k个元素要归并好的 for(int k=lo;k<=hi;k++){ //左端数组已经比较完了,全部归并了,但是总元素还没归并完,右端数组还剩,将右端数组直接全部放进 if(i>mid) a[k]=aux[j++]; //右端比较完了 else if(j>hi) a[k]=aux[i++]; //左端的元素比右端的小 else if(aux[i]<aux[j]) a[k]=aux[i++]; //右端的元素比作端的小 else a[k]=aux[j++]; } }//end merge
二:自顶向下的归并排序(递归实现)
自顶向下,意味着从整个大数组开始,不断对半分,将左半部分和又半部分的数组归并。
不过当前的两个数组都是无序的,怎么办的?要进行递归,一直递归到单个元素的时候,该数组可以看成有序的,即可返回了。
然后将11归并变成一个2元素有序的数组,然后22归并变成4元素有序的数组................直到n/2的两个数组归并,排序完成。
public static int aux[]; public static void sort(int a[]){ aux = new int[a.length]; sort(a,0,a.length-1); } public static void sort(int a[],int lo,int hi){ if(lo>=hi) return; int mid = (lo+hi)/2; sort(a,lo,mid); sort(a,mid+1,hi); merge(a,lo,mid,hi); }
三:自底向上的归并排序(循环实现)
整个递归的归并排序思路是,先递归,一直到元素为1的时候返回,进行归并,再返回。
那么换种方法,我们能不能直接从元素为1的数组入手,一步一步向上归并呢?
答案是肯定的,我们用循环就能实现。
public static void sort2(int a[]){ int N = a.length; aux = new int [N]; for(int sz = 1;sz<N;sz=sz+sz)//每次的数组长度:1、2、4、8..... for(int lo=0;lo<N-sz;lo+=sz+sz)//每次归并的起点,要跨两个sz merge(a,lo,lo+sz-1,Math.min(lo+sz+sz-1,N-1));//一次归并 }
时间复杂度:
可以看出,归并的次数等于将数组对半分的次数。能分一次,就归并一次。所以次数为logN,底数为2。
一次归并的时间,如果包括了复制辅助数组的话,算一个N。后面的每次遍历都会比较,是同时进行的,也是N。总的算2N。
整个的时间复杂度是NlgN级别的。
空间复杂度:
用了一个辅助数组,所以是N。
网上也有常数空间复杂度的归并,但是那种方法,导致时间复杂度退化为了平方级,具体就补不贴代码了。
稳定性:
稳定