本篇博客梳理归并排序算法
一、归并排序递归版本
1.算法思想
使每个子序列有序,再使子序列之间有序,是分治法的典型应用,归并的过程发生在递归往回退的过程当中,可以类比二叉树的后序遍历
递归最小子问题:区间只有1个值或者不存在,就认为是有序了
2.具体操作
(1)归并操作
归并操作图解如下
(2)使左右子序列有序
使用递归即可解决,递归至最小子序列之后回归的过程会进行归并操作,递归结束之后整个序列自然也就有序了
程序架构如下
void _MergeSort(int* a,int* tmp,int begin,int end)
{
if(begin>=end)//此时达到了递归的最小子问题:也就是区间只有1个值或者不存在
return;
//...递归左右子序列,递归完之后进行归并操作
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if(tmp==NULL)
{
perror("malloc failed!");
return;
}
_MergeSort(a, tmp, 0, n - 1);
}
3.动图展示
数据被拉到下面进行的操作就是归并操作,可以想象底下有一个tmp数组,begin1和begin2在上面选出更小的那个数放到tmp数组里面,这个子序列归并完成之后再拷贝回原数组
4.具体代码实现
void _MergeSort(int* a, int* tmp, int begin,int end)
{
if (begin >= end)
return;
//递归到左右子序列
_MergeSort(a, tmp, begin, (begin + end) / 2);
_MergeSort(a, tmp, (begin + end) / 2 + 1, end);
//归并操作
int mid = (begin + end) / 2;
int begin1 = begin,end1 = mid;//左子区间[begin1,end1]
int begin2 = mid + 1,end2 = end;//右子区间[begin2,end2]
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i] = a[begin1];
i++;
begin1++;
}
else
{
tmp[i] = a[begin2];
i++;
begin2++;
}
//此时可能会剩下一组数据没有完全拷贝到tmp数组当中
}
//两个循环只会进一个
while (begin1 <= end1)
{
tmp[i] = a[begin1];
begin1++;
i++;
}
while (begin2 <= end2)
{
tmp[i] = a[begin2];
begin2++;
i++;
}
//把tmp数组拷回原数组
//注意:dest是a + begin处位置,不然会导致前面已经拷贝好的数据被覆盖掉
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 failed!");
return;
}
_MergeSort(a, tmp, 0, n - 1);
}
有一个细节要注意:左右子区间的划分
如果是[偶数,偶数+1]区间的mid,mid=(begin+end)/2。若分割成[begin,mid-1]和[mid,end],
会导致[mid,end]跟原区间一模一样,进而导致死循环(偶数代入2即可验证)
正确的划分方法:[begin,mid]和[mid+1,end]
二、归并排序非递归版本
1.算法思想
先从最小的子序列开始归并,然后归并的序列不断扩大
(1)单趟:走归并的逻辑,用for循环控制
(2)整体:用gap迭代控制归并的子序列规模,gap就是每组归并的数据个数。外面套一层while循环即可
具体就是:一 一归并,两两归并,四四归并…以此类推
随着gap的增大,左右子区间的边界就需要考虑是否越界的问题
左子区间:[begin1,end1]
右子区间:[begin2,end2]
分为以下三种情况(begin1已经受循环条件限制,不可能越界)
- [begin1,end1],[begin2,end2]中只有end2越界
- [begin1,end1],[begin2,end2]中,begin2,end2越界
- [begin1,end1],[begin2,end2]中,end1,begin2,end2越界
针对后两种情况,总结起来就是右子区间不存在了,那么也就不需要进行归并了,直接break结束循环即可
对于第一种情况,右子区间还有一些数需要进行归并操作,那么只需要修正以下end2即可
if(end2>=n)
end2 = n - 1;
2.具体代码实现
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc failed");
return;
}
int gap = 1;
while (gap <= n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;//左子区间[begin1,end1]
int begin2 = i + gap, end2 = i + 2 * gap - 1;//右子区间[begin2,end2]
if (begin2 >= n)
break;
//到此至少begin2没有越界
if (end2 >= n)
end2 = n - 1;//把end2修正一下,然后继续归并
//归并操作
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j] = a[begin1];
j++;
begin1++;
}
else
{
tmp[j] = a[begin2];
j++;
begin2++;
}
//此时可能会剩下一组数据没有完全拷贝到tmp数组当中
}
//两个循环只会进一个
while (begin1 <= end1)
{
tmp[j] = a[begin1];
begin1++;
j++;
}
while (begin2 <= end2)
{
tmp[j] = a[begin2];
begin2++;
j++;
}
//把tmp数组拷回原数组
//i就相当于begin
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap = gap * 2;
}
}
三、归并排序特性总结
1.时间复杂度:O(N·logN)
2.空间复杂度:O(N)
3.稳定性:稳定
4.缺点在于需要O(N)的空间复杂度,归并排序更多是解决在磁盘中的外排序问题