归并排序是将一段区间分成若干个子问题,子问题再次分成子问题,这个是分治过程;最后分成的子问题只存在一个数时,就可以开始合并,合并的过程就是比较两个子问题的过程,合并完成后将合并的新数据拷贝到原数据即可。
递归实现归并排序,就是把一个大的数组分治分治,不断分治下去成一个小的数组,
最后分治成只有一个数字为止,然后每一个数字之间两两合并成2个数字,两组数组的两个数字之间再合并成4个数字,以此类推,知道合并成最后一个大的数组为止。
第一步:通过left和right下标找到数组中间位置的下标,以该下标为界限,划分成两组数据。
第二步:重复第一步的过程,但是先把左边的组彻底分完,再分右边的组,是二叉树的前序遍历的思想。
第三大步:不断进行分治,直到分解到还剩一个元素时停下来,判断只有一个元素,就是当left>=right时。
第四步:两两比较,四四比较合并
注意:每次合并完都需要把tmp的数据拷贝回原数组。
最后一步:两个子区间合并成总的区间:
注意:每次合并完都需要把tmp的数据拷贝回原数组。
void _MergeSort(SortDataType* a, int left, int right, SortDataType* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) >> 1; // 右移一位相当于/2
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left; // tmp的下标,不能从0开始,因为有些归并是不会从0开始的。
_MergeSort(a, begin1, end1, tmp);
_MergeSort(a, begin2, end2, tmp);
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 = left; i <= right; ++i)
//{
// a[i] = tmp[i];
//}
// source, destination , size
//每次归并完都拷贝一次
memcpy(a + left, tmp + left, sizeof(SortDataType) * (right - left + 1));
}
void MergeSort(SortDataType* a, int n)
{
SortDataType* tmp = (SortDataType*)malloc(sizeof(SortDataType) * n);
_MergeSort(a, 0, n - 1, tmp);
}
对于递归实现归并排序来说,是把大问题分成小问题,是自上往下分的。
而对于非递归来说,是从小问题开始合并成大问题,是从下往上分的。
以上面的数字为例:
大致思路如下:
但面临第一个问题:
如何选择从一一开始比较到两两开始比较
选择用gap
gap表示每次归并时每组的数据个数
初始时gap = 1,表示第一次是一一比较,每合并完一轮,gap*2,下一轮进行两两比较,以此类推。
不过,第二个理解的难点在于:begin1和end1,begin2和end2该如何选择的问题!
首先是i每次跳跃2×gap,因为一开始是一一比较,比较完一次相当于比较了两个数据,
而gap的含义就是每次合并时每组的数据个数!
那么就需要跳过2 ×gap的长度。
其次是begin1 和end1,begin1 = i 好理解;
end1 = i+gap-1是这样的:i+gap表示从begin1开始的往后的gap个数据, 由于是数据,那么-1才是下标。
而begin2 = i+gap也好理解,end1的后面一个就是begin2;
end2 = i+2*gap-1,就是从i位置开始,跳跃2×gap的数据个数到达最后一个需要比较的数据,-1就是这个最后的数据的下标。
难点3在于边界如何处理
先讲讲归并完一串数字如何拷贝回原数组:
1.一次性拷贝法,也叫梭哈拷贝法(不推荐)
2.每合并一次,就拷贝一次(推荐)
1.梭哈拷贝法:就是到合并完所有的数据之后再一次性拷贝回原数组,简单粗暴。
2.每合并一次就拷贝一次:在一一合并成两个有序数据之后,就拷贝会原数组。
这里的边界有三种情况:
第一种:end1越界了,如下情况,当合并到四四比较时,begin1刚好为末位置,那么end1开始都越界了:
这里的处理方法有两种,但不同的方法是根据如何将归并好的数据拷贝回原数组决定的。
如果是梭哈拷贝法,不管哪种情况,都要修正过来。
先说end1越界的情况,如果是采用梭哈拷贝法一次性拷贝会原数组,就要让end1修正到
end1 = n-1 ,让begin2和end2修正到一个不存在的区间,比如:
begin2 = n ,end2 = n-1。这样做的目的是不让begin2、end2这个区间进入循环,防止拷贝到界外的数据。
如下:
begin2 和end2的修正当然不唯一,只要修正到一个不存在的区间即可。
第二种:begin2越界
可能发生的begin2越界如下:
第二种情况处理方式与第一种相同,在梭哈拷贝法的前提下,需要修正begin2 、end2这两个数据到一个不存在的区间,防止它们被拷贝。
比如:begin2 = n,end2 = n-1。
如下:
第三种:end2越界
此时只需要把end2修正到n-1位置即可,
如下:
注意:begin1是不可能越界的,begin1是不可能越界的,begin1是不可能越界的,因为如果begin1越界了,那后面的end1,begin2,end2全都越界了,那还归并啥!
梭哈写法的代码如下:
void MergeSortNonR(SortDataType* a, int n)
{
SortDataType* tmp = (SortDataType*)malloc(sizeof(SortDataType) * n);
assert(tmp);
int gap = 1;
//gap 是归并过程中,每组数据的个数
while (gap < n)
{
for (int i = 0; i < n; i+=2*gap)
{
//理解难点
//当gap为2时,i每次都会走2步,相当于跳过一个归并组
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int index = i;
//梭哈修正写法,但是不推荐
if (end1 >= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
else 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++];
}
}
//不推荐
//法1:梭哈法:一次性整体拷贝
memcpy(a, tmp, sizeof(SortDataType) * n);
gap *= 2;
}
free(tmp);
tmp = NULL;
}
二、如果是每归并一次,就拷贝一次数据回到原数组的拷贝方法的话,处理情况就不同。
在合并一次拷贝一次的情况下:
1.end1 越界了
因为是合并一次拷贝一次,则前面的红色的数据已经全部从tmp临时数组拷贝回到原数组了,至于3这个数据,不需要再拷贝到tmp了,让他留在原来的地方即可。
所以处理方法是直接break
2.begin2 越界了
与end1越界的情况相同,因为是合并一次拷贝一次,则前面的红色的数据已经全部从tmp临时数组拷贝回到原数组了,至于后面的数据,不需要再拷贝到tmp了,让他留在原来的地方即可。
所以直接break
3.end2越界
同样的,如果是end2越界,就需要修正end2到n-1位置,保证begin1 和begin2可比即可。
所以修正 :end2 = n-1
走一步拷贝一步的非递归写法如下:
void MergeSortNonR(SortDataType* a, int n)
{
SortDataType* tmp = (SortDataType*)malloc(sizeof(SortDataType) * n);
assert(tmp);
int gap = 1;
//gap 是归并过程中,每组数据的个数
while (gap < n)
{
for (int i = 0; i < n; i+=2*gap)
{
//理解难点
//当gap为2时,i每次都会走2步,相当于跳过一个归并组
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int index = i;
//法2:三种情况,但是前两种情况可以使用相同的方法解决
//如果end1越界了,那就不归并了,
//如果begin2越界了,那也不归并了
if (end1 >= n || begin2 >= n)
{
break;
}
//如果end2越界了,让end2修正到n-1位置
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++];
}
// destination source size
//推荐
//法2:归并一点,拷贝一点,需要画图理解
//如果是end1 或begin2大于等于n的时候越界
//不同于梭哈一次性拷贝,梭哈拷贝需要把所有的拷贝进tmp,必须再拷回去,虽然做了无用功,但是是必须做的,这也是比较挫的地方
//这个法2没做无用功,既然end1或者begin2越界了,那就干脆不拷贝了
memcpy(a + i, tmp + i, sizeof(SortDataType) * (end2 - i+1));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
注意两种写法中,拷贝的代码放在了while循环的不同位置!
归并排序具有稳定性,即对于两个及以上的相同数据,归并排序前后不会改变相同数据的相对位置,这个就是稳定性。
归并排序对数据的顺序是不敏感的。
归并排序时间复杂度为O(NlogN),从一一归并开始,每次归并都需要遍历所有数据,但由于是二路归并,所以n个数据的 ”高度“是logN,即没进行一层,就需要遍历一次所有数据,所以时间复杂度就是O(NlogN).
空间复杂度:O(N),因为需要开辟一个临时数组来保存合并好的值,所以空间复杂度是O(N).