手撕排序算法系列之第六篇:归并排序(上)
从本篇文章开始,我会介绍并分析常见的几种排序,大致包括直接插入排序,冒泡排序,希尔排序,选择排序,堆排序,快速排序,归并排序等。
大家可以点击此链接阅读其他排序算法:排序算法_大合集(data-structure_Sort)
这篇文章我们将一起讨论归并排序的递归方法实现
目录
1.归并的思想
2.归并排序的思想
2.1基本思想
3.归并排序的代码实现(递归法)
4.递归方法的代码解析
4.1解析
4.2注意事项
5.归并排序测试
6.归并排序的时空复杂度
6.1时间复杂度
6.2空间复杂度
在学习的过程中,这是我第二次与归并方法相识。第一次见归并排序还是再链表OJ题中,合并两个有序列表,我们当时就采用了归并的思想。
在此,顺带回顾加学习总结一下归并的思想(默认升序):
归并是有两个有序的数组(或链表),分别使用两个指针来遍历和控制这两个数组,比较两个指针的值的大小,将值小的先取下来放到临时开辟的空间中,然后让值小的数组的指针往后走一步,再进行比较。由于每次只拿下来一个数字,因此一定会有一个数组先被取完,由于我们是对两个有序数组进行合并,因此我们在一个数组遍历完后直接将另一个数组连接在新数组后即可。
归并排序是建立在归并的操作上进行的一种排序方法。
分解:
归并排序的思想是将已有的区间二分成两个子区间(左区间和右区间)。然后分别对这两个子区间再进行二分.....直到子区间只剩下一个数据时不再划分,因此一个数据被认为有序。
合并:
然后将两个子序列进行合并成一个有序的序列,直到合并成最终的有序数组。
举个实例方便理解:
归并排序的实现和常规合并是有一点点区别的,区别在归并排序只会开辟一个临时空间,将最小的子区间合并结果放在这个临时空间内,然后将临时空间的有序数组memcpy到原数组上,再进行下一个区间的合并。直到最后一次合并,有序的数组会存在临时空间内,然后将临时数组memcpy到原数组,再将临时空间free掉即可。
//归并排序 -- 递归
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]
//printf("归并[%d,%d][%d,%d]\n", begin, mid, mid+1, end);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
这一段我们正是使用了递归
我们能够发现我们实现划分左区间,将左区间划分成最小区间就剩一个数据时才会对右区间进行划分。待左右两边全部划分完成后,我们就要对最小区间进行合并。
这一段我们能够发现是合并的过程,我们讲第一段区间用[begin1,end1]表示。第二段区间用[begin2,end2]表示。index记录我们在临时开辟数组的begin位置,下面就是简单的两个区间合并。到最后我们要将临时空间内排好序有序数组重新拷回原数组。这里使用memcpy函数
在划分两个小区间是我们一定划分为[begin,mid]和[mid+1,end]。
一定不能是[begin,mid-1],[mid,end]!!!
这里我们虽然用逻辑来想好像没什么差别,但是在分子区间的时候,这种情况可能会造成死循环。用下面这个为例子
如果区间是[begin,mid-1],[mid,end]
但是如果区间是 [begin,mid]和[mid+1,end],我们就会避免这个问题
我们依然画图来看
区间细节总结:我们第一种情况之所以出现死循环的原因是因为,我们区间是begin+end的和除以2,由于计算器对除法是向下舍去的,因此如果一旦出现此情况,左区间一定要包括mid,这样才会补上因为除法造成的舍去。如果左区间只有一个值时,我们再对右区间划分时,我们就可能会造成死循环。
//打印数组
void PrintArray(int* a, int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
//归并排序 -- 递归
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]
//printf("归并[%d,%d][%d,%d]\n", begin, mid, mid+1, end);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
assert(tmp);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
//归并排序
void TestMergeSort()
{
int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
MergeSort(a, sizeof(a) / sizeof(int));
PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
//归并排序
TestMergeSort();
return 0;
}
测试结果:
我们递归分解是二分法,时间复杂度是O(logN),合并过程是O(N)。
因此时间复杂度是O(N*logN)。
我们开辟了临时空间长度为N因此空间复杂度是O(N)。
以上就是对并排序的递归方法,非递归方法我们再明天总结出来,还望大家持续关注~
(本篇完)