背景
归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并(来自百度百科doge)。
算法思想
归并排序的思想和我国古代的分封制很像。古代的“天子”精力有限,对于国家大事无法做到亲历亲为,因此便将土地分给王室子弟、功臣或者古代帝王的后裔,让这些诸侯协助自己管辖一方。这些诸侯需要服从天子的命令并定期朝觐述职,在天子和诸侯的共同努力下运转国家这个巨大机器。归并排序也使用了这种分治思想,简单来说,就是把大问题划分成多个小问题然后递归求解。下面用一幅图展示(图片来源:https://www.cnblogs.com/chengxiao/p/6194356.html)
下面用伪代码的形式展示归并排序的思想:
MergeSort(A, start, end)
if end > start
mid = satrt + (end - start) / 2
MergeSort(A, start, mid)
MergeSort(A, mid+1, end)
Merge(A, start, mid, end)
下面展示归并排序的源代码:
#include <stdio.h>
#include <stdlib.h>
/** Merge(int *arr, int start, int mid, int end) -> none
* 用于合并划分出来的两个数组
* @param
* @param arr: 需要进行合并操作的数组
* @param start: 合并的起点
* @param mid: 合并的中点
* @param end: 合并的结尾
* @return: none
*/
void Merge(int *arr, int start, int mid, int end) {
int *tmp = (int *)malloc(sizeof(int) * (end - start + 1));
int i = start;
int j = mid+1;
int k = 0;
/* 逐个比较中点左右两个数组元素的大小 */
while (i <= mid && j <= end) {
if (arr[i] < arr[j])
tmp[k++] = arr[i++];
else
tmp[k++] = arr[j++];
}
/* 左边有剩余 */
while (i <= mid) {
tmp[k++] = arr[i++];
}
/* 右边有剩余 */
while (j <= end) {
tmp[k++] = arr[j++];
}
for (i = 0; i < k; i++) {
arr[i+start] = tmp[i];
}
free(tmp);
}
/** MergeSort(int *arr, int start, int end) -> none
* 归并排序函数入口
* @param
* @param arr: 需要排序的数组
* @param start: 排序的起点
* @param end: 排序的中点
* @return: none
*/
void MergeSort(int *arr, int start, int end) {
if (start >= end)
return ;
int mid = start + (end - start) / 2;
MergeSort(arr, start, mid);
MergeSort(arr, mid+1, end);
Merge(arr, start, mid, end);
}
int main() {
int arr[] = {5, 4, 3, 2, 1, 20, 19, 18, 14, 1, 12};
int length = sizeof(arr) / sizeof(arr[0]);
printf("Original array: ");
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
MergeSort(arr, 0, length - 1);
printf("After sort the array: ");
for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
算法分析
时间复杂度
根据伪代码,我们可以得到一个递推表达式
T
(
n
)
=
2
T
(
n
2
)
+
c
n
T(n) = 2T(\frac{n}{2}) + cn
T(n)=2T(2n)+cn,这个很好理解,
M
e
r
g
e
S
o
r
t
(
A
,
s
t
a
r
t
,
e
n
d
)
MergeSort(A, start, end)
MergeSort(A,start,end)内部调用了
M
e
r
g
e
S
o
r
t
(
A
,
s
t
a
r
t
,
m
i
d
)
MergeSort(A, start, mid)
MergeSort(A,start,mid)和
M
e
r
g
e
S
o
r
t
(
A
,
m
i
d
+
1
,
e
n
d
)
MergeSort(A, mid+1, end)
MergeSort(A,mid+1,end),这两个就相当于原问题一半规模的子问题,此外还有一个合并数组的操作,我们将这个记为
c
n
cn
cn。那么如何计算这个递推表达式的具体值呢?这里我们使用递归树的形式展开,树的两个孩子的值加上根节点的值即为表达式,下图为递推表达式的原始形式。
进一步将其展开,直到叶子节点均为常数项。
一共有
l
o
g
n
log n
logn层,每一层的和都是
c
n
cn
cn,那么整个加起来就是
n
l
o
g
n
nlogn
nlogn,所以归并排序的时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度
在合并的过程中我们需要一个大小为
n
n
n的临时空间来保存合并之后的序列,因此空间复杂度为
O
(
n
)
O(n)
O(n)
算法稳定性
可以看到在判断的时候我们并未考虑元素相等的情况,因此也就不会交换数值相同元素的位置,所以归并排序是稳定的算法