分治算法:
分治算法和冒泡排序一样有着好听的名字。工作方案如下:
1. 将一个问题划分为同一类型的若干子问题,子问题最好规模相同。
2. 对这些子问题求解(一般用递归)
3. 有必要的话,合并这些子问题,得到原始问题的答案。
归并排序:
根据分治思想,我们可以实现归并排序:要将一个数组排序,可以先(递归地)将它分成两半分别排序,然后将结果归并起来。这个算法的基本操作是合并两个已排序的表,因为这两个表是有序的,所以可以通过一次遍历来完成(花费线性时间)。
实现分析:
实现归并的一种直接的方法是将两个不同的有序数组归并到第三个数组中,这会导致归并排序的空间复杂度为O(N),这也是归并排序的主要缺点。我们可以修改代码,使得它可以在原地归并,但已有的实现都过于复杂,没有实际意义。
如果在每次递归时都生成一个临时数组,那么在任一时刻就可能用logN个临时数组处在活动期。但实际上,因为merge方法在mergeSort的最后一行,我们可以只使用一个公有的数组。我在实现中把该数组放在类的下面,当然也可以将它放在public型的mergeSort方法中,通过参数传递。
以下是归并排序的自顶向下的实现代码:
private static Integer[] aux;
public static void mergeSort(Integer[] r){
aux = new Integer[r.length];
mergeSort(r, 0, r.length - 1);
}
private static void mergeSort(Integer[] r, int left, int right){
if(right<=left)//如果数组大小不大于1,不再递归
return;
int mid = (left+right)/2;
mergeSort(r, left, mid);
mergeSort(r, mid+1, right);
merge(r, left, mid, right);
}
private static void merge(Integer[] r, int left, int mid, int right){
int i=left, j=mid+1;
for(int k=left; k<=right; k++)
aux[k] = r[k];
for(int k=left; k<=right; k++)
if(i>mid)//mid是左侧数列的最后一个位置。这里判断i是否溢出
r[k] = aux[j++];
else if(j>right)
r[k] = aux[i++];
else if(aux[i]<aux[j])//比较元素的大小
r[k] = aux[i++];
else
r[k] = aux[j++];
}
算法分析:
假设数组的长度N是2的k次方(N=2^k),这样的话我们总可以把它分成两个等长的数组。对于N=1,归并排序所用时间为1;否则,归并排序所有时间为排序两个子数组的时间加上合并的时间。
计算过程如下:
对于N不是2的幂次的数组,答案几乎是一样的。所以归并排序的时间复杂度为O(NlogN),空间复杂度为O(N)。
与其他的O(NlogN)排序算法比较,归并排序的运行时间严重依赖于比较元素和在数组中移动元素的相对开销,这和语言有关。如,在Java中比较操作是昂贵的,但移动元素的开销的较小的;而C++通常相反。
总结:
归并排序的时间复杂度为O(NlogN),空间复杂度为O(N)。可以自顶向下或自底向上地实现。对于长度很小的数组,递归地开销还是比较昂贵的,我们可以对小数组使用插入排序这样的算法,以此改进归并排序的性能。总之,归并排序是一种渐进最优的基于比较排序的算法。