1.基本思想
归并:将两个或两个以上的有序表组合成一个新的有序表
归并算法采用“分治思想”。设初始序列有n个元素,可以看作n个有序的子序列,每个子序列长度为1,将子序列两两归并,得到[n/2]个长度为1或2的子序列,再重复两两归并直到得到一个长度为n的序列
2.算法实现
递归实现
public static <AnyType extends Comparable<? super AnyType>> void mergesort(AnyType[] a)
{
//新建一个临时数组用来存放每次归并的数据
AnyType[] tmpArray = (AnyType[])new Comparable[a.length];//泛型不可以直接创建数组,需要其可实例化父类创建实例并强制类型转换
mergesort(a, tmpArray, 0, a.length - 1);
}
private static <AnyType extends Comparable<? super AnyType>> void mergesort(AnyType[] a, AnyType[] tmpArray, int left, int right)
{
if(left < right)//最小的序列只有一个元素
{
int center = (left + right) / 2;//中心位置
mergesort(a, tmpArray, left, center);//左子序列:不断减小子序列长度,进行归并排序(递归)
mergesort(a, tmpArray, center + 1, right);//右子序列:不断减小子序列长度,进行归并排序(递归)
merge(a, tmpArray, left, center + 1, right);//将两个序列归并
}
}
private static <AnyType extends Comparable<? super AnyType>> void merge(AnyType[] a, AnyType[] tmpArray, int leftPos, int rightPos, int rightEnd)
{
int leftEnd = rightPos - 1;//左子序列尾端
int tmpPos = leftPos;//临时数组的指针
int numElements = rightEnd - leftPos + 1;//子序列长度
//当左右子序列都有元素可以进行比较时
while(leftPos <= leftEnd && rightPos <= rightEnd)//等号表示子序列只有一个元素
if(a[leftPos].compareTo(a[rightPos]) < 0)
tmpArray[tmpPos++] = a[leftPos++];
else
tmpArray[tmpPos++] = a[rightPos++];
//当右子序列已归并完时,将左子树剩余元素存入临时数组
while(leftPos <= leftEnd)
tmpArray[tmpPos++] = a[leftPos++];
//当左子序列已归并完时,将右子树剩余元素存入临时数组
while(rightPos <= rightEnd)
tmpArray[tmpPos++] = a[rightPos++];
//将临时数组的数据复制到原数组
for(int i = 0; i < numElements ; i++, rightEnd --)
a[rightEnd] = tmpArray[rightEnd];
}
}
非递归实现
public static <AnyType extends Comparable<? super AnyType>> void mergesort2(AnyType[] a)
{
AnyType[] tmpArray = (AnyType[])new Comparable[a.length];
int len = 1;//归并的子序列长度
while(len < a.length)
{
mergePass(a, tmpArray, len);//将a中元素归并到临时数组里
len *= 2;
mergePass(tmpArray, a, len);//将临时数组里的元素归并回a
len *= 2;
}
}
private static <AnyType extends Comparable<? super AnyType>> void mergePass(AnyType[] a, AnyType[] tmpArray, int len)
{
int leftPos = 0;
while(leftPos + len * 2 - 1 < a.length )//右序列尾不能超过数组
{
int rightPos = leftPos + len;
int rightEnd = leftPos + len * 2 - 1;
merge(a, tmpArray, leftPos, rightPos, rightEnd);//两两归并
leftPos = rightEnd + 1;//下一个要归并的两个序列的左开头位置
}
//归并最后两个序列
if(leftPos < a.length - len)
merge(a, tmpArray, leftPos, leftPos + len, a.length - 1);
//将剩下的单个子序列归并进临时数组
else
for(; leftPos < a.length; leftPos++)
tmpArray[leftPos] = a[leftPos];
}
两个算法共用一个merge方法
3.算法分析
- 一趟归并(扫描所有元素并两两和并)需要耗费O(n)时间,由完全二叉树的深度可知,整个归并排序需要进行log(2)n次,因此总时间复杂度为O(n*logn),最好、最坏、平均时间复杂度均是如此
- 由于在归并过程中需要与原始数据相当数量的存储空间存放归并结果,以及递归时深度为log(2)n的栈空间,因此空间复杂度为O(n)
- 递归排序是两两比较,不存在跳跃,因此是一种稳定的算法
- 递归排序是一种比较占内存,但效率高且稳定的算法
- 递归排序使用所有流行算法中最少的比较次数,是标准Java类库中泛型排序所使用的算法(泛型排序中比较是昂贵的(比较可能不容易被内嵌,从而动态调度的开销可能会减慢执行的速度),而移动元素是简单的(引用的仅仅是赋值,而不是对象的拷贝))
- 非递归实现避免了深度为log(2)n的栈空间,空间上只使用了临时数组,空间复杂度为O(n),由于避免递归因此时间性能也有提升
- 在使用归并排序时,非递归实现是更好的方法