归并排序
归并排序是利用递归的排序。在递归返回的过程中进行合并排序。递归可以从字面理解,有递:数据在向下传递的过程中切分成越来越小的规模,直到可以直接得出结果;有归:数据在返回时逐渐汇总,得到最终结果。归并的归对应递归的归,就是在递归返回的过程中进行排序。
原理过程:
归并排序采用分治思想,将元素二分后递归到下一层,直到无法再次分割。在返回后将两段数据合并,合并时进行排序。
将数组左起第一个元素标记为L = 0,最后一个标记为R = size - 1,中间元素设定为MID = (L + R) / 2。分割时左侧数据从L到MID,右侧从MID + 1到R。
在归并排序中,递归前进段只是求数据段中点并分割然后传递给下一层,并不会进行排序处理。

递归的基准条件设计为当L == R时返回。
在递归的返回段,将左右两段数据进行合并同时排序。

与快速排序相对比,快排在递归的前进段就已经进行快排分割了,递归返回时数据已经有序;而归并排序在递归的返回段才进行数据合并和排序。
在归并排序的过程中,需要先将排序后的数据存储到新的数组上,然后再复制到数组对应段落。
代码解释:
使用之前的方式生成随机数数组,使用和之前快排相似的接口,根据上面的思路,可以得到归并排序的代码:
cpp
#include <iostream>
#include <random>
#include <ctime>
//归并排序合并
void Merge(int arr[],int l, int m, int r)
{
int* p = new int[r - l + 1];
int i = l;
int j = m + 1;
int index = 0;
while (i <= m && j <= r)
{
if (arr[i] <= arr[j])
{
p[index++] = arr[i++]; //执行完赋值语句后对index和i分别++
}
else
{
p[index++] = arr[j++];
}
}
//两段数据中剩余的数据后缀到p
while (j <= r)
{
p[index++] = arr[j++];
}
while (i <= m)
{
p[index++] = arr[i++];
}
//把合并后的大段数据复制到arr数组的[l, r]区间
for (i = l, j = 0; i <= r; i++, j++)
{
arr[i] = p[j];
}
delete[] p;
}
//归并排序递归接口
void MergeSort(int arr[], int begin, int end)
{
if (begin == end)
return;
int mid = (begin + end) / 2;
//递归前进
MergeSort(arr, begin, mid);
MergeSort(arr, mid + 1, end);
//递归返回后合并排序
Merge(arr, begin, mid, end);
}
//归并排序接口
void MergeSort(int arr[], int size)
{
MergeSort(arr, 0, size - 1);
}
int main()
{
int arr[10];
srand(time(NULL));
for (int i = 0; i < 10; i++)
{
arr[i] = rand() % 100;
std::cout << arr[i] << " ";
}
MergeSort(arr, 10);
//换行输出排序后数组
std::cout << std::endl << "Array Sorted: " << std::endl;
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
}
}
下面是对归并排序代码的分析:
归并排序的接口类型可以根据实际调用情况修改。比如只给了数组大小的话,就要再多写一个接口接入递归。因为对数组的递归往往要给出数据段开始和结束的位置。
在递归前进过程中只是求出数据段中点,并将数据段二分后分别递归到下一层。
在递归返回过程中,得到返回值后才会进行合并排序。这里归并排序处理数组指针,直接在数组对应数据段上进行了排序,不需要处理返回值。
将合并排序的过程抽象到一个函数Merge中。
cpp
//归并排序递归接口
void MergeSort(int arr[], int begin, int end)
{
if (begin == end)
return;
int mid = (begin + end) / 2;
//递归前进
MergeSort(arr, begin, mid);
MergeSort(arr, mid + 1, end);
//递归返回后合并排序
Merge(arr, begin, mid, end);
}
//归并排序接口
void MergeSort(int arr[], int size)
{
MergeSort(arr, 0, size - 1);
}
在合并排序过程中,需要一个新的数组来保存排序好的序列。这里在栈空间开辟与处理的数据段等长的数组。设计两个整型i、j分别记录两段数据的开始点。
在两段数据没有遍历完之前,用一个while来比较两个下标处数据的大小,较小的赋值到新数组中保存。由于两段数据分别有序,只需要按顺序比较就可以在合并两段数据的同时得到有序数组。
把剩余的数据添加到新数组的末尾,就得到了合并排序后的数组。
之后把数组赋值给原数组对应位置。
不要忘记删除指针指向的数组内存,释放栈空间。
cpp
//归并排序合并
void Merge(int arr[],int l, int m, int r)
{
int* p = new int[r - l + 1];
int i = l;
int j = m + 1;
int index = 0;
while (i <= m && j <= r)
{
if (arr[i] <= arr[j])
{
p[index++] = arr[i++]; //执行完赋值语句后对index和i分别++
}
else
{
p[index++] = arr[j++];
}
}
//两段数据中剩余的数据后缀到p
while (j <= r)
{
p[index++] = arr[j++];
}
while (i <= m)
{
p[index++] = arr[i++];
}
//把合并后的大段数据复制到arr数组的[l, r]区间
for (i = l, j = 0; i <= r; i++, j++)
{
arr[i] = p[j];
}
delete[] p;
}
性能分析:
时间复杂度:
二路归并不存在最好最坏时间复杂度之分,因为在分割数据时严格从中间二分。
空间复杂度:
开辟新的数组所消耗的空间复杂度为O(n),递归消耗的空间复杂度为O(log n)可以忽略。
**稳定性:**稳定
由于归并排序在返回时数据严格按照顺序合并,不会扰乱原本的顺序。
如 2① 5 2② 8 1 在归并排序时
前进段:
2① 5 2② | 8 1
2① 5 | 2② | 8 | 1
2① | 5 | 2② | 8 | 1
返回段:
2① 5 | 2② | 8 | 1
2① 2② 5 | 1 8
1 2① 2② 5 8