目录
基本思想
将待排序的数组不断二分,直到每个子数组只包含一个元素或为空。然后通过合并操作将这些子数组逐步合并成较大的有序数组,最终得到完全有序的结果:
递归实现归并排序的基本思路:
1、分解:将待排序的数组从中间位置切割成两个子数组
- 找到中间位置
- 递归地对左半部分进行归并排序
- 递归地对右半部分进行归并排序
2、合并:将两个已经排好序的子数组合并为一个有序数组
3、重复执行上述步骤,直到每个子数组只包含一个元素或为空
4、复制:将临时空间 temp[] 中的元素复制回原数组 arr[]
递归分解过程如下图所示:
递归合并的过程如下图所示:
下标0和1的组合合并:
下标2和3的组合合并:
下标4和5的组合合并:
下标6和7的组合合并:
下标0至1和2至3的组合合并:
下标4至5和6至7的组合合并:
下标0至3和4至7的组合合并:
如果不是数组个数不是偶数呢?可以自己尝试画一画(绝对不是我懒🤡)
代码实现
//归并排序(递归部分)
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//当递归至只有一个元素时就返回
if (begin >= end)
return;
//找到中间位置
int mid = (begin + end) / 2;
// [begin, mid][mid+1, end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
// [begin, mid][mid+1, end]归并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
//用i来记录临时数组的下标
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++];
}
//将临时数组中排序好内容拷贝给原数组,数组中元素个数等于end-begin+1
//a和tmp表示的是数组首元素,因为要完成数组的拼接工作,所以需要保证tmp拷贝元素的起始位置与要拷贝的目的地a中对应的起始位置相同,比如要将tmp的第3、4两个元素拷贝给a,那么就要从tmp+2开始拷贝两个元素给a的第3、4个元素的位置
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, 0, n - 1, tmp);
free(tmp);
}
巩固理解(重要)
在第一个while循环中进行以下操作:
- 比较两个子数组中的元素大小:
- 如果第一个子数组的首元素小于第二个子数组的首元素,则将前者拷贝到临时数组tmp,并将临时数组的索引i++(即
tmp[i++] = a[begin1++];
)。否则,将后者拷贝到临时数组,并将临时数组的索引i++(即tmp[i++] = a[begin2++];
)- 循环继续直到其中一个子组数遍历完毕(如果左子数组有两个数,右子数组有三个数,那么最后左边的两个和右边的两个都会按顺序放入临时数组,但是右子数组肯定会留下一个(比大小我们每次只要小的大的不管每次结束大的肯定留下了),即左子数组和右子数组在第一个while循环中能放入临时数组的个数是两数组元素之和-1,多出来的那个最大的元素(因为它比所有元素都大所以被留下了了)就交给最后两个while处理)
剩余两个while循环的任务是:
- 如果剩余未参与排序部分的数在左子数组(最大的数),则将该数拷贝进入临时数组
- 如果剩余未参与排序部分的数在右子数组(最大的数),则将该数拷贝进入临时数组
- 最后两个while循环每次只会有一个起作用,因为除了向临时数组中放入数据时临时数组的i++以外,原子数组的下标索引也会++,一个数组全都被放入临时数组的情况下,该子数组下标索引再次++(beigin1++或begin2++)后就会超过数组尾元素下标索引值(end1或end2)
memcpy函数
函数原型:void * memcpy ( void * destination, const void * source, size_t num );
头文件: <string.h>
作用:从source的起始位置开始向后复制num个字节的数据到destination指向的内存空间
注意事项:
1、返回的是目标空间的起始地址
2、源内存空间大小 <= 目标内存空间大小
3、memcpy函数只能处理源内存空间与目标内存空间不重叠时的数据拷贝
(arr+2,arr,20)是想将12345拷贝至34567的内存空间,希望的输出结果是12123458910,但是实际结果为12121218910,这就是因为使用memcpy函数时出现了以下的情况:
当我们完成1和2与3和4的拷贝时,3和4的内存空间其实已经变成1和2了,所以当我们想把原来3和4位置上的数拷贝给5和6时其实是将1和2重新拷贝给了5和6,此时5和6的内存空间也变成了1和2,7和8的情况也是一样的,只不过num=20所以8侥幸逃过一劫。
memcpy函数模拟实现
#define _CRT_SECURE_NO_WARNINGS 1
#include<ctype.h>
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<errno.h>
my_memcpy(void* dest, const void* src, size_t num)
{
void* ret = dest;
assert(dest && src);
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[10] = { 0 };
int arr2[] = { 1,2,3,4,5,6,7,8 };
my_memcpy(arr1, arr2, 20);
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
时空复杂度
最坏时间复杂度:O(N*logN)(logN层,递归N次,N*logN)
空间复杂度:O(N)(归并排序需要额外的空间来存储临时结果和辅助变量。对于每一层递归调用,在合并阶段都需要创建一个与原始输入大小相同的临时数组来存储结果,并在每次迭代结束后释放该内存。因此,在最坏情况下,即所有层级上同时存在(但不重叠)的子问题数量达到最大时,所需额外空间总量达到 O(n) 级别
)
测试各排序性能
void TestOP()
{
srand(time(0));
const int N = 10000000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
}
int begin1 = clock();
//InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
//SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSort(a5, 0, N - 1);
int end5 = clock();
int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();
int begin7 = clock();
//BubbleSort(a7, N);
int end7 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
printf("BubbleSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}
~over~