JDK的TimeSort就使用了归并排序。
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。若将2个有序表合并为一个有序表,称为二路归并。
归并排序:将集合排序时分为2大阶段。
- 归:将原集合不断拆分,拆分到每个小数组只剩下一个元素时,拆分过程就结束。
- 并:将拆分后的小数组不断合并,直到合并到整个数组,此时整个数组已经有序。
最关键就是合并过程:没有用到任何排序方法,只是元素比较。
递归版本:
/**
* 在arr上进行归并排序 稳定的
* @param arr
*/
public static void mergeSort(int[] arr) {
mergeSortInternal(arr, 0, arr.length - 1);
}
/**
* 在arr[l...r]上进行归并排序
* @param arr
* @param l
* @param r
*/
private static void mergeSortInternal(int[] arr, int l, int r) {
//优化① 减少若干次递归的过程
if(r - l <= 15){
//拆分后的小区间直接使用插入排序,不再递归
insertBase(arr, l, r);
return;
}
// if(l >= r){
// //区间只剩下一个元素,整个区间有序,不需要再排序
// return;
// }
//若r和l都特别大的时候,有溢出的风险 (r + l) >> 1
int mid = l + ((r - l) >> 1); //会规避溢出风险
//在拆分后的两个小数组上使用归并排序
//先排序左半区间
mergeSortInternal(arr, l, mid);
//再排序右半区间
mergeSortInternal(arr, mid + 1, r);
// //此时左半区间和右半区间已经有序,只需要合并两个小区间即可
// merge(arr, l, mid, r);
//优化② 减少若干次合并的过程
//arr[mid]是左区间的最后一个元素,arr[mid + 1]是右区间的第一个元素
//不是上来就和并,当两个小区间之间存在乱序时才合并
//arr[mid] < arr[mid + 1],说明左区间已经小于右区间的所有值,整个区间已经有序,不需要merge
if(arr[mid] > arr[mid + 1]) {
merge(arr, l, mid, r);
}
}
/**
* 将已经有序的arr[l...mid]和arr[mid + 1...r]合并为一个大的有序数组arr[l...r]
* @param arr
* @param l
* @param mid
* @param r
*/
private static void merge(int[] arr, int l, int mid, int r) {
//假设此时l = 1000, r = 2000
//开辟一个大小和合并后数组大小相同的数组
int[] temp = new int[r - l + 1];
//将原数组内容拷贝到新数组中
for (int i = l; i <= r; i++) {
//temp[0] = arr[1000]
//新数组的索引和原数组的索引有l个单位的偏移量
temp[i - l] = arr[i];
}
//遍历原数组,选择左半区间和右半区间的最小值写回原数组
//i对应于左半有序区间的第一个索引
int i = l;
//j对应右半区间的第一个索引
int j = mid + 1;
//k表示当前处理到原数组的哪个位置
for (int k = l; k <=r; k++) {
if(i > mid){
//此时左半区间已经全部处理完毕,将右半区间的所有值写回原数组
arr[k] = temp[j - l];
j++;
} else if(j > r) {
//此时右半区间已经全部处理完毕
arr[k] = temp[i - l];
i++;
} else if(temp[i - l] <= temp[j - l]) {
arr[k] = temp[i - l];
i++;
} else {
arr[k] = temp[j - l];
j ++;
}
}
}
稳定性:稳定的。
数组拆分不会造成元素乱序,元素的相对位置不会发生移动,当最终合并时,<=值默认放在左区间,因此合并过程也是稳定的。
时间复杂度:O(nlogn)
- 拆分过程:原数组长度为n,不断拆分数组,将元素一分为2,直到数组长度为1。n/2/.../2/2==1,总共拆分次数是logn。
- 合并过程:最终合并的大数组的长度为N,遍历N次,才能将数组元素合并完。
归并排序和堆排序一样,都是非常稳定的O(nlogn)的排序算法,不管集合元素是否有序,上来就将集合一分为二。
看到nlogn时间复杂度,想到树结构(不一定是二叉树)。logn结构一定都和树有关,此处归并排序和快速排序,logn的原因都在于递归树。
非递归版本:自下而上
/**
* 归并排序的非递归版本
* @param arr
*/
public static void mergeSortNonRecursion(int[] arr) {
//sz表示每次合并的元素个数,最开始从1个元素开始合并,(每个数组只有一个元素)
//第二次循环时,合并的元素个数就成了2(每个数组有两个元素)
//第三次循环时,合并的元素个数就成了4(每个数组有四个元素)
for (int sz = 1; sz <= arr.length; sz = sz + sz) { //个数和数组长度可以相等
//merge过程,i表示每次merge开始的索引下标
for (int i = 0; i + sz < arr.length; i += sz + sz) { //索引比数组长度小1
//i + sz表示第二个小数组的开始索引 < n 表示还有右半区间要合并
//当sz长度过大时,i + 2sz - 1会超过数组长度了
merge(arr, i, i + sz - 1, Math.min(i + 2 * sz - 1, arr.length - 1) ); //数组长度和索引有一个差值
//Math.min(i + 2 * sz - 1, arr.length - 1)极端情况:当sz = arr.length时,要合并整个数组,2 * sz = 2n,没这个索引,此时我们就取arr.length - 1作为合并的最右侧索引值
}
}
}