【注】以下代码参考力扣<<排序算法全解析>>
想要很好的了解归并排序,建议先了解一下插入排序。
- 了解归并排序需要先来看看,两个有序的数组如何结合成为一个有序的数组。按照之前所学的排序方式,可以将其中一个有序数组不断插入另一个有序数组亦或是直接拼在一起冒泡等。但这样是不是太暴力了,我们希望能够充分利用两个数组的有序这一特征。可以牺牲一定的空间复杂度换取时间复杂度。新开辟一个能容纳两个数组的(空间)数组,两个数组从前往后开始取出元素进行比较,小(或大)的元素有序放在新数组里。
归并排序:先将一组数不断地进行分组,例如先分成两个部分、每个部分再分成两个部分…以此类推,如果存在部分只有一个元素时,可以认为该部分已经有序,所以我们就可以按照合并两个有序数组的方法先将每两个部分(每个部分一个元素)合并为一个部分(每个部分两个元素),再将每两个部分(每个部分两个元素)合并为一个部分(每个部分四个元素)…以此类推,最终变成一个部分即所有元素有序。
有些朋友可能会疑惑,如果合并的两个部分元素个数不同,会对结果有影响吗?当然没有影响。在上述条件下,两个部分中一定会有一个部分先把所有元素放入新数组中,并且因为两个部分是有序的,另一个部分的剩余元素也一定是有序的,只需要将剩余元素继续放入新数组就行了。
public static void mergeSort(int[] arr) {
if (arr.length == 0) return;
int[] result = new int[arr.length];
mergeSort(arr, 0, arr.length - 1, result);
}
// 对 arr 的 [start, end] 区间归并排序,每次迭代只在规定范围进行操作,所以其他范围不会受影响
private static void mergeSort(int[] arr, int start, int end, int[] result) {
// 只剩下一个数字,停止拆分
if (start == end) return;
int middle = (start + end) / 2;
// 拆分左边区域,并将归并排序的结果保存到 result 的 [start, middle] 区间
mergeSort(arr, start, middle, result);
// 拆分右边区域,并将归并排序的结果保存到 result 的 [middle + 1, end] 区间
mergeSort(arr, middle + 1, end, result);
// 合并左右区域到 result 的 [start, end] 区间
merge(arr, start, end, result);
}
// 将 result 的 [start, middle] 和 [middle + 1, end] 区间合并
private static void merge(int[] arr, int start, int end, int[] result) {
//这里end1就是middle,也即第一个部分的末尾
int end1 = (start + end) / 2;
//这里start2就是middle+1,也即第二个部分的开头
int start2 = end1 + 1;
// 用来遍历两个部分的指针
int index1 = start;
int index2 = start2;
while (index1 <= end1 && index2 <= end) {
if (arr[index1] <= arr[index2]) {
//只要有数放入result数组,其下标就要更新:start+(index1-start)+(index2-start2) = index1 + index2 - start2,表示为start加上两个指针的偏移量,也即当前向result放入元素的下标。
result[index1 + index2 - start2] = arr[index1++];
} else {
result[index1 + index2 - start2] = arr[index2++];
}
}
// 其中某个部分元素已经全部放入result数组后,将另一部分剩余元素补到result数组之后
while (index1 <= end1) {
result[index1 + index2 - start2] = arr[index1++];
}
while (index2 <= end) {
result[index1 + index2 - start2] = arr[index2++];
}
// 更新arr,将 result 操作区间的数字拷贝到 arr 数组中,以便下次比较
while (start <= end) {
arr[start] = result[start++];
}
}
数组会被拆分 logn 次,每层执行的比较次数都约等于 n 次,所以时间复杂度是 O(nlogn)。归并排序是一种稳定的排序算法。
【注】 归并排序在排序规模较大的时候可能会出现栈溢出的情况,我们可以用不同步长的循环实现非递归形式的归并来解决这个情况。
public static void mergeSort(int[] arr) {
if (arr.length == 0) return;
mergeSort(arr);
}
private static void mergeSort(int[] arr) {
int n = arr.length;
int[] temp = new int[n];
// 从单个元素开始进行组合,可以将gap视为每组数据的个数,每次翻倍
int gap = 1;
while(gap < n){
for (int i = 0; i < n; i += 2 * gap){//因为相邻两组要合并,所以i的步长为2倍的gap
//本次循环中的[i, i + gap - 1] 和 [i + gap, i + 2 * gap - 1]两组数据进行合并
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap,;
int end2 = i + 2 * gap - 1;
//可能多出来部分数据无法构成完整的两组数据。
//归并是从单个元素开始的,子任务的核心思想是将两个有序的排列合并成一个有序的排列
if (begin2 >= n)//这两组有序排列中后一组不存在,则无需再进行合并,直接break
break;
//若后一组存在,但组内元素比前一组少,会导致归并过程中右半区间越界,需要修正一下
if (end2 >= n)
{
end2 = n - 1;
}
int index = i;//index用于新建的result索引
//这里也可以不动begin1、begin2、end1、end2,可以同上递归形式给出index1=begin1,index2=begin2,然后用index1和index2进行操作
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
temp[index++] = arr[begin1++];
}
else
{
temp[index++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
temp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
temp[index++] = arr[begin2++];
}
//拷贝进去
for (int j = i; j <= end2; ++j)
{
arr[j] = temp[j];
}
}
gap *= 2;
}
}