归并排序
前面讲了插入排序、选择排序和冒泡排序,下面说一种不太容易想到的排序方法:归并排序。
原理
归并排序,是用分治法的一个典型的例子。分治法就是把一个大问题分成几个小问题,分而治之。那归并排序的原理就是把一个要排序的数组分成两个子数组,分别排序那两个子数组,然后归并成一个数组。而那两个子数组排序的时候同样可以用归并排序再分成两个子数组,分别排序后再合并。以此类推,可以一直分到分无可分,即数组中只有一个元素为止。
由上面的分析可见,切分子数组是很容易的,不需要什么技巧,一般来说,对半分就可以了。而归并排序的技巧在于归并的过程。
归并的过程,需要每次找出两个子数组中最小的一个元素,将其移动到结果数组中。因为两个子数组已经是排好序的,所以只需要比较两个子数组中最小的元素就可以了。一旦一个子数组中的元素都已移动到结果数组中,就不需要继续比较了,直接移动另一个子数组到结果数组中就行了。
实现
按照这个原理我们来用代码实现。同样的问题又来了:是否可以像插入排序、选择排序和冒泡排序一样,也不需要另外开辟空间,直接在原址上排序呢?这次的答案是不行。因为在归并的过程中我们需要额外的空间来存储结果数组。
下面就是用C语言实现的代码。分成三个函数来实现。
- 要排序的数组a有n个元素。
- merge_sort 函数进行封装,调用 merge_sort_。
- merge_sort_ 进行子数组的切分,将数组a[p...r]切分成a[p...q]和a[q+1...r]两个子数组,然后调用 merge 进行归并。
- merge 中先把要归并的两个已排好序的数组复制到 L 和 R 中,然后每次找出 L 和 R 中最小的一个元素放入 a 中。
void merge_sort(int a[], int n)
{
if(n<=0)
return;
merge_sort_(a, 0, n-1);
}
void merge_sort_(int a[], int p, int r)
{
if(p<r) {
int q=(p+r)/2;
merge_sort_(a, p, q);
merge_sort_(a, q+1, r);
merge(a, p, q, r);
}
}
void merge(int a[], int p, int q, int r)
{
int n1=q-p+1;
int n2=r-q;
int *L=malloc(n1*sizeof(int));
int *R=malloc(n2*sizeof(int));
for(int i=0;i<n1;i++) {
L[i]=a[p+i];
}
for(int i=0;i<n2;i++) {
R[i]=a[q+1+i];
}
int i=0; // index of L
int j=0; // index of R
int k=p; // index of a
while(i<n1 && j<n2) {
if(L[i]<=R[j]) {
a[k]=L[i];
i++;
} else {
a[k]=R[j];
j++;
}
k++;
}
// copy the remaining to a
if(i==n1) {
for(int jj=0;jj<n2-j;jj++) {
a[k+jj]=R[j+jj];
}
} else {
for(int ii=0;ii<n1-i;ii++) {
a[k+ii]=L[i+ii];
}
}
}
为了验证此函数的效果,加上了如下辅助代码,对3个数组进行排序,运行结果在最后,可见排序成功。
#include <stdio.h>
#include <stdlib.h>
#define SIZE_ARRAY_1 5
#define SIZE_ARRAY_2 6
#define SIZE_ARRAY_3 20
void merge_sort(int a[], int n);
void show_array(int a[], int n);
void main()
{
int array1[SIZE_ARRAY_1]={1,4,2,-9,0};
int array2[SIZE_ARRAY_2]={10,5,2,1,9,2};
int array3[SIZE_ARRAY_3];
for(int i=0; i<SIZE_ARRAY_3; i++) {
array3[i] = (int)((40.0*rand())/(RAND_MAX+1.0)-20);
}
printf("Before sort, ");
show_array(array1, SIZE_ARRAY_1);
merge_sort(array1, SIZE_ARRAY_1);
printf("After sort, ");
show_array(array1, SIZE_ARRAY_1);
printf("Before sort, ");
show_array(array2, SIZE_ARRAY_2);
merge_sort(array2, SIZE_ARRAY_2);
printf("After sort, ");
show_array(array2, SIZE_ARRAY_2);
printf("Before sort, ");
show_array(array3, SIZE_ARRAY_3);
merge_sort(array3, SIZE_ARRAY_3);
printf("After sort, ");
show_array(array3, SIZE_ARRAY_3);
}
void show_array(int a[], int n)
{
if(n>0)
printf("This array has %d items: ", n);
else
printf("Error: array size should bigger than zero.\n");
for(int i=0; i<n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
运行结果:
Before sort, This array has 5 items: 1 4 2 -9 0
After sort, This array has 5 items: -9 0 1 2 4
Before sort, This array has 6 items: 10 5 2 1 9 2
After sort, This array has 6 items: 1 2 2 5 9 10
Before sort, This array has 20 items: 13 -4 11 11 16 -12 -6 10 -8 2 0 5 -5 0 18 16 5 8 -14 4
After sort, This array has 20 items: -14 -12 -8 -6 -5 -4 0 0 2 4 5 5 8 10 11 11 13 16 16 18
分析
时间复杂度
从代码可见,merge的过程是一层循环,为 n 的量级。而merge的次数是 log n,所以归并排序的时间复杂度为 O(n log n)。
空间复杂度
因为归并排序需要额外的空间来存储结果数组,结果数组为 n 的量级,所以空间复杂度是 O(n)。