这一篇文章,我们来讲解一下使用非递归的方式来实现归并排序:
如果有小伙伴没有看过归并排序的常规写法,或者想了解其他的排序方法,可以看下面这篇博客:
【图解C语言】只是朴实无华的排序罢了
首先我们我们要思考,我们是使用循环还是栈模拟来代替递归:
对于这个问题,一般我是这样选择的:递归是把一个问题有最大分解到最小,而循环是把一个问题由最小还原到最大,所以,如果一个问题从最小大最大 或者 最大到最小 都可以解决,那么一般我们使用循环去实现递归,否则,使用栈模拟。
可能这样讲比较抽象,我们以斐波那契数列为例:
Fib(N)=Fib(N-1)+F(N-2)
我们使用递归的解法是:
很显然,对于递归,就是一个从上到下的过程,而循环,就是一个从下到上的过程。所以,对于斐波那契数列的模拟,我们可以用循环替代递归。
·
我们依然以数组[10, 6 ,7 , 1, 3, 9, 4, 2]为例:
既然我们选择用循环模拟,就应该从递归的方面思考,从局部到整体排序:
从图里可以看出,我们的思路很明确,先一个一组,每组排序(一个元素默认有序)先两个一组,每组排序,再四个一组,每组排序,最后八个一组,整体排序,
总结为一句话,我们使数组由局部有序 到整体有序。
当然,这里有同学要问:这个数组排序前哪里局部有序了?当然有,如果分组的时候我们只以一个元素为一组,每一组就是有序的。
为了控制我们每组元素的个数 以及在合并两个组的时候方便排序,我们要对每个组的两个边界做标记。在这里我们以begin作左边界,end作右边界.
文字描述有点抽象,因此我们可以通过画图表现大致的过程:
- 最开始,每一组中元素只有一个,相邻两组合并。
2.经过一次归并,数组中元素两两有序,现在我们以两个元素为一组,相邻组合并:
- 经过两次归并,数组中元素四四有序,现在我们以四个元素为一组,相邻组合并
4.经过三次归并,数组中元素全部有序,排序结束。
注意,在两个组的归并中,由于数组是一段连续的空间,所以,我们对数组的局部排序在原数组上并不方便,所以,我们得特别开辟一个动态数组,我们从两个组中取数放到临时数组中,排好序后,再将临时数组中的数据拷贝回原数组的对应位置。具体的分析在之前的 归并常规写法中有。
相信讲到这里,思路已经很清晰了,我们现在以代码的形式写出:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
int groupNum = 1;
while (groupNum < n)
{
for (int i = 0; i < n; i += 2 * groupNum)
{
// [begin1][end1] [begin2,end2]
// 归并
int begin1 = i, end1 = i + groupNum - 1;
int begin2 = i + groupNum, end2 = i + groupNum * 2 - 1;
int index = begin1;
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++];
}
}
// 拷贝回原数组
for (int i = 0; i < n; ++i)
{
a[i] = tmp[i];
}
groupNum *= 2;
//PrintArray(a, n);
}
free(tmp);
}
这个时候,运行我们的代码,却会发现程序报错:
显然,我们的程序存在越界相关的问题,逻辑并不严谨。
那么问题出在哪里?
我们将之前的示范数组添加两个数,然后通过画图看一下运行过程:
通过画图,问题很显然了,在我们第二次归并的时候,我们的组数不是偶数,导致最后一组没有其他组和它合并。
也就是说:数组数据个数,并不一定是按整数倍,所以划分的分组可能越界或者不存在
这个问题并不难解决:
if (begin2 >= n)
{
begin2 = n + 1;
end2 = n;
}
当出现这个问题,我们将[begin2,end2]这个区间修正为一个不存在的区间,即end2>begin2,这样我们就直接跳过两个组的归并过程,不处理这个多出的组。
组数可能是奇数,那么数组的元素个数是奇数也会是个问题,而且与上面的情况不一样,所以我们得使用其他方法修正.
这个时候begin2没有越界,end2越界,所以我们针对这一点进行修正:
if (end2 >= n)
{
end2 = n - 1;
}
我们将end2修正为n-1,也就是begin2==end2,这样就可以使程序正常运作了,并且将这个多出的数参与排序。
同理,可能出现begin1没有越界,end1越界,我们用同样方法修正:
if (end1 >= n)
{
end1 = n - 1;
}
至此,我们可以写出完整的程序:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
int groupNum = 1;
while (groupNum < n)
{
for (int i = 0; i < n; i += 2 * groupNum)
{
// [begin1][end1] [begin2,end2]
// 归并
int begin1 = i, end1 = i + groupNum - 1;
int begin2 = i + groupNum, end2 = i + groupNum * 2 - 1;
int index = begin1;
// 数组数据个数,并不一定是按整数倍,所以划分的分组可能越界或者不存在
// 1、[begin2,end2] 不存在, 修正为一个不存在的区间
if (begin2 >= n)
{
begin2 = n + 1;
end2 = n;
}
// 2、end1越界,修正一下
if (end1 >= n)
{
end1 = n - 1;
}
// 3、end2越界,需要修正后归并
if (end2 >= n)
{
end2 = n - 1;
}
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++];
}
}
// 拷贝回原数组
for (int i = 0; i < n; ++i)
{
a[i] = tmp[i];
}
groupNum *= 2;
//PrintArray(a, n);
}
free(tmp);
}