排序算法 归并排序 MergeSort -- C语言实现

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

用途

速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列.

也常用于外排序实现.

归并排序在许多场景中都有应用,特别是在需要稳定排序的情况下:

  • 大数据处理:当数据量非常大,以至于无法完全放入内存时,归并排序可以通过外部存储(如硬盘)进行分段排序和合并。

特性

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

  2. 时间复杂度:O(N*logN)

  3. 空间复杂度:O(N)

  4. 稳定性:稳定

  5. 非原地排序:归并排序需要额外的空间来存放中间结果,空间复杂度为O(n)。

优点

  1. 稳定:归并排序保持了原始数据的稳定性。
  2. 高效:对于大数据集,归并排序的时间复杂度始终为O(n log n),这使得它在处理大规模数据时效率很高。
  3. 易于理解与实现:归并排序的基本逻辑相对简单,容易理解和实现。
  4. 并行化:由于归并排序可以自然地被分解成多个独立的任务,因此非常适合并行处理环境。

与其他排序算法比较的优点

  • 相较于快速排序,归并排序的最坏情况性能更好(快速排序的最坏情况为O(n^2)),而且归并排序更稳定。
  • 与堆排序相比,归并排序具有更好的稳定性,并且不需要对数据结构进行额外维护。
  • 对于小数据集或者已经部分排序的数据,虽然插入排序或冒泡排序可能更快,但随着数据规模的增长,归并排序的性能优势更加明显。

缺点

  1. 空间复杂度较高:归并排序需要额外的空间来存储临时数组,空间复杂度为O(n)。
  2. 递归调用开销:递归实现归并排序可能会带来较大的函数调用开销,尤其是在深度很大的情况下。
  3. 效率问题:对于较小的数据集,归并排序可能不如一些简单的排序算法(如插入排序)高效。

算法步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

步骤 - 二叉树

image-20240816152129280

(从上到下完成归并)

实际动图演示

归并排序

递归实现

void _MergeSort(int* a, int begin, int end, int* tmp) {
if(begin>=end) return ;
int mid = (begin+end)/2;
//先划分(或先递归让左右子树有序),再归并 ---> 后序
_MergeSort(a,begin,mid,tmp);
_MergeSort(a,mid+1,end,tmp);
//归并
int begin1 = begin; int end1 = mid;
int begin2 = mid+1; int end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2) {
if (a[begin1] < a[begin2]) {
tmp[i] = a[begin1++];
}
else {
tmp[i] = a[begin2++];
}
i++;
}
//出来后一个满足另一个不满足
while (begin1 <= end1) {
tmp[i++] = a[begin1++];
}
while (begin2 <= end2) {
tmp[i++] = a[begin2++];
}
memmove(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!");
exit(-999);
}
_MergeSort(a,0,n-1,tmp);
free(tmp);
tmp = NULL;
}

时间/空间复杂度分析

时间花费主要是在合并上.根据归并过程(二叉树图),每一层都需要合并,即每一次都是n次,总共有高度次,即log2(n),因此,归并排序的时间复杂度非常稳定,最好,最坏和平均都是O(N*log2N)

空间复杂度为开辟的等长数组加递归高度次开销,为O(N + log2N),因为N>>log2N, 所以空间复杂度为O(N)

非递归实现

在实现非递归前,对比快排的非递归实现能够更好理解.

归并与快排的区别

  • 归并排序是先划分,再处理,划分这个过程是明确的,即一路划分到最小子数组后再合并.

  • 快排是先处理再分治.分治这个过程结果是不固定的.

因此,归并在非递归实现过程,可以不借助栈或队列进行,直接循环迭代.(归并流程是后序的,难以借助栈/队列实现非递归)

void MergeSortNonR(int *a, int n)
{
//range = 范围、区间。layer = 层数
// 第一个结束下标是第二个起始下标-1
// 每个起始位置可以由循环次数控制 ---- 设一个循环次数i ---- 要循环多少次?
// 结束条件是:range == n
// 下一轮区间变大可以由层数控制 ---- 设一个层数控制N
// 合并时需要两组 ,两组区间一轮循环---- 设两组起始和结束下标
int *tmp = (int*)malloc(n*sizeof(int));
if (!tmp)
{
perror("malloc fail");
exit(-1);
}
int begin1 = 0; int begin2 = 0;
int end1 = 0; int end2 = 0;
//int i = 0;
int rangeN = 1;//一个begin到end 的范围
while (rangeN < n)
{
int j = 0;
//控制变量让其进入下一轮
for (int i = 0; 2 * rangeN * i < n; i++)
{ //范围:2、4、8、16 --- 2^rangeN == range=range*2
//[0,1][2,3][4,5]...到[0,1,2,3][4,5,6,7][8,9,10,11]
begin1 = 2 * rangeN * i; // 0 ,2 ,4 / 0 ,4 ,8 / 0 , 8 ,16 , 24 /
begin2 = begin1 + rangeN ; // 1 ,3 ,5 / 2 ,6 ,10 / 4 , 12, 20 , 28
end1 = begin1 + rangeN - 1;
end2 = begin2 + rangeN - 1;
//修正
if (end1 >= n-1)
{
end1 = n - 1;
// 不存在区间
begin2 = n; //随便写
end2 = n - 1;//小于begin2就行
}
//else if (end1 == n-1)
//{
// // 不存在区间
// begin2 = n;
// end2 = n - 1;
//}
else if (end2 > n-1) //必须要if,否则就是end1 < n-1 => 可能出现end2 也小于n-1
{
end2 = n - 1;
}
//归并
/*
1、一组一组拷贝:比较一组完拷贝到tmp后,就拷贝回原数组,
2、一轮一轮拷贝:比较完一轮所有组后,再拷贝回原数组
*/
j = 2 * rangeN * i;//起始位置
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j] = a[begin1++];
}
else
{
tmp[j] = a[begin2++];
}
j++;
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++]; //只能一个一个拷,或memcpy
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//归并完一组就拷贝回一组
//memcpy(a + 2 * rangeN * i, tmp + 2 * rangeN * i, (end2 - 2 * rangeN * i + 1) * sizeof(int));
}
memcpy(a, tmp, (n)*sizeof(int));
rangeN *= 2;
}
}
posted @   HJfjfK  阅读(228)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示