归并排序算法(基于Java实现)
归并排序算法的原理与代码实现:
一、归并排序算法的原理
其核心思想其实蛮简单的,就是如果我们要排序一个数组,我们先数组从中间分成前后两部分,然后对前后两部分分别排序,然后再将排好序的两部分合并在一起,这样整个数组就都有序了。总而言之,归并排序使用的就是分治思想,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。而分治算法一般都是用递归来实现的,分而治之是一种解决问题的处理思想,递归则是一种编程技巧。
可以先来看一段伪代码:
1 | // 归并排序算法, A是数组,n表示数组大小 |
你可能已经发现了,merge(A[p…r], A[p…q], A[q+1…r]) 这个函数的作用就是,将已经有序的 A[p…q]和 A[q+1…r]合并成一个有序的数组,并且放入 A[p…r]。那这个过程具体该如何做呢?
如图所示,我们申请一个临时数组 tmp,大小与 A[p…r]相同。我们用两个游标 i 和 j,分别指向 A[p…q]和 A[q+1…r]的第一个元素。比较这两个元素 A[i]和 A[j],如果 A[i]<=A[j],我们就把 A[i]放入到临时数组 tmp,并且 i 后移一位,否则将 A[j]放入到数组 tmp,j 后移一位。
继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并之后的结果了。最后再把临时数组 tmp 中的数据拷贝到原数组 A[p…r]中。
二、归并排序算法的代码实现
1 | package com.company; |
输出的结果为:
1 | 之前的排序: |
三、代码优化
可以利用“哨兵”简化编程的技巧,如果上述的merge()合并函数借助“哨兵”,代码就会简洁很多。
哨兵方法:
先将要合并的两个放到tmpLeft和tmpRight数组,其中每个数组都多出一个位置放哨兵 ;
tmpLeft[leftSize] = int.MaxValue; tmpRight[rightSize] = int.MaxValue;完整的merge函数代码如下:(只需要将该段代码与上述的merge函数代码段替换即可,就能够实现代码的优化。)1
2
3
4
5
6
7
8
9
10
11
3. 比较两个tmp数组,哪个小就放到原数组,使用哨兵不用再判断是否有剩下的有序数组。
```java
for (k=left,i=0,j=0; k<=right; k++){
if(tmpLeft[i] < tmpRight[j]){
arr[k] = tmpLeft[i]; i++;
}else{
arr[k] = tmpRight[j]; j++;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30private static void mergeBySentry(int[] arr, int p, int q, int r){
int[] leftArr = new int[q - p +2];
int[] rightArr = new int[r - q + 1];
for(int i=0; i<=q - p; i++){
leftArr[i] = arr[p + i];
}
//第一个数组添加哨兵(最大值)
leftArr[q - p + 1] = Integer.MAX_VALUE;
for(int i = 0; i<r-q; i++){
rightArr[i] = arr[q+1+i];
}
//第二个数组添加哨兵(最大值)
rightArr[r-q] = Integer.MAX_VALUE;
int i = 0;
int j = 0;
int k = 0;
while(k<=r){
//当左边数组达到哨兵值时,i不再增加,直到右边数组读取完剩余值,同理右边数组也一样
if(leftArr[i] <= rightArr[j]){
arr[k++] = leftArr[i++];
}else{
arr[k++] = rightArr[j++];
}
}
}
四、归并排序的性能分析
结合我前面画的那张图和归并排序的伪代码,你应该能发现,归并排序稳不稳定关键要看 merge() 函数,也就是两个有序子数组合并成一个有序数组的那部分代码。在合并的过程中,如果 A[p…q]和 A[q+1…r]之间有值相同的元素,那我们可以像伪代码中那样,先把 A[p…q]中的元素放入 tmp 数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法。
从我们的原理分析和伪代码可以看出,归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,**时间复杂度都是 O(nlogn)**。
归并排序并没有像快排那样,应用广泛,这是为什么呢? 因为它有一个致命的“弱点”,那就是归并排序不是原地排序算法。
实际上,递归代码的空间复杂度并不能像时间复杂度那样累加。刚刚我们忘记了最重要的一点,那就是,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以**空间复杂度是 O(n)**。
- 本文作者: feng之锋
- 本文链接: http://example.com/2020/12/15/归并排序算法/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!
