归并排序详解,Java版描述。
为了简单起见,使用int类型数组讲述归并算法,后面扩展到其他类型的排序。
目录
1.1 用具体例子说明
十人排序问题。
- 将十人均分为两队
- 五人分为三人,二人两队
- 对于三人的队伍,再次分成两人和一人的队伍
- 对于两人的队伍分成一人一队
- 循环上面几步,直到有十队时,结束。
- 那么此时所有队伍都是一人一队
- 找到相邻的两队进行有序合并。
- 循环上一步直到只有一个队伍时截止。
- 此时就已经完成了排序
1.2 排序思想
分而治之:将一个大数组均分为两个大小相等的数组(奇数容量下有 1 的差值),然后进行排序的话,时间就从对T = O(n^2)操作变成了2*O((n/2)^2) =2*O((n^2)/4) = O(n^2) / 2 = T / 2,时间缩小了一倍,我们将这个过程循环下去,直到所有数组都被分成一个元素为止,由归纳可得时间为O(n*lgn),之后进行循环有序合并,那么明显时间为O(n),舍去,所以归并排序将基础排序中的O(n^2)改进为了O(n*lgn);有效的改进了时间复杂度。
1.3 见名知意
归并排序就是将原来的数组分为两个小的数组就行排序之后进行合并,因为常常使用递归实现(由先拆分后合并的性质决定的),所以我们称其为归并排序。
1.4 抽象过程
- 对于一个大小为n的待排序数组,将其划分为n个小数组,即每个数划分为一个数组,对于只有一个元素的数组,那么该数组有序。
- 有序合并相邻相同大小的数组,即合并两个有序数组为一个有序数组。
- 循环上一步直到所有小数组合并到一个数组中为止。
- 此时排序就已经在不断的有序中完成,数组有序。
1.5 实例操作
1.6 代码实现(JAVA版本)
这个排序属于高级排序,比较抽象,所以我划分为多个方法进行实现,力求每个方法都不难理解。
而且因为归并排序是java语言底层的Arrays.sort()的实现,所以力求学会归并,并且可以看懂sort源码。
1.6.1调用排序部分
// 两次入口以便他人调用(Java底层的sort就是这么做的)
public static void mergeSort(int[] a, int lo, int hi) {
// 归并两个数组需要额外的数组空间,我们直接在此地new一个,防止递归时循环申请空间
int[] temp = new int[a.length];
// 接下来进入递归体中。专门提取出来便于理解递归。
mergeSort(a, temp, lo, hi);
}
public static void mergeSort(int[] a) {//为了方便使用,直接转发至另一个排序实体中。
mergeSort(a, 0, a.length - 1);
}
我们提供给外界两个调用归并的接口,默认排序调用条件排序。条件排序中申请了一个中间数组,这是因为归并两个数组要有额外数组。(你可以试一试不用额外数组实现自身归并,即使你可以实现,你也会发现时间损耗将失去归并的O(n*lgn)).
1.6.2递归实体
/**
* Description: 归并算法实现
*
* @Title: mergeSort
* @param a
* 原数据位置
* @param temp
* 用来存放数组
* @param lo
* 逻辑数组中的下界,递归中频繁使用。
* @param hi
* 逻辑数组中的上界,递归中频繁使用。
*void
*/
private static void mergeSort(int[] a, int[] temp, int lo, int hi) {
int mid = (lo + hi) / 2; // 求切分点
if (hi - lo > 1) { // 子数组长度大于1时,进行拆分
// 拆分两个数组
mergeSort(a, temp, lo, mid); //递归
mergeSort(a, temp, mid + 1, hi); // 递归,注意mid已经属于前一个数组,所以记得+1
}
//合并两个数组a[lo..mid]和 a[mid..hi],两个数组都是有序数组,合并出来一个有序数组
merge(a, temp, lo, mid, hi);
}
接下来进入重点,两个有序的逻辑数组的合并。
我们首先将两个有序数组合并到一个中间数组中,然后遍历放回到原来的数组中。(无法实现不借用外空间的合并)
注:此时便可以看懂我们为什么要将 temp 数组从外面的调用中传入进来,以防止在递归中反反复复的申请,初始化。
1.6.3合并两个数组
private static void mergeArr(int[] a, int[] temp, int lo, int mid, int hi) {
// 初始化arr1的上界与下界
int arr1Begin = lo;
int arr1End = mid;
// 初始化arr2的上界与下界
int arr2Begin = mid + 1;
int arr2End = hi;
// 初始化上面的目的:1.可读性强 2.在下面的逻辑中我们需要改变两个Begin的值,保存lo,mid,hi 的值。
// 初始化中间数组的起始位置。
int k = lo;
// 所有使用到的变量进行初始化完毕,开始合并arr1和arr2
while (arr1Begin <= arr1End || arr2Begin <= arr2End) { // 当两个数组都没有结束时,进入循环
if (arr1Begin > arr1End) { // 说明1号数组中已经无值,只需将二号数组放入结果resultArr数组中即可
temp[k++] = a[arr2Begin++];
} else if (arr2Begin > arr2End) { // 同上。
temp[k++] = a[arr1Begin++];
} else {// 两个数组都没有结束,比大小添加。
if (a[arr1Begin] < a[arr2Begin]) {
temp[k++] = a[arr1Begin++];
} else {
temp[k++] = a[arr2Begin++];
}
}
}
// 将结果数组中的值取出放到排序数组a[]中,完成a中数组排序。
for (int i = lo; i <= hi; i++) {
a[i] = temp[i];
}
}
1.7 代码实现(C语言版)
TODO,改天写
1.8 算法分析
归并排序是所有排序中快且稳定的,时间为O(n*lgn),但是明显的,他需要一个辅助数组,在大数组中,一个相同大小的辅助数组也是很恐怖的。
快速排序则是另一种实现了O(n*lgN)排序,而且不需要额外空间,但是代价是失去了稳定性。我们总是在权衡中进行前进。
还有一个非常大的改进空间就可以当做java1.6之前的底层的sort使用了(系统的sort里快排,插排,归并,计数排序都用到了,根据数组长度和类型sort会选择不同的排序方法)
因为归并排序会将一个大数组分割为若干个小数组,我们加一个判断,当小数组的长度小于某个值(一般选取9~15)时,转为插入排序(插入排序也是稳定的),所以既可以保证稳定性又能明显改善归并排序实际使用时间。
java底层的sort一般开始都有一个if语句,就是为了在不同的递归下使用,毕竟小数组的话,谁都一样,大数组的话,可以明显改善。
1.9全部源码
package sort;
/**
* Description: 归并排序
*
* @ClassName: MergeSort
* @author 过道
* @date 2018年8月11日 上午10:42:38
*/
public class MergeSort {
public static void mergeSort(int[] a, int lo, int hi) {
// 归并两个数组需要额外的数组空间,我们直接在此地new一个,放置递归时循环申请空间
int[] temp = new int[a.length];
mergeSort(a, temp, lo, hi);
}
public static void mergeSort(int[] a) {
mergeSort(a, 0, a.length - 1);
}
/**
* Description: 归并算法实现
*
* @Title: mergeSort
* @param a
* 原数据位置
* @param temp
* 用来存放数组
* @param lo
* @param hi
* void
*/
private static void mergeSort(int[] a, int[] temp, int lo, int hi) {
int mid = (lo + hi) / 2;
if (hi - lo > 1) { // 子数组长度大于1时,进行拆分
mergeSort(a, temp, lo, mid);
mergeSort(a, temp, mid + 1, hi);
}
merge(a, temp, lo, mid, hi);
// 合并a[lo..mid]和 a[mid..hi] //此时两个数组都是有序数组
}
/**
* Description:
*
* @Title: merge
* @param a
* @param temp
* @param lo
* @param mid
* @param hi
* void
*/
private static void merge(int[] a, int[] temp, int lo, int mid, int hi) {
int arr1Begin = lo;
int arr1End = mid;
int arr2Begin = mid + 1;
int arr2End = hi;
int k = lo;
while (arr1Begin <= arr1End || arr2Begin <= arr2End) {
if (arr1Begin > arr1End) { // 说明1号数组中已经无值
temp[k++] = a[arr2Begin++];
} else if (arr2Begin > arr2End) { // 说明二号数组中已经无值
temp[k++] = a[arr1Begin++];
} else {// 两个数组都没有结束,比大小添加。
if (a[arr1Begin] < a[arr2Begin]) { // 比大小添加
temp[k++] = a[arr1Begin++];
} else {
temp[k++] = a[arr2Begin++];
}
}
}
for (int i = lo; i <= hi; i++) {
a[i] = temp[i];
}
}
public static void main(String[] args) {
int a[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
mergeSort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
}