1.定义
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
2.定义理解
由定义我们知道,如果我们要想利用归并的方法来排序,那么就要求被归并的两个序列本身是有序的。于是,当我们要对一个数组进行归并排序时,我们可以把这个数组分成两个序列,然后我们要想办法使这两个序列有序,于是我们可以把这两个序列再看分别成一个需要排序的数组,然后我们把这每个序列再分成左右两个序列,但是如果我们要用归并排序,那我们还要保证这次分割的两个序列有序,于是我们继续用这个思想进行分割,知道一个需要归并排序的序列,被分成左右两个序列只有一个元素。
如上图此时待归并序列已经被我们逐渐分割成了左右序列只有一个元素,现在我们就可以从最底层的左右序列开始归并,因为最底层的左右序列只有一个元素,一个元素本身有序。
如图所示10,6被归并成了6,10;7,1被归并成了1,7。此时序列“6,10”和序列“1,7”变得有序,我们可以继续归并。
此时待归并序列的左序列变得有序,然后我们对右序列进行和左序列同样的操作。
此时待排序数组的左右序列都有序了,然后我们让左右序列进行归并,得到“1,2,3,4,6,7,9,10”,此时归并排序整个过程结束。
3.(递归)归并排序代码
上面的过程非常像二叉树的后序遍历,然后我们这个思想来实现代码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void _MergeSort(int arr[], int begin, int end, int* tmp)
{
if (begin >= end)
{
return;
}
//将arr数组分解为左右两个序列
int midi = (begin + end) / 2;
_MergeSort(arr, begin, midi, tmp);
_MergeSort(arr, midi + 1, end, tmp);
//一直递归,当代码运行到这时,说明此时左右序列只有一个元素了
//归并[begin,midi][midi+1,end]
int left1 = begin, right1 = midi;
int left2 = midi + 1, right2 = end;
int j = begin;
while (begin <= right1 && left2 <= right2)
{
if (arr[left1] < arr[left2])
{
tmp[j++] = arr[left1++];
}
else
{
tmp[j++] = arr[left2++];
}
}
while (left1 <= right1)
{
tmp[j++] = arr[left1++];
}
while (left2 <= right2)
{
tmp[j++] = arr[left2++];
}
//此时归并后的结果在tmp数组中,拷贝回arr
memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int arr[], int begin, int end)
{
int* tmp = (int*)malloc(sizeof(int) * (end + 1));
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(1);
}
_MergeSort(arr, begin, end, tmp);
}
int main()
{
int arr[] = { 10, 6, 7, 1, 3, 9, 4, 2 };
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
MergeSort(arr, 0, n - 1);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
在这个代码中我们额外创建了一个数组tmp,tmp这个数组用于存放每次左右序列归并后的有序列,然后再把这个序列拷贝到原数组中对应的位置。
在这里我们还要注意每次递推传的begin和end,不要漏数据;递推的终止条件时当左序列或右序列只有一个元素时就不递推了,及不分割了。
4.非递归理解
如果我们想把归并排序的递归,改为非递归,我们主要要解决的是:要怎么实现把待排序数组分为左右两个有序的序列。
我们可以将待归并数组先按照一个一个归并,然后整个数组的每个元素被一个一个归并完后,我们再对数组的元素进行两个两个归并,以此类推。
我们要注意,当我们每对一对元素进行归并后,tmp里面存放的是归并好的序列,我们每归并好一对后就要将归并好的拷贝到arr对应的位置中,为我们进行两个两个归并做准备。即
对待归并数组的所有元素一个一个归并后,我们再进行两个两个归并,以此类推,直到对待归并序列进行(1/2)个(1/2)个归并
5.(非递归)归并排序代码
由前面的的铺垫,非递归代码如下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void MergeSortNonR(int arr[], int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(1);
}
int gap = 1;//gap为1表示一个一个排序
while (gap <= (n / 2) + 1)
{
for (int i = 0; i < n; i += gap)
{
int left1 = i, right1 = i + gap - 1;//左序列[i,i + gap - 1]
int left2 = i + gap, right2 = i + 2 * gap - 1;//右序列[i,i + gap + gap - 1]
//归并区间边界处理
if (right1 > n - 1)
{
right1 = n - 1;
left2 = n;
right2 = n - 1;
}
else if (left2 > n - 1)
{
left2 = n;
right2 = n - 1;
}
else if (right2 > n - 1)
{
right2 = n - 1;
}
int j = i;
//归并
while (left1 <= right1 && left2 <= right2)
{
if (arr[left1] < arr[left2])
{
tmp[j++] = arr[left1++];
}
else
{
tmp[j++] = arr[left2++];
}
}
while (left1 <= right1)
{
tmp[j++] = arr[left1++];
}
while (left2 <= right2)
{
tmp[j++] = arr[left2++];
}
memcpy(arr + i, tmp + i, sizeof(int) * (right2 - i + 1));
}
gap *= 2;
}
}
int main()
{
int arr[] = { 10, 6, 7, 1, 3, 9, 4, 2};
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
MergeSortNonR(arr, n);
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
6.非递归时归并区间边界问题
当我们用
int left1 = i, right1 = i + gap - 1;//左序列[i,i + gap - 1]
int left2 = i + gap, right2 = i + 2 * gap - 1;//右序列[i,i + gap + gap - 1]
确定每次归并的区间[left1,right1] 和[left2,right2]时,我们要考虑区间是否越界
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void MergeSortNonR(int arr[], int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!\n");
exit(1);
}
int gap = 1;//gap为1表示一个一个排序
while (gap <= (n / 2) + 1)
{
for (int i = 0; i < n; i += gap)
{
int left1 = i, right1 = i + gap - 1;//左序列[i,i + gap - 1]
int left2 = i + gap, right2 = i + 2 * gap - 1;//右序列[i,i + gap + gap - 1]
//越界处理
printf("[%d,%d][%d,%d] ", left1, right1, left2, right2);
int j = i;
//归并
while (left1 <= right1 && left2 <= right2)
{
if (arr[left1] < arr[left2])
{
tmp[j++] = arr[left1++];
}
else
{
tmp[j++] = arr[left2++];
}
}
while (left1 <= right1)
{
tmp[j++] = arr[left1++];
}
while (left2 <= right2)
{
tmp[j++] = arr[left2++];
}
memcpy(arr + i, tmp + i, sizeof(int) * (right2 - i + 1));
}
printf("\n");
gap *= 2;
}
}
int main()
{
int arr[] = { 10, 6, 7, 1, 3, 9, 4, 2 };
int n = sizeof(arr) / sizeof(arr[0]);
/*for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");*/
MergeSortNonR(arr, n);
/*for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");*/
return 0;
}
上面代码运行结果:
从这张图可以看出,部分归并区间存在越界,于是我们要对越界区间进行特殊处理。
观察发现一共有三种越界情况:
- right1越界:当right1>n-1时,说明此时[left1,right1]和[left2,right2]不用归并了,因为[left2,right2]不存在,此时,我们可以把right1手动改为n-1,将lelt2改为大于right2,right2改为n-1,这样就不会对[left2,right2]进行归并,并且保证了最后memcpy拷贝数据个数。
- left2越界:当left2>n-1时,我们不用对[left2,right2]归并,将lelt2改为大于right2,right2改为n-1,即可。
- 当right2越界,我们只需把right2改为n-1即可。
7.时间和空间复杂度
1.时间复杂度:nlogn
我们以非递归为例,归并排序分为折半,和合并部分,而折半过程中,我们用midi进行分割,这个过程基本不占时间,因为它就是把待排序数组分割通过midi分割了一下,主要的时间复杂度要看合并阶段,合并阶段我们可以看每一层,我们可以发现每一层归并完的时间复杂度是n即数组元素个数。第一层n个元素,归并的时间复杂度相当与遍历一遍的时间复杂度,所以第一层时间复杂度为n
然后我们直到一共有logn层,所以时间复杂度为nlogn
2.空间复杂度:n
无论是递归还是非递归的归并排序,我们都创建了一共tmp零时数组,它的大小为n,所额外的空间为n,即空间复杂度为n
8.稳定性:稳定
在归并排序中,如果相同的元素被分割到同一区间,相同的元素在归并时的相对位置不会改变,即使没有分割到同一区间,在后来的归并过程中,它们的相对位置还是不会改变。所以归并排序是一种稳定的排序。