文章目录
🚆一、递归实现归并排序的问题
递归实现快速排序一样,递归实现归并排序一样需要在栈上建立栈帧,消耗栈空间,当递归深度过深时,就会出现栈溢出的现象。为了解决这个缺陷,本文将带来非递归实现归并排序的算法。
✈️二、利用循环实现归并排序
🍟2.1 思想
我们利用一个新的数组,新数组和原数组等长,先1个数为一组,相邻两组进行归并,并将归并完的数拷贝回原数组,这样归并完一次后数组就变成两两有序;然后2个数为一组,相邻两组进行归并,这样归并完后就四四有序了;然后4个数为一组,相邻两组进行归并,这样归并完后就八八有序了;依次类推,8个数为一组,相邻两组进行归并 ...16个数为1组,32个数为1组...
🥪2.2 图解
🚢三、代码实现
void MergrSortNonR(int* arr, int n)
{
//创建临时数组
int* tmp = (int*)malloc(sizeof(int)*n);
int gap = 1;//代表gap个数和gap个数归并,初始为1
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//每次需要归并的区间为 [i,i+gap-1]和[i+gap,i+2*gap-1]
//开始进行归并
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
int index = i; //归并时放入临时数组的位置,从两个需要归并的区间的最左边开始
while (begin1 <= end1 && begin2 <= end2)
{
//谁小谁下放至临时数组
if (arr[begin1] < arr[begin2])
{
tmp[index++] = arr[begin1++];
}
else
{
tmp[index++] = arr[begin2++];
}
}
//循环结束把还没放下来的数放下来,两个循环只会进去一个
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
}
//一轮归并完再拷贝
for (int j = 0; j <n ; j++)
{
arr[j] = tmp[j];
}
gap *= 2;
}
//销毁临时数组
free(tmp);
}
代码写完了,各位觉得这个代码有问题吗?
哈哈,当然有,问题很大,这个代码只能对2^n个数进行排序,多一个都不行
🛩️四、代码剖析
在排序的过程中会出现三种情况,分别为右区间不存在
;右区间存在但是算多了
;左区间算多了
。针对这三种情况我们怎么解决呢?
🧀4.1 右区间不存在
我们看下用上面的代码排一下这个数组:
崩了???
对于上面的情况,第一轮两两归并时,最后的3就没有右区间和他归并,怎么办?
答案就是:直接break掉
有的人可能会担心,直接break掉会不会导致还有数没有归并完?
答案就是:不会
因为不存在右区间的情况一定出现在本轮归并的末尾,所以不存在会漏掉其他数。 怎么处理?
在归并前加上一个判断即可
if (end1 >= n-1)
{
break;
}
🍿4.2 右区间存在但是算多了
对于上面这种情况,当进行第二轮归并时,最后的两组为[3 10]和[8 ?],其实这个时候右区间只有8一个数,但是代码在计算end2的时候其实已经计算到8的后面去了,所以会出现什么情况?
对于这种情况我们需要对end2进行修正。
怎么修正?
将end2修正为最后一个数的下标n-1
if (end2 >= n)
{
end2 = n - 1;
}
🌭4.3 右区间不存在的同时左区间算多了
还是这个例子,当进行第三轮归并时,[ 1 4 7 9 ]和[ 0 2 8 10 ]进行归并,[ 3 8 10]和[???]进行归并,这里不仅缺省右区间,左区间还少一个数,但是代码在计算end1时,依旧会算到8的后面,如果我们在一轮归并完后再拷贝,就会导致我们拷贝回去的代码进一个随机数。如下图所示
所以我们不能等到一轮归并完之后一起将数组拷贝回原数组,应该一组归并完后立刻将数据拷贝回原数组。
🚀五、修改后完整代码
void MergrSortNonR(int* arr, int n)
{
//创建临时数组
int* tmp = (int*)malloc(sizeof(int)*n);
int gap = 1;//代表gap个数和gap个数归并,初始为1
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//每次需要归并的区间为 [i,i+gap-1]和[i+gap,i+2*gap-1]
//开始进行归并
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
int index = i; //归并时放入临时数组的位置,从两个需要归并的区间的最左边开始
//归并过程中,右区间不存在
//这里直接break掉,因为必定已经归到最后了
if (end1 >= n-1)
{
break;
}
//如果右区间存在,但是算多了,需要修正
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
//谁小谁下放至临时数组
if (arr[begin1] < arr[begin2])
{
tmp[index++] = arr[begin1++];
}
else
{
tmp[index++] = arr[begin2++];
}
}
//循环结束把还没放下来的数放下来,两个循环只会进去一个
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//一次归并完后就把归并完的数据赶紧拷贝回原数组
//不要留着全部归并完再拷贝,文章里会讲原因
for (int j = i; j <= end2; j++)
{
arr[j] = tmp[j];
}
}
//不要一起拷贝
//for (int j = 0; j <n ; j++)
//{
// arr[j] = tmp[j];
//}
gap *= 2;
}
//销毁临时数组
free(tmp);
}