一、归并排序的理解
本质而言可以看作二叉树遍历的一种应用。
二、merge操作
merge方法用于将已经排好序的左右两边进行整理和合并。
假设要被排序的数组被分成了很多个小块, 在进行当前递归步骤之前,每一个小块上的元素已经是有序的了,需要通过merge操作将两个已经排号序的小块整理为一整块有序部分。
这里的merge操作在非递归实现和递归实现中是通用的。
//
public static void merge(int[] arr, int L, int M, int R){
int[] help = new int[R - L + 1]; // 设置一个长度为L到R元素个数的辅助数组help
int i = 0; // 为help使用所准备的指针
int p1 = L; // 设置一个左指针从左往右移动
int p2 = M + 1; // 设置一个右指针从右往左移动
while (p1 <= M && p2 <= R){ // 当左右指针都没有越界时
// help中放入p1或p2指针所对应的元素中教小的那个,i以及对应指针p1或p2移动一位
help[i++] = arr[p1] <= arr[p2] ? arr[p1++]:arr[p2++];
}
// 上面的while循环终止的条件是,要么p1越界,要么p2越界
// p1,p2不可能都越界,所以下面两个while循环只会有一个执行
// 然后将没越界的那个要走的剩余部分放入help中
while (p1 <= M){
help[i++] = arr[p1++];
}
while (p2 <= R){
help[i++] = arr[p2++];
}
// 然后将help数组中的内容重新刷入arr中
for (i = 0; i < help.length; i++){
arr[L + i] = help[i];
}
}
三、递归实现
public class Code01_MergeSort {
// 递归方法实现
public static void mergeSort1(int[] arr){
if(arr == null || arr.length < 2){
return;
}
process(arr, 0, arr.length - 1);
}
// 递归函数的定义:让arr在[L..R]范围上, 变成有序的
public static void process(int[] arr, int L, int R) {
if (L == R){ // base case: 当范围上只有一个数时停止划分
return;
}
int mid = L + ((R - L) >> 1); // 如果范围上不止一个数, 先选出中点
process(arr, L, mid);
process(arr, mid+1, R);
merge(arr, L, mid, R);
}
}
四、非递归(迭代)实现
用迭代的方法进行归并排序的话,可以理解为,自初始的有序长度1开始,不断对已经有序的左组和长度位置,未经排序的右组进行合并,在合并的过程中左右组会一起变得有序,每完成一次进行合并的区域长度倍增。
public static void mergeSort2(int[] arr){
if(arr == null || arr.length < 2){
return;
}
int N = arr.length;
// 如果设置组长度K,应从2开始
int mergeSize = 1; // 当前有序的,左组长度,从1开始
while(mergeSize < N){ // log N
int L = 0; // L用于枚举第N个左组的位置从哪里开始,一次归并完成会扩大mergeSize回到下标0位置重新开始新一轮归并
// 0 - ..
while(L < N){
// L .. M 当前的左组,其长度为mergeSize
int M = L + mergeSize - 1; // M的位置可以直接算出
if (M >= N){ // 当前左组的右端位置已经到达arr末端,可以不用再排序了
break;
}
// L..M M+1..R(mergeSize)
int R = Math.min(M + mergeSize, N - 1);// 右组右端的位置取决于剩余元素数量够不够和左组一样长
merge(arr, L, M, R);
L = R + 1;// 一次左右组归并完成,将L移动至R+1位置
}
// 加上下面这一句是为了防止溢出:可能会有未知的错误, 例如超过Int类型最大值(21亿左右)
if (mergeSize > N / 2){ // 判断2 * mergeSize是否大于arr长度
break;
}
mergeSize <<= 1; //每次完成一组排序后如果组长度仍然不比arr长度大,mergeSize*2
}
}
五、总结
递归操作的时间复杂度公式:
若递归操作符合:
- ⇒ 时间复杂度为:
- ⇒ 时间复杂度为:
- ⇒ 时间复杂度为:
d 表示除了递归调用之外剩余所有行为的时间复杂度。
计算时间复杂度:
尾项为merge的时间复杂度,merge的时间复杂度为,所以对于递归的归并排序而言,d=1。
两个2的来源是“二分等规模的递归操作”。
所以时间复杂度为:
归并排序的时间复杂度为。
归并排序相较于冒泡排序、选择排序和插入排序的时间复杂度,其优势在于把比较的结果作为有效信息固定了下来 ,而上面的三种排序,在一次遍历的比较操作中只有一次是有效的。