描述
归并排序是将一个大的无序序列,可以先将它分成两半分别排序(递归地),然后将结果归并起来,最终将整个无序序列排序成有序序列。归并排序能够将长度为N的序列排序所需时间和NlongN成正比,归并排序的缺点是需要花费额外和N成正比的空间。归并排序是一种渐进最优的基于比较排序的算法。
归并排序使用的就是分治思想,分治,顾名思义就是分而治之,将一个大问题分解成小的问题来解决。小的问题解决了,大问题也就解决了。
性质
稳定性:稳定
时间复杂度:O(NlogN)
空间复杂度: O(N)
归并排序实现
public class MergeSorted extends AbstractSorted {
// 归并所需要的辅助空间
private Comparable[] temp;
@Override
public <T extends Comparable<T>> void sort(T[] arrays) {
// 分配空间
temp = new Comparable[arrays.length];
// 归并排序
sort(arrays, 0, arrays.length - 1);
}
/**
* 归并排序
* @param arrays 原数据
* @param low 低位下标(左下标)
* @param height 高位下标(右下标)
*/
private void sort(Comparable[] arrays, int low, int height) {
// 表示已经不能将数组分解成更小的数组了
if (height <= low) {
return;
}
// 获取数组中间下标位置
int mid = low + (height - low) / 2;
// 分治递归,将数组分成左部分[low, mid]和右部分[mid+1, height]进行处理
// 左半边排序
sort(arrays, low, mid);
// 右半边排序
sort(arrays, mid + 1, height);
// 归并结果(将左部分[low, mid]和右部分[mid+1, height]排序后的结果进行归并)
// 注释: 此时左部分[low, mid]和右部分[mid+1, height]是已经排好顺序的。归并只是将小结果归并成大结果。
merge(arrays, low, mid, height);
}
/**
* 归并排序, 左部分[low, mid]和右部分[mid+1, height]是已经排好顺序的。归并只是将小结果归并成大结果。
* <p>
*
* 假设arrays=[3, 8, 9, 11, 1, 5, 7], low = 0, mid = 3, height=6
* 此时,temp = arrays = [3, 8, 9, 11, 1, 5, 7], left = low = 0, right = mid + 1 = 4
*
* 1. k = 0, left = 0, right = 4
* -> if走分支3:将temp[right=4]放入到arrays[k=0]中, 此时arrays=[1, 8, 9, 11, 1, 5, 7]
* 2. k = 1, left = 0, right = 5
* -> if走分支4: 将temp[left=0]放入到arrays[k=1]中, 此时arrays=[1, 3, 9, 11, 1, 5, 7]
* 3. k = 2, left = 1, right = 5
* -> if走分支3: 将temp[right=5]放入到arrays[k=2]中, 此时arrays=[1, 3, 5, 11, 1, 5, 7]
* 4. k = 3, left = 1, right = 6
* -> if走分支3: 将temp[right=6]放入到arrays[k=3]中, 此时arrays=[1, 3, 5, 7, 1, 5, 7]
* 5. k = 4, left = 1, right = 7
* -> if走分支2: 右边的数据已经归并完了(right > height),直接将左边数据拷贝, 此时arrays=[1, 3, 5, 7, 8, 5, 7]
* 6. k = 5, left = 2, right = 7
* -> if走分支2: 此时arrays=[1, 3, 5, 7, 8, 9, 7]
* 7. k = 6, left = 3, right = 7
* -> if走分支2: 此时arrays=[1, 3, 5, 7, 8, 9, 11]
* 归并排序完成,arrays已经排好序。
* </p>
* @param arrays
* @param low
* @param mid
* @param height
*/
private void merge(Comparable[] arrays, int low, int mid, int height) {
// 左移动位置
int left = low;
// 右移动位置
int right = mid + 1;
// 把原数组中需要排序的元素拷贝到辅助空间中去
for (int k = low; k <= height; k ++) {
temp[k] = arrays[k];
}
for (int k = low; k <= height; k ++) {
// 分支1: 左边已经归并完了,直接拷贝右边
if (left > mid) {
arrays[k] = temp[right ++];
}
// 分支2: 右边已经归并完了,直接拷贝左边
else if (right > height) {
arrays[k] = temp[left ++];
}
// 分支3: 右边数据小, 将小到元素放入到原数组中
else if (less(temp[right], temp[left])) {
arrays[k] = temp[right ++];
}
// 分支4: 将小到元素放入到原数组中, 左边数据小
else {
arrays[k] = temp[left ++];
}
}
}
}
归并排序的性能分析
稳定性分析
结合前面的图和归并排序代码,可以发现,归并排序是否稳定关键在于要看合并阶段merge()函数,merge()函数将两个有序子数组合并成一个有序数组。在合并过程中,如果arrays[low, mid]和arrays[mid+1, heigh]中有相同的元素,在merge()函数中比较时走分支4,所以可以保证相同元素的先后顺序是不变的。所以归并排序是稳定排序算法。
时间复杂度分析
归并排序涉及到递归,假设对n个元素进行归并排序需要时间T(n),分解成2个子数组排序的时间都是T(n/2), merge()函数合并两个有序子数组的时间复杂度是O(n), 所以,归并排序的时间复杂度计算公式为:
T(1) = C; (C为常数,只需要常数量级的执行时间)
T(n) = 2 * T(n/2) + n; n > 1
根据上面公式我们可以推导出以下公式:
T(n) = 2 * T(n/2) + n
= 2 * (2 * T(n/4) + n/2) + n = 4 * T(n/4) + 2 * n
= 4 * (2 * T(n/8) + n/4) + 2 * n = 8 * T(n/8) + 3n
......
= 2^k * T(n/2^k) + k * n
最终,当n/2^k = 1时,T(n/2^k) = T(1) = C, 此时k = log(n)。我们就给k=log(n)代入到T(n) = 2^k * T(n/2^k) + k * n中,可以得到,T(n) = 2 * C * n + n * log(n)。使用大O标记法来表示就是T(n) = O(nlogn)。