归并排序
- OJ题引入
- 排序思想
- 代码注意事项
- 递归方法——归并排序的实现
- 非递归方法——归并排序的实现
- 引入
- 局部的分割与一次排序
- 多次排序
- 隐藏bug与代码完善
OJ题引入
在学习归并排序之前,我们先来回忆一下我们在顺序表阶段写过的一个OJ题——两个有序数列的合并问题,题目如下:
这道题我们使用双指针与一个新数组即可快速得到正确的答案,代码如下:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int a[m+n];
int cur1=0;
int cur2=0;
int put=0;
while(cur1<m&&cur2<n)
{
if(nums1[cur1]<=nums2[cur2])
{
a[put++]=nums1[cur1++];
}
else if(nums1[cur1]>nums2[cur2])
{
a[put++]=nums2[cur2++];
}
}
while(cur1<m)
{
a[put++]=nums1[cur1++];
}
while(cur2<n)
{
a[put++]=nums2[cur2++];
}
for(int i=0;i<nums1Size;i++)
{
nums1[i]=a[i];
}
}
由两个有序数组合并成为一个有序数组就是本节标题归并中的“并”一字,其实会解本道题,归并排序的算法我们已经读懂了一半,下面我们具体来讲解归并排序的相关思想。
排序思想
所谓归并排序,顾名思义就是“先归后并”,归其实就是我们熟悉的递归,并也就是合并的意思先递归再合并。那我们通过引入已经知道合并操作是让书局有序的操作,那递归的作用是什么?如何递归?就是接下来我们需要讨论的重点。
如下图所示,包含了归并排序的重要步骤。
我们将待排数据从中间分割成较小的数据,经过数次分割,数据组被我们分割成单个数据(该部操作本质上就是递归的大问题化小问题思想),可采用递归实现。当分割到一个元素时,其实一个元素就构成一个有序的数据组,此刻开始有序数组的合并操作(与上述OJ题思想一致)。
动态图片演示如下:
代码注意事项
1.进行合并时别忘记拷贝这一操作,同时由于有拷贝这一操作,我们写代码时,最好将归并操作重新用一个函数进行分装
2.拷贝时,我们要边合并边拷贝,不能所有合并结束之后才开始拷贝,这是因为没一次合并我们都会改变数组的顺序,改变之后的数组是下一次合并的数组基础。
3.合并操作时,各数组的下标要和递归所传值保持一致。
递归方法——归并排序的实现
合并操作我们需要一个新的数组,在VS环境下,我们最好采用动态开辟建立该新数组。(以升序为例实现)
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int begin = 0;
int end = n - 1;
if (tmp == NULL)
{
perror("malloc error\n");
}
_MergeSort(a, 0, n-1, tmp);
//注意动态开辟时要对空间进行释放。并对指针置空,避免野指针。
free(tmp);
tmp=NULL;
}
归并操作在我们的分装函数 _MergeSort 中实现。
void _MergeSort(int* a, int begin, int end,int* tmp)
{
//当分割时只有一个元素或没有任何元素时,返回。
if (begin >= end)
{
return;
}
int key= (begin + end) / 2;//从中间分割数组
//[begin,key]&&[kye+1,end]为分割区间划分组
_MergeSort(a, begin, key,tmp);
_MergeSort(a, key + 1, end,tmp);
//合并时,由于合并区间的可变性,注意合并区间的范围
int begin1 = begin; int end1 = key;
int begin2 = key + 1; int end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else {
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//拷贝操作一定要一次归并操作结束后立刻进行。
memcpy(a+begin, tmp+begin, sizeof(int) * (end-begin+1));
}
非递归方法——归并排序的实现
引入
我们之前实现斐波那切数列的的非递归算法时采用的是循环法,先求和再赋值。那我们实现归并排序的算法时我们能否借助同样的思路?递归思路是由整体拆分到局部合并,而循环思路就是由局部到整体直接合并。
如下图所示,循环解法就是反其道而行之,现合并红色椭圆组别,再排序蓝色椭圆组别。
局部的分割与一次排序
我们要进行局部的合并,首先就要考虑如何进行局部分割,考虑到合并时我们需要有序数组进行合并。所以分割时,我们考虑第一次循环一个数进行分割,建立一个gap变量进行分组。
int gap=1;
for(int i=0;i<n;i+=gap*2)
{
......//组内进行合并排序
}
由gap建立完组别并控制了排序内部结束条件(i<n),接下来我们应该考虑每组中两个待排数组的区域划分:我们以合并排序的第一次循环一个数为一组为例:
gap=1;
gap=2;
由以上两组可轻易发现数组的分区规律为
第
一
个
数
组
:
[
b
e
i
g
i
n
1
,
e
n
d
1
]
=
[
i
,
i
+
g
a
p
−
1
]
第
二
个
数
组
:
[
b
e
i
g
i
n
2
,
e
n
d
2
]
=
[
i
+
g
a
p
,
i
+
2
∗
g
a
p
−
1
]
\begin{matrix} 第一个数组:[beigin1,end1]=[i,i+gap-1]\\ 第二个数组:[beigin2,end2]=[i+gap,i+2*gap-1] \end{matrix}
第一个数组:[beigin1,end1]=[i,i+gap−1]第二个数组:[beigin2,end2]=[i+gap,i+2∗gap−1]
通过以上排序数组分组分析,我们能结合合并有序数组写出一次排序循环(别忘了开辟空间):
拷贝数据时,每次要拷贝 [i,end2] 个数据。
void MergeSortNouR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
int gap = 1;
for (int i = 0; i < n; i += gap * 2)
{
int begin1 = i; int end1 = i + gap - 1;
int begin2 = i + gap; int end2 = i + 2 * gap - 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) * (end2 - i + 1));
}
}
多次排序
当然这仅仅是一次排序,我们要进行多次排序,那总共要进行多少次排序呢?当数组元素(gap)大于总元素个数(n)时就排序完成。代码如下:
void MergeSortNouR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += gap * 2)
{
int begin1 = i; int end1 = i + gap - 1;
int begin2 = i + gap; int end2 = i + 2 * gap - 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) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
隐藏bug与代码完善
那代码写到这就算结束了吗?
当我们运行这个代码时,编译器竟然报错了!!!
这是由于,代码还有一处隐藏bug。数组的区域划分并不一定和上述一致(不一定为偶数),有可能少几个值,这时就要对区间进行限制。那考虑需要限制哪几个值呢?
begin1:由于begin1=i,已经在for循环中进行了限制无需限制;
end1:有可能大于n,大于n时代表数据已经排好;
begin2:和end1类似;
end2:end2大于n时代表数组不为偶数,按上诉分割方式并不能完刚好分割,造成越界访问,end2最大能取n-1因此,添加限定之后的代码如下:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += gap * 2)
{
int begin1 = i; int end1 = i + gap - 1;
int begin2 = i + gap; int end2 = i + 2 * gap - 1;
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 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) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}