归并排序的递归:
归并排序是建立在归并操作上的一种有效的排序算法,
该算法是采用分治法(分治的思想一般通过递归去实现);
将有序的子序列(前提是有序,若无序,则要先令其有序)合并,
得到完全有序的序列;
即先使每一个子序列有序,再使子序列段之间有序。
最后将俩个有序表合并成一个有序表;
如图所示:
先让左右子树去解决同样的问题,然后得到结果之后,再整合为整颗树的结果
- 思路:
归并函数需要用到递归,
而且还需要自定义一个归并函数的子函数 _MergeSort (),它的参数同主函数会有区别
在这个子函数中完成递归的全过程,
具体步骤如下:
先在主函数 MergeSort ()中 malloc 一个数组空间,
用以存放经过 归并 + 排序 后的有序表,
然后调用子函数 _MergeSort ()
在子函数 _MergeSort ()中,
创建一个变量 mid 用来划分左子区间与右子区间,
[ begin, mid ] [ mid+1, end ]
这里注意右子区间的 mid 要加上1,这里是一个易错点,
然后就是要通过在子函数中递归俩次子函数(递归俩次的子函数的参数不同)划分左子树与右子树,
接下来就是要进行 归并 + 排序 的操作,
其中通过之前划分到单个数据时,左右子区间(此时一个区间中有一个数据也可被称作区间),
进行比较大小的操作(以升序为例),
俩个子区间在进行比较大小时的思路:
一个子区间的 begin1 ,与另一个子区间的 begin2 先进行比较,
然后 begin1 与 begin2 分别++,用以比较接下来俩个子区间中的其它数据。
最后不要忘记将 malloc 数组空间内已经排好顺序的数据拷贝回原数组中
- 归并排序的时间复杂度:
N * logN
- 代码实现:
//归并排序:
/*是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。
将已有序(如果不有序则使其变得有序)的子序列合并,
得到完全有序的序列;
即先使每个子序列有序,再使子序列段间有序。
若将两个有序表合并成一个有序表,称为二路归并。*/
void _MergeSort(int *a, int begin, int end,int *tmp)/*tmp是归并过程中
要求存在的一个额外的数组,
而 a 是存放有原数据的数组*/
{
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;/*创建变量用来存放形参的原因:
要表示一个区间被划分成的左右俩个的子区间*/
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)//这里以升序为例
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];//为了接近while()中的控制条件
/*因为此时这俩组数据满足升序的情况,
需要去找下一个数据来看是否与另一组数据满足升序的情况*/
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)/*下面的俩个 while 循环程序只会执行到其中一个,
因为在单趟比较的过程中,一定有其中一个区间(左或右)
被比较到最后,而此时另一个区间剩余的数则无需进行比较,
直接把改区间的剩余部分下放到临时的数组中即可*/
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
// 把归并数据拷贝回原数组
memcpy(a + begin, tmp + begin, (end - begin + 1)*sizeof(int));
/*这样的拷贝比使用 for 循环拷贝更方便,更简洁*/
}
void MergeSort(int *a, int n)
{
int *tmp = (int *)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc is fail\n");
exit(-1);
}
_MergeSort(a, 0, n-1, tmp);//n-1是数列区间的边界,不要误写为 n
free(tmp);
}
归并排序的非递归:
首先要明确:
归并排序的非递归分为俩种:
- 部分拷贝
- 整组拷贝
这里归并排序的非递归实现是利用循环去代替递归的;
不使用数据结构中的 栈 和 队列去实现归并排序的非递归的原因:
用栈,队列去实现非递归适合前序遍历而不适合后序遍历,因为出栈后还要在归并的过程中再次使
用出栈后的子区间
非递归排序的思路:
给定一个数列对其进行排序;
先俩俩之间(俩俩相邻的数字)进行排序使其成为多个有序的数列;
然后因为此时俩俩之间有序;
所以将比较的范围扩大到俩俩相邻的有序区间(一个区间含俩个数字);
比较过后,再次扩大区间,
即此时一个所要比较的区间含四个数字
然后再次扩大区间内含有的数据个数,直至最后一个区间可以包含整个数列
注意:
每一个区间在一趟过程中只需要比较一次即可,这里的 “ 俩俩 ” 不同于常规认知的 “ 俩俩 ”
所以,
出现部分拷贝与整组拷贝这俩种非递归代码的原因:
当左(右)区间不存在时,
因为子区间的扩张是按整数倍计算的,
而且是俩个相邻的子区间之间的
俩俩不重复比较,所以是会有数组越界的情况出现
- 越界情况一:左子区间存在,右子区间不存在
该种情况的解决方法:
无需对右区间不存在的左区间进行归并,直接将左子区间放入原数组即可,(部分拷贝)
修改区间的边界(整组拷贝)
- 越界情况二:右子区间存在,但是算多了
或者是:
解决方法是:
缩小到实际的范围即可
- 越界情况三:左子区间存在一部分,而右子区间不存在
解决方法:
缩小到实际的区间范围
- 部分拷贝:
不需要修改边界,而是在每一次循环中就将不参与归并排序的子区间放入 malloc 出来的数组空间
//归并排序的非递归(部分拷贝):
void MergeSortNonR(int* a, int n)//其中 n 是待排序中数据的个数
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;//gap的值是每组中数据的个数
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, end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;//修正边界的关键
// end1越界或者begin2越界,则可以不进行归并的过程了
/*因为如果是整组拷贝而且不去进行边界的修正时,end1 与 begin2 的值会超出数组的边界:
memcpy(a + i, tmp + i, sizeof(int)* n);//当里面乘以的是 n 时,
应该是哪一部分不归并就把哪一部分拷贝下来(部分拷贝无需进行边界的修整),
而不是整组(即包括了不应该归并的,也包括了要归并的)去进行拷贝,
因为会把边界值与随机值的比较结果放到要进行拷贝的整个数组中,
此时尽管不需要修正边界,但也不会出现数组越界的误差。*/
if (end1 >= n || begin2 >= n)
{
break;/*无需进行递归,说明右子区间不存在,也无需修整边界,因为这是部分拷贝*/
}
else if (end2 >= n)/*如果执行到该行代码,
说明一定是归并过程中的右半区间存在,但是算多了的情况
(算多的原因是因为子区间会随着每次循环扩大含有的数据个数)*/
{
end2 = n - 1;
}
//printf("[%d,%d] [%d, %d]--", begin1, end1, begin2, end2);
int m = end2 - begin1 + 1;//无需归并的区间
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];//每一次循环都是从头开始排序
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
/*将无需归并排序的数据拷贝回数组中:
(因为无需归并排序,所以直接拷贝到原数组中便于下一次循环中的排序)*/
memcpy(a + i, tmp + i, sizeof(int) * m);//注意里面乘以的是 m ,不是 n
} //for循环的范围内
gap *= 2; //每一次循环的区间数扩大二倍
}
free(tmp);//最后不要忘记释放 malloc 出来的动态内存空间的地址
}
- 整组拷贝:
需要修整该边界使其避免数组越界,使其多余的数组空间不会参与归并排序,从而为了后面的整组
拷贝奠定了条件
//归并排序的非递归(整组递归):
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int 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, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
// 越界-采用修正边界的方法
if (end1 >= n)
{
end1 = n - 1;
// [begin2, end2]修正为不存在区间
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
// [begin2, end2]修正为不存在区间
begin2 = n;
end2 = n - 1;
}
else if(end2 >= n)
{
end2 = n - 1;
}
//printf("[%d,%d] [%d, %d]--", begin1, end1, begin2, end2);
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
printf("\n");
memcpy(a, tmp, sizeof(int) * n);/*脱离 for 循环后一次性进行整组的拷贝,
类比于其递归形式最后的整组拷贝*/
/*不同于在 for 循环里面的部分拷贝,sizeof(int) 要乘以的是 n */
gap *= 2;
}
free(tmp);
}