一、归并排序基本介绍
①基本思想
归并排序的基本思想是:分而治之。现有如下两个有序数组,[1, 3, 5, 7, 9],[2, 4, 6, 8, 10],我们利用双指针分别指向两个数组,利用双指针法很轻松的可以将上述两个有序数组合并为一个有序的数组。
上面的案例给了我们启发:我们可以将待排序数组一分为二,如果能实现左右两数组分别有序,那么利用双指针法的技巧就可以轻松实现整体数组的有序。
利用分而治之的思想,我们将问题不断分解为规模更小的子问题。直到子问题不可以在分解,也就是数组中只有一个元素的时候,此时对于子数组来说只有一个数,那自然是有序的,所以使用双指针法使用的条件。由此我们由最小子问题的有序实现局部的有序,由局部的有序,实现整体数组的有序。
②过程图解
二、递归方法实现
void MergeSort(int* arr, int* tmp, int left, int right) //(1)
{
if (left >= right) //(2)
return;
int mid = left + (right - left) / 2;
MergeSort(arr, tmp, left, mid); //(3)
MergeSort(arr, tmp, mid + 1, right);
int start1 = left, end1 = mid;
int start2 = mid + 1, end2 = right;
int index = 0;
while (start1 <= end1 && start2 <= end2) //(4)
{
if (arr[start1] < arr[start2])
tmp[index++] = arr[start1++];
else
tmp[index++] = arr[start2++];
}
while (start1 <= end1)
tmp[index++] = arr[start1++];
while (start2 <= end2)
tmp[index++] = arr[start2++];
memcpy(arr + left, tmp, sizeof(int) * (right - left + 1)); //(5)
}
①代码剖析
- arr表示待排序数组,tmp是一个临时数组,用于存储arr中两个子数组合合并后的结果。最后将tmp的结果重新拷贝回arr数组中
- 注意递归的边界条件
- 这里注意区间的划分。不能划分成:[left, mid - 1] 与[mid, right],这样会产生死循环。
- 使用双指针法实现两个有序数组的合并
- 最后将tmp的数组拷贝回arr数组时注意拷贝的位置
②时间复杂度分析
- 不像快速排序,归并排序是严格的二分.
- 递归的时间复杂度 = 递归次数 x 每次递归的时间
- 我们可以看到递归的调用次数为 logn, 每一次调用所用去的时间是O(n)(双指针法要用去O(n)的时间),所以归并排序的时间复杂度为O(N * logn)
③空间复杂度
- 因为要创建一个tmp数组作为合并后数据的临时存放空间,所以要占用的空间复杂度为 O(N)
三、循环方法实现
①思想介绍
如何用循环的思想来解决上面的问题呢?我们回过头来看归并排序中的操作,其实它本质上是一种后序遍历——即先遍历左右子树再处理根节点,也就是先处理左右子树所对应的数据空间,再将左右数据空间进行合并。既然我们要用循环实现,我们只要先划分好区间,再进行合并即可。
我们只需要创建两层循环,一层gap循环表示划分区间的大小,接着在某一gap的大小下,遍历每一组数据。gap每次×2倍增,反向还原二分的过程。
void _MergeSort(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n); //(1)
for (int gap = 1; gap < n; gap *= 2) //(2)
{
int index = 0;
for (int i = 0; i < n; i += 2 * gap) //(3)
{
int start1 = i, end1 = i + gap - 1;
int start2 = i + gap, end2 = i + 2 * gap - 1;
if (end1 >= n) //(4)
end1 = n - 1;
if (start2 >= n)
{
start2 = n;
end2 = n - 1;
}
if (end2 >= n)
end2 = n - 1;
while (start1 <= end1 && start2 <= end2) //(5)
{
if (arr[start1] <= arr[start2])
tmp[index++] = arr[start1++];
else
tmp[index++] = arr[start2++];
}
while (start1 <= end1)
tmp[index++] = arr[start1++];
while (start2 <= end2)
tmp[index++] = arr[start2++];
}
memcpy(arr, tmp, sizeof(int) * n); //(6)
}
}
②代码剖析
- tmp暂时存储arr合并数组后的结果
- gap表示划分组别的大小。初始 gap = 1表示每个数组中的成员个数为1个,以此类推
- 每2*gap个元素进行一次比较合并,i控制合并的次数
- ⭐对划分区间的范围修正:由于区间是我们人为按照2^n进行划分的,所以免不了出现范围的越界,因此最简单的处理方法就是在强制的对数据的范围进行修正。
显然除了start1之外,其他三个端口都有可能出现越界的情况,因此我们需要分类讨论:
- end1 >= n,说明左子区间的右端越界,将其强行赋值为n - 1
- start2 >= n, 说明右子区间不存在,此时我们通过赋值使得start2大于end2,这样就不会进入下面的循环
- end2 >= n, 说明右子区间的右端越界,将其强行赋值为n - 1,
- 使用双指针法实现两数组的合并
- 最后别忘记将tmp数组的内容拷贝回原数组arr中
③错误修正方法辨析
if (start1 >= n)
start1 = n - 1;
if (start2 >= n)
start2 = n - 1;
if (end2 >= n)
end2 = n - 1;
①问题
我们可以将上面的修正方法改为,只要越界了就强制设置为n - 1吗?
②解答
不可以。虽然start1和end1以及start2和end2的范围没有问题,但是在start1遍历完后即start1 > end1,此时虽然两个数据已经处理完毕,但是start2和end2仍然满足start2 <= end2的条件,所以会继续往tmp数组中添加数据9,也就意味着数字9被重复添加两次,那么在tmp数组中就会发生越界错误。
④补充
void Merge(struct data* arr, struct data* tmp, int low, int n1, int n2)
{
int begin1 = low, end1 = low + n1 - 1;
int begin2 = low + n1, end2 = low + n1 + n2 - 1;
int index = low;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1].key_ < arr[begin2].key_)
{
tmp[index++] = arr[begin1++];
}
else
{
tmp[index++] = arr[begin2++];
}
}
while (begin1 <= end1) tmp[index++] = arr[begin1++];
while (begin2 <= end2) tmp[index++] = arr[begin2++];
}
void MergeSort(struct data* arr, int n)
{
struct data* tmp = new struct data[MAXSIZE];
int len = 1;
while (len < n)
{
int low = 0;
while (low + len < n) // 说明至少存在两个子序列需要合并
{
int len1 = len, len2 = len;
if (low + 2 * len >= n)
len2 = n - low - len;
Merge(arr, tmp, low, len1, len2);
low += len1 + len2;
}
memcpy(arr, tmp, sizeof(struct data) * n);
len *= 2;
}
}