一、归并排序的定义
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
二、归并排序的算法原理
归并排序的算法可以用递归法和非递归法来实现,在理解的角度来看,归并排序就是一种递归排序。其将一个数组分成均匀的两份小的数组,然后将其分成的两份各自再分,得到四份小的数组,如此重复,直到所分成的小数组没有元素或者只有一个元素为止,这就是分而治之的分。当们把数组分好后,再依次进行归并(将两个有序的小数组归并成一个有序数组),只有一个元素的小数组视为有序,第一次归并后,每个有序数组的元素个数就从一变成了二,再次归并有序数组,每个有序数组的个数就从二变成了四,如此重复,直到有序的数组个数等于原数组的个数,此时整个数组完全有序,完成了排序的工作。
下面是归并排序分而治之的演示图:
上面这个图长得非常像一个二叉树,治其实就是回溯这个二叉树进行归并的过程。而归并两个有序数组的方法非常简单,这里就不进行赘述。下面我来使用递归的方法和非递归的方法来实现递归排序。
归并排序的动态示意图:
三、归并排序的递归法实现
递归的方法实现归并排序很简单,就是不断递归左子树和右子树进行归并排序,当左右子树都有序后,就对左子树和右子树进行归并排序即可。为了简单起见,排序整数数组。
具体代码如下:
//归并排序递归法
void _MergeSort(int* arr,int* pTempArr,int leftIndex,int rightIndex)
{
//如果区间内没有元素,停止递归
//如果区间内只有一个元素,则视为有序,停止递归
if (leftIndex >= rightIndex)
return;
//将当前区间均分为两个区间
//[leftIndex,midIndex]和[midIndex + 1,rightIndex]
int midIndex = (leftIndex + rightIndex) / 2;
//递归左右子树使左右子树有序,当左右子树有序时进行递归排序
_MergeSort(arr, pTempArr, leftIndex, midIndex);//递归左子树
_MergeSort(arr, pTempArr, midIndex + 1, rightIndex);//递归右子树
//到了这里,说明左右子树有序了
//归并两个有序数组[leftIndex,midIndex]和[midIndex + 1,rightIndex]
int key = leftIndex;
int index1 = leftIndex;
int index2 = midIndex + 1;
while(index1 <= midIndex && index2 <= rightIndex)
{
if (arr[index1] <= arr[index2])
pTempArr[key++] = arr[index1++];
else
pTempArr[key++] = arr[index2++];
}
//归并剩余的元素
while(index1 <= midIndex)
{
pTempArr[key++] = arr[index1++];
}
while (index2 <= rightIndex)
{
pTempArr[key++] = arr[index2++];
}
//将归并好的有序数据转移到待排序的数组中
memcpy(arr + leftIndex, pTempArr + leftIndex, sizeof(int) * (rightIndex - leftIndex + 1));
}
//归并排序递归法实现
void MergeSort(int* arr,int nums)//传入数组和数组的大小
{
//为归并两个有序数组临时开辟所需要的空间
int* pTempArr = (int*)malloc(sizeof(int) * nums);
//对数组进行归并排序
_MergeSort(arr, pTempArr, 0, nums - 1);
//释放临时数组的空间
free(pTempArr);
}
四、归并排序的非递归方法实现
归并排序推荐使用非递归的方法来实现的,因为递归会出现栈溢出的问题,而非递归的方法就不用在意这个问题,迭代不需要像递归那样开辟大量栈空间。但是非递归的方法实现起来有一点的困难。
在上面的分而治之的图中:
迭代的方式不需要分而治之,实现归并排序可以跳过分的过程,直接治,我们从图中可以看到,第一层有序数组进行归并排序时,每个有序数组的元素个数为1;当第二层有序数组进行归并排序时,每个有序数组的元素个数为2;当第三层进行归并排序时,每个有序数组的元素个数为4,我们发现,每次归并排序完成后,其每个将要归并的有序数组的元素个数是上一层的两倍,于是我们可以使用迭代的方式进行递归。
下面我将画图来演示这个过程:
归并排序的非递归法需要注意的是数组最后的几组有序数组元素的归并,视情况进行特殊处理。
其情况有以下几种:
1、倒数第二组有序数组和倒数第一组有序数组匹配进行归并。
此时又分为两种情况:
倒数第一组有序数组的个数和倒数第二组的有序数组的个数相同,归并的数组大小是对称的。
倒数第一组有序数组的个数比倒数第二组有序数组的个数少,归并的数组大小不是对称的。
2、倒数第二组有序数组和倒数第三组有序数组匹配进行归并,而倒数第一组有序数组没有可以匹配归并的有序数组,此时倒数第一组有序数组不需要进行归并操作。
具体代码如下:
//归并两个有序数组到新数组中
void MergeArray(int* pTempArr, int* arr,int leftIndex, int midIndex, int rightIndex)
{
//归并有序数组[leftIndex,midIndex]和[midIndex + 1,rightIndex]
int index1 = leftIndex;
int index2 = midIndex + 1;
int key = leftIndex;
while (index1 <= midIndex && index2 <= rightIndex)
{
if (arr[index1] <= arr[index2])
pTempArr[key++] = arr[index1++];
else
pTempArr[key++] = arr[index2++];
}
while (index1 <= midIndex)
{
pTempArr[key++] = arr[index1++];
}
while (index2 <= rightIndex)
{
pTempArr[key++] = arr[index2++];
}
}
//归并排序非递归
void MergeSortNonR(int* arr,int nums)
{
//为归并有序数组临时开辟所需要的空间
int* pTempArr = (int*)malloc(sizeof(int) * nums);
int gap = 1; //每组有序数组的元素个数
while (gap < nums)
{
int leftIndex = 0; //归并的有序数组的起始位置(下标的左边界)
// 每一层归并后的有序数组下标的分组
// 0 1 2 3 4 5 6 7 8 9 10
// [0 1] [2 3] [4 5] [6 7] [8 9] 10
// [0 1 2 3] [4 5 6 7] [8 9 10]
// [0 1 2 3 4 5 6 7] [8 9 10]
// [0 1 2 3 4 5 6 7 8 9 10]
// nums - leftIndex 得到leftIndex以及往后的元素的个数
// nums - leftIndex >= 2 * gap 可以保证归并到的有序数组都是对称的
while (nums - leftIndex >= 2 * gap)
{
int rightIndex = leftIndex + 2 * gap - 1;
int midIndex = (leftIndex + rightIndex) / 2;
//归并有序数组[leftIndex,midIndex]和[midIndex + 1,rightIndex]
MergeArray(pTempArr, arr, leftIndex, midIndex, rightIndex);
//将归并好的元素转移到待排序的数组中
memcpy(arr + leftIndex, pTempArr + leftIndex, sizeof(int) * (rightIndex - leftIndex + 1));
//归并下两组有序数组
leftIndex += 2 * gap;
}
//处理不对称的归并和没有归并的有序数组可以匹配的情况
//如果满足 nums - leftIndex > gap
//说明leftIndex以及后面的元素个数大于gap个
//需要进行归并排序,只不过归并排序的区间并不对称
if (nums - leftIndex > gap)
{
//这里分成的两个区间由于不对称,所以不能使用左右下标相除的方法算出中间下标
//归并排序[leftIndex,leftIndex + gap - 1] [leftIndex + gap,nums - 1]
MergeArray(pTempArr, arr, leftIndex, leftIndex + gap - 1, nums - 1);
memcpy(arr + leftIndex, pTempArr + leftIndex, sizeof(int) * (nums - leftIndex));
}
//如果剩余元素不足gap个,不需要进行归并排序
//进行下一层的归并排序
gap *= 2;
}
//释放临时数组的空间
free(pTempArr);
}
五、总结
归并排序的时间复杂度为O(nlogn),空间复杂度为O(N),因为归并有序数组需要额外开辟空间,所以其排序的性能仅次于快排,但是归并排序稳定。