前面提到三种简单排序和三种高级排序之间的关系:
冒泡排序 (初级) ----> 快速排序 (高级)
选择排序 (初级) ----> 堆排序 (高级)
插入排序 (初级) ----> 希尔排序 (高级)
但是归并排序不在其中,它是一种很另类的排序方法。它的另类表现在:
1、它没有初级版本,只有高级形式。
2、另外三种高级排序都是不稳定的排序方式,但归并排序是稳定的。
3、归并排序跟堆排序一样,时间复杂度永远都是O(N * logN),效率非常稳定。但是堆排序有准备阶段,而归并排序没有,所以归并排序比堆排序快,它的速度仅次于快速排序。
4、归并排序比较占内存,空间复杂度为O(N),比其它所有排序算法都要高。正所谓想要马儿长得好,一定要给马儿吃得饱。当设备有足够多的内存,且要求排序结果稳定的情况下,归并排序是一种不错的选择。
归并排序的思路:把两个有一个数的数组拼接成一个有两个元素的有序数组;然后把两个有两个元素的有序数组拼接成一个有四个元素的有序数组;再把两个有四个元素的有序数组拼接成一个有八个元素的有序数组。。。。。。最终得到一个完整的数组。
没有插图。。。。
归并排序有递归和迭代等两种实现方式。这里只实现迭代方式。除了专门介绍指针的章节,在本书大部分内容都避免使用指针,但是归并排序的代码却无法避免使用指针。原因是在申请数组时,C语言不支持用变量作为数组的大小。如果想动态申请数组,只能手动向操作系统申请空间。但这个问题在高级语言 (包括C++、Java、C#、Python、PHP等) 中不是问题,因为这些高级语言中都可以使用变量作为数组的大小。
归并排序的代码如下:
#include <stdio.h>
#include <stdlib.h>
int arr[] = {4, 1, 5, 7, 3, 2, 9, 8, 6, 0, 5, 4, 3, 6, 7, 8};
int size = 0;
//归并:将arr数组中from ~ middle 和 middle ~ to的两个子数组进行有序合并
void combine(int from, int middle, int to)
{
int i, j, k, size1;
int * p; //指针,指向整个数组
int * temp; //指针,指向临时空间
i = from;
j = middle;
k = 0;
size1 = to - from + 1;
p = arr;
//向操作系统申请临时空间
temp = (int *)(malloc(size1 * sizeof(int)));
//有序合并:将数组中的内容复制到临时数组中
while (i < middle && j <= to)
{
//根据大小排序
if (p[i] < p[j])
{
temp[k] = p[i];
k++;
i++;
}
else //if(p[i] > p[j])
{
temp[k] = p[j];
k++;
j++;
}
}
//前一个子数组的内容没有完全复制到临时数组中,则继续复制
while (i < middle)
{
temp[k++] = p[i];
i++;
}
//后一个子数组的内容没有完全复制到临时数组中,则继续复制
while (j <= to)
{
temp[k++] = p[j];
j++;
}
//将临时数组中的内容重新交还给arr
for (k = 0, i = from; k < size1; k++)
{
p[i] = temp[k];
i++;
}
free(temp); //释放指针,回收内存
}
//迭代的归并排序
void combineSortFor()
{
int step; //step是步长,取值范围是1, 2, 4, 8。。。
//两个子数组,第一个子数组的范围是left1和right1
//第二个子数组的范围是left2和right2
int left1, right1, left2, right2;
int size = sizeof(arr) / sizeof(int);
for (step = 1; step < size; step *= 2) //这个循环产生步长,一共循环logN次
{
//这个循环是为了进行单层的归并排序
for (left1 = 0; left1 + step < size; left1 = right2)
{
//确定范围
right1 = left1 + step;
left2 = right1;
right2 = left2 + step;
//确定范围:右侧不能超出长度
if (right2 > size)
{
right2 = size;
}
combine(left1, right1, right2 - 1);
}
}
return;
}
int main()
{
size = sizeof(arr) / sizeof(int);
combineSortFor();
return 0;
}
归并排序也有优化方案:
1、跟快速排序一样,当数据规模较小时,可以用插入排序。由于插入排序是稳定的排序方法,所以这种优化方式不影响归并排序的稳定性。
2、上述代码是将排序好的结果从临时数组复制到原来的数组中。其实可以不用复制。在下一轮排序中直接从临时数组排序到原来的数组即可。如果最后排序完成的数据是在临时数组中,再把这些结果复制到原来的数组中。这样可以减少logN次复制,可以大大提高排序效率。上述两种优化方案就请读者自己完成吧。
上述7种排序算法的时间复杂度都在O(N * logN) ~ O(N * N) 之间。那么是不是还能找到更快的排序算法呢?已经证明:基于两个数的比较的排序算法的时间复杂度最低是O(N * logN)。也就是说,即使有更快的算法出现,新算法也不可能比快速排序等有质的飞跃。