基本思想
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。
假设原始数据有n个,可以将n个数据看成n个独立的有序子序列,每个子序列的长度为1。这就将大问题分成了n个子问题。这是”分“的思想
相邻子序列两两比较并排序,得到[n/2]个长度为2或1的有序子序列;再两两比较并排序,一种重复下去,直到得到一个长度为n的有序序列为止。这就是”治“的思想
分
按照递归的写法。将整个序列分成n个独立的有序子序列。
治(合并)
再来看看治阶段,需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
代码实现
import java.util.Arrays;
public class A {
static int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
static int len = arr.length;
public static void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 下面的三个while:总的来说就是合并两个有序数组为一个有序数组(新建)
// 第四个while:把新的有序数组覆盖到旧的无序数组空间
// 把较小的数先移到新数组中
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = arr[i++];
}
// 把右边边剩余的数移入数组
while (j <= right) {
temp[k++] = arr[j++];
}
// 将temp中的元素全部覆盖到原数组中
// 这个步骤是是需要重复多次的。先覆盖最小的串,越来越大,最后覆盖整个长串
int t = 0;
while (left <= right) {
arr[left++] = temp[t++];
}
}
public static void mergeSort(int[] a, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
// 左边
mergeSort(a, left, mid);
// 右边
mergeSort(a, mid + 1, right);
// 左右归并
merge(a, left, mid, right);
//System.out.println(Arrays.toString(a));// 输出每一趟的排序结果
}
}
public static void main(String[] args) {
mergeSort(arr, 0, len - 1);
System.out.println("最终排序结果:" + Arrays.toString(arr));
}
}
复杂度分析
-
时间复杂度
每次合并操作的平均时间复杂度为,而完全二叉树的深度为。总的平均时间复杂度为。 而且,归并排序的最好,最坏,平均时间复杂度均为。与初始序列排列状态无关。
至于拆分其实是抽象上的,可以用下标来表示,一次拆分只需要时间。拆分的次数时可以计算的:第一层拆分1次,第二层拆分2次,第三层拆分4次。。。一直到倒数第二层,最后一层只有一个数据不会拆分。所以拆分操作的用时为:
-
空间复杂度
用顺序表实现归并排序时,需要一个数组来暂存整个待排序数组,故空间复杂度为
算法特性
- 是稳定的排序
- 可用于顺序存储结构,也可用于链式存储结构。并且使用链式存储结构使不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈。
- java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。
重要的应用:求逆序数的个数
归并排序的基本思想是分治,在治的过程中有前后数字的大小对比,此时就是统计逆序对的最佳时机。具体看下面的代码段(只加入了一个ans变量记录个数,其他部分完全不用动):
int ans=0;
...
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
temp[k++] = arr[i++];
}
else {
// 如果使用ans+=(mid-i+1); 的写法,在牛客的编译器上,当遇到大规模输入时,会溢出
ans=ans+mid-i+1;
temp[k++] = arr[j++];
}
}
- 因为当前左半段和右半段都是有序的,所以,如果arr[i]>arr[j]了,那么arr[i]到arr[mid]这几个数,都肯定比arr[j]大,所以这mid-i+1个数,都与arr[j]构成逆序数
- 因为检测到这个现象后会立马把arr[j]放在暂存数组的尾部,构成有序的数列,所以代码后续不会再运行到这个else片段里,也就不会出现重复计数的情况