简介
归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序的思想
归并排序分为分解和合并两个步骤。假设数组长度为n.
分解:将数组分割为两个数组,在分别将两个数组分别分割为两个数组,直到最后每个数组都是一个元素,这时将该单元素数组看为有序数组。
合并:将分割出的有序数组进行合并,合并为新的有序数组,如此重复,直到得到一个长度为n的有序数组。
核心操作是将数组中前后相邻的两个有序序列归并为一个有序序列。
归并排序的时间复杂度
O(N * logN)
归并排序的空间复杂度
O(N)
算法稳定性
稳定
归并排序的递归实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Print(int* a, int n)
{
for (int i = 0; i < n; i++) printf("%d ", a[i]);
printf("\n");
}
void _MergeSort(int* a, int* tmp, int begin, int end)
{
//分解:当数组只有一个元素时停止
if (begin >= end) return;
int mid = (begin + end) / 2;
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);
//合并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2]) tmp[i++] = a[begin1++];
else tmp[i++] = a[begin2++];
}
while (begin1 <= end1) tmp[i++] = a[begin1++];
while (begin2 <= end2) tmp[i++] = a[begin2++];
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, tmp, 0, n - 1);
}
int main()
{
int a[] = { 5,2,8,1,9,0,3,6,7,6 };
int n = sizeof(a) / sizeof(int);
Print(a, n);
MergeSort(a, n);
Print(a, n);
return 0;
}
先申请空间,再将归并排序的过程作为子函数调用,这样不用在每次递归过程中申请释放空间。上述代码中,_MergeSort为MergeSort的子函数。
归并排序的非递归实现
归并排序的非递归实现是一种比较复杂的算法,它不像快排那样借助栈来存储要处理的区间的范围,而是直接利用循环搞定,此方法要求我们对细节的把控要非常好,尤其是在处理数组边界这一块。
非递归实现归并排序的思想
归并排序,它利用了分治的思想,所谓分治,就是分而治之。不管是用递归来实现还是用非递归来实现,都不可能脱离算法本身的思想。在上述的递归实现过程中我们可以看到,首先是利用递归将数组分解成单个元素的数组(单个元素意味着有序),接着再一一归并,二二归并,四四归并……非递归则是省略了将数组分解为单元素数组的过程,直接引入一个gap代表每组的元素个数,如果令gap = 1,那么每个子数组不就是有序的单元素数组吗?相邻的两个单元素数组归并后,得到了一个有两个元素的有序数组,这时,在令gap = 2,让相邻的有两个元素的数组归并……这一过程和递归方法中的合并过程是一致的。
两个数组归并,并不要求这两个数组中的元素个数相同。上图中的数字27,在第一趟归并过程中,并没有参与归并,但是随着gap的增大,27也会参与到归并当中,这里,我想说明的是,不用担心归并过程中会漏掉最后一个元素,因为随着gap的增大,它一定会参与到归并中来。
相邻两个子数组的下标表示
因为在归并过程中,是针对相邻两个数组的,因此,我们得把控好它们的下标。用 i 表示原数组的下标,begin1、end1,begin2、end2分别表示第一个子数组和第二个子数组的首尾元素下标。begin1 = i,end1 = i + gap - 1,begin2 = i + gap, end2 = i + 2*gap - 1。
begin1 = i,end1 = i + gap - 1,begin2 = i + gap, end2 = i + 2*gap - 1(均为左闭右闭区间), i += 2*gap(跳过两个子数组),再去算begin1、end1,begin2、end2
以下是一趟归并排序的过程展开图。
数组边界处理
数组的边界控制是非递归方法中最为细节的一部分。当数组元素个数不是2的次方的时候就存在越界的问题。越界分为以下三种情况:
1、end1,begin2、end2全部越界
2、begin2、end2越界
3、end2越界
越界的三种情况如下图所示:
> 第一种情况, end1,begin2、end2全部越界,这时,第二个数组中没有任何元素,而且第一个数组中的元素是有序的,这种情况下不需要归并,也就是说,本趟归并不对第一个数组做任何处理,后面, 随着gap的增大,该组中的数据会参与到归并当中。
> 第二种情况,begin2、end2越界,同第一种情况,第一个数组中有元素,第二个数组中没有元素,不需要归并。
> 第三种情况,end2越界,第一个数组和第二个数组中都有元素, 因此需要归并,考虑到end2是越界的,所以需要对end2进行修正,将end2修改为 n - 1(n为数组总元素个数),即将end2修改为最后一个元素的下标,然后再归并。
非递归实现归并排序的代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Print(int* a, int n)
{
for (int i = 0; i < n; i++) printf("%d ", a[i]);
printf("\n");
}
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
int gap = 1;//gap为每个子数组的元素个数,只有一个元素就代表着有序
while (gap < n)
{
int j = 0;
for (int i = 0; i < n; i += 2 * gap)
{
//子数组的下标
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//第一、第二种情况,第二个数组中没有元素,不需要归并
if (end1 >= n || begin2 >= n) break;
//第三种情况,两个子数组中都有元素,需要修正end2,然后完成归并
if (end2 >= n) end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2]) tmp[j++] = a[begin1++];
else tmp[j++] = a[begin2++];
}
while (begin1 <= end1) tmp[j++] = a[begin1++];
while (begin2 <= end2) tmp[j++] = a[begin2++];
//归并一组就拷贝一组,不是整体(n)拷贝
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
}
int main()
{
int a[] = { 5,2,7,1,9,3,6,7,0 };
int n = sizeof(a) / sizeof(int);
Print(a, n);
MergeSortNonR(a, n);
Print(a, n);
return 0;
}