本文内容和代码来源于数据结构教材。
归并排序(Merging Sort)是又一类不同的排序方法。"归并"的含义是将2个或2个以上的有序表组合成1个新的有序表。无论是顺序存储还是链表存储结构,都可在O(m+n)的时间量级上实现。归并的基本思想如下:
假设初始序列含有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到
⌈
n
2
⌉
\lceil \frac{n}{2} \rceil
⌈2n⌉个长度为2或1的有序子序列;再两两归并,… …, 如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2-路归并排序。如下图
2-路归并排序中的核心操作是将一维数组中前后相邻的两个有序序列归并为一个有序序列。
java实现
void merge(int[] sr, int[] tr, int i, int m, int n) {
if (i == n) { // 数组最后一个元素的处理
tr[i] = sr[i];
return;
}
int k = 0;
int j = 0;
// 将SR[i..m]和SR[m+1..n]归并为有序的TR[i..n]
for (j = m + 1, k = i; i <= m && j <= n; ++k) {
if (sr[i] <= sr[j]) // 将SR中记录由小到大地并入TR
tr[k] = sr[i++];
else
tr[k] = sr[j++];
}
if (i <= m) {
for (int u = i, q = 0; u <= m; u++) {// 将剩余的SR[i...m]复制到TR
tr[k + q] = sr[u];
++q;
}
}
if (j <= n) {
for (int u = j, q = 0; u <= n; u++) {// 将剩余的SR[j...n]复制到TR
tr[k + q] = sr[u];
++q;
}
}
}
int[] mergSort(int[] arr) {
int[] sr = arr;
int[] tr = new int[arr.length];
int h = 1, len = arr.length;
// 需要进行几趟归并,以2为底,len的对数向上取整。
int count = (int) Math.ceil(log(len, 2));
for (int x = 0; x < count; x++) {
// 每一趟中,需要进行归并的分组数,len/2h得到的小数向上取整。
int ceil = (int) Math.ceil((float) len / (2 * h));
for (int y = 0; y < ceil; y++) {
int i = y * 2 * h;
int m = i + h - 1;
int n = m + h;
n = n >= len ? n - 1 : n;// 数组最后一个元素的下标处理
merge(sr, tr, i, m, n);
}
// 保存一趟排序的结果到原始数组中
for (int z = 0; z < tr.length; z++) {
arr[z] = tr[z];
}
// 子序列长度修改
h *= 2;
}
return tr;
}
/**
* 求对数方法,此方法利用了数学中对数中的换底公式实现。
* 因为JDK没有提供求以2为底的对数的方法。
* @param value
* @param base
* @return
*/
double log(double value, double base) {
return Math.log(value) / Math.log(base);
}
@Test
public void fun1(){
int[] arr = new int[] { 49, 38, 65, 97, 76, 13, 27 };
int[] tr = mergSort(arr);
System.out.println(Arrays.toString(tr));
}
一趟归并排序的操作是,调用 ⌈ n 2 h ⌉ \lceil \frac{n}{2h} \rceil ⌈2hn⌉(n/2h后的小数结果向上取整,如2.01向上取整后为3)次算法merge将SR[1…n]中前后相邻且长度为 h h h的有序段进行两两归并,得到前后相邻、长度为 2 h 2h 2h的有序段,并存放在TR[1…n]中,整个归并排序需进行 ⌈ log 2 n ⌉ \lceil \log_2n \rceil ⌈log2n⌉趟。可见,实现归并排序需和待排记录等数量的辅助空间,因此它的空间复杂度为O(n),其时间复杂度为O(nlogn)。
与快速排序和堆排序相比,归并排序的最大特点是,它是一种稳定的排序方法。貌似Java JDK中Collections.sort(List);方法用的就是归并排序或经过改良的版本。
这2种语言的版本中都使用了计算对数和向上取整的数学库函数,分别由Math类和math.h头文件提供。
在计算对数的时候有一个使用对数的换底公式的实现方法,原数学公式如下:
对于
a
,
c
∈
(
0
,
1
)
∪
(
1
,
+
∞
)
a,c\in(0,1) \cup (1,+\infin)
a,c∈(0,1)∪(1,+∞)且
b
∈
(
0
,
+
∞
)
b\in(0,+\infin)
b∈(0,+∞),有
log
a
b
=
log
c
b
log
c
a
\log_ab=\frac{\log_cb}{\log_ca}
logab=logcalogcb
下面是C/C++的实现版本。
/*
* 归并排序
* 数据结构(C语言版) 严蔚敏 吴伟民
*
*/
#include<iostream>
#include<math.h>
using namespace std;
/**
* 求对数方法
* @param value
* @param base
* @return
*/
double logPlus(double value, double base) {
return log(value) / log(base);
}
void merge(int sr[], int tr[], int i, int m, int n) {
if (i == n) {// 数组最后一个元素的处理
tr[i] = sr[i];
return;
}
int k = 0;
int j = 0;
// System.out.println(i+","+m+","+n);
for (j = m + 1, k = i; i <= m && j <= n; ++k) {
if (sr[i] <= sr[j]) {
tr[k] = sr[i++];
} else {
tr[k] = sr[j++];
}
}
if (i <= m) {
// 将剩余的SR[i...m]复制到TR
for (int u = i, q = 0; u <= m; u++) {
tr[k + q] = sr[u];
++q;
}
}
if (j <= n) {
// 将剩余的SR[j...n]复制到TR
for (int u = j, q = 0; u <= n; u++) {
tr[k + q] = sr[u];
++q;
}
}
}
int* mergSort(int arr[], int arrLength) {
int* sr = arr;
int* tr = new int[arrLength];// 为数组指针分配内存,不然会报段错误核心已转储
int h = 1, len = arrLength;
// 需要进行几趟归并,以2为底,len的对数向上取整。
int count = (int) ceil(logPlus(len, 2));
for (int x = 0; x < count; x++) {
// 每一趟中,需要进行归并的分组数,len/2h得到的小数向上取整。
int _ceil = (int) ceil((float) len / (2 * h));
for (int y = 0; y < _ceil; y++) {
int i = y * 2 * h;
int m = i + h - 1;
int n = m + h;
n = n >= len ? n - 1 : n;// 数组最后一个元素的下标处理
merge(sr, tr, i, m, n);
}
// 保存一趟排序的结果到原始数组中
for (int z = 0; z < arrLength; z++) {
arr[z] = tr[z];
}
// 子序列长度修改
h *= 2;
}
return tr;
}
int main(int argc, char* argv[]) {
int arr[] = { 49, 38, 65, 97, 76, 13, 27 };
int* tr = mergSort(arr, 7);
for(int i=0;i<7;i++){
cout<<tr[i]<<" ";
}
cout<<endl;
return 0;
}
最后还有一个递归实现的版本
原伪代码如下:
void MSort(RcdType SR[], RcdType &TR1[], int s, int t){
// 将SR[s..t]归并排序为TR1[s..t]。
if(s == t) TR1[s]=SR[s];
else{
m = (s+t)/2;
MSort(SR, TR2,s,m);
MSort(SR, TR2,m+1,t);
Merge(TR2, TR1, s, m, t);
}
}// MSort
void MergeSort(SqList &L){
// 对顺序表L作归并排序
MSortt(L.r, L.r, 1, L.length);
}//MergeSort
以上这段伪代码始终未能转换成功,于是自己改写了一版。这段伪代码意思是将数组拆分直到剩下1个元素为止,然后不断的两两归并,不断向前回溯,直到最终数组有序。这个递归的过程就像二叉树的先序遍历,最后层层返回,形成一个排好序的数组,然而它这个TR1,TR2实在是不知所云。
改写如下:
void mergeV2(int[] sr, int[] tr, int i, int m, int n) {
int k = 0;
int j = 0;
// 将SR[i..m]和SR[m+1..n]归并为有序的TR[i..n]
for (j = m + 1, k = i; i <= m && j <= n; ++k) {
if (sr[i] <= sr[j]) // 将SR中记录由小到大地并入TR
tr[k] = sr[i++];
else
tr[k] = sr[j++];
}
if (i <= m) {
for (int u = i, q = 0; u <= m; u++) {// 将剩余的SR[i...m]复制到TR
tr[k + q] = sr[u];
++q;
}
}
if (j <= n) {
for (int u = j, q = 0; u <= n; u++) {// 将剩余的SR[j...n]复制到TR
tr[k + q] = sr[u];
++q;
}
}
}
//递归实现,完成。和伪代码差别略大
void MSortV2(int[] sr, int[] tr1, int s, int t) {
if (s == t) {
tr1[s] = sr[t];
} else {
int m = (s + t) / 2;
MSortV2(sr, tr1, s, m);
MSortV2(sr, tr1, m + 1, t);
mergeV2(sr, tr1, s, m, t);
for (int i = s; i <= t; i++) {
sr[i] = tr1[i];
}
}
}
@Test
public void fun3() {
int[] arr = new int[] { 49, 38, 65, 97, 76, 13, 27 };
int[] tr1 = new int[7];
MSortV2(arr, tr1, 0, arr.length - 1);
System.out.println(Arrays.toString(tr1));
}