前言
在好久之前学习过排序算法,本篇文章用来复习归并排序,将实现递归和非递归版本。
递归写法
代码
void Copy(int* src, int* dest, int left, int right)
{
while (left <= right)
{
dest[left] = src[left];
left++;
}
}
void Merge(int* src, int* dest, int left, int m, int right)
{
int i = left, j = m + 1;
int index = left;
while (i <= m && j <= right)
{
dest[index++] = src[i] <= src[j] ? src[i++] : src[j++];
}
while (i <= m)
{
dest[index++] = src[i++];
}
while (j <= right)
{
dest[index++] = src[j++];
}
}
void MergePass(int* src, int* dest, int left, int right)
{
if (left < right)
{
int mid = left + (right - left) / 2;
MergePass(src, dest, left, mid);
MergePass(src, dest, mid + 1, right);
Merge(src, dest, left, mid, right);
Copy(dest, src, left, right);
}
}
void MergeSort(int* nums, int numsSize)
{
if (nums == nullptr || numsSize < 2) return;
int* tmp = new int[numsSize];
MergePass(nums, tmp, 0, numsSize - 1);
delete[] tmp;
}
解析
Merge函数的参数有五个,有三个相当于下标,这三个下标把一组数据分成两组。然后在函数内循环时候比较大小,小的数据往前放。退出第一个循环时有两种情况,要么前半部分数据放完了,要么后半部分数据放完了,再加两个循环对其补充,保证所有元素被放入到dest内。
对于MergePass函数,首先会找出中间下标,再两个递归,参数分别给的是左下标到中间下标,中间下标到右下标。在递推过程中,直到左右两个指针相遇,意味着只剩一个数据时,开始回归,回归时执行Merge和Copy函数,使得数据在回归时变得有序。是 11有序,22有序,44有序,每次回归后有序都是比上一次多一倍,因为递推划分的时候也是一半一半划分。
示例
int main(void)
{
int nums[] = { 52, 12, 78, 90, 34, 23, 100, 56, 45, 67, 89 };
int numsSize = sizeof(nums) / sizeof(nums[0]);
PrintNums(nums, numsSize);
MergeSort(nums, numsSize);
PrintNums(nums, numsSize);
return 0;
}
非递归
在之前写过非递归的过程,在这里先把代码贴出来,然后再给出第二种非递归形式。
方法1
static void Merge(int* arr, int len, int gap)
{
//申请额外空间,等长于原始数组
int* brr = (int*)malloc(sizeof(int) * len);
assert(brr != NULL);
int i = 0; // 额外数组brr的下标
//申请四个指针,用来表示合并两组的边界
int low1 = 0;
int high1 = low1 + gap - 1;
int low2 = high1 + 1;
int high2 = low2 + gap - 1 < len ? low2 + gap - 1 : len - 1;
while (low2 < len) // 当low2满足小于len,则low1和high1肯定满足条件
{
//比较两组数据
while (low1 <= high1 && low2 <= high2)
{
if (arr[low1] <= arr[low2])
{
brr[i++] = arr[low1++];
}
else
{
brr[i++] = arr[low2++];
}
}
//上面的while循环跳出
//要么因为右边组数据比较完了,那就把
//左边组剩下的数据也放下来
while (low1 <= high1)
{
brr[i++] = arr[low1++];
}
//要么因为左边组数据比较完了,那就把
//右边组剩下的数据也放下来
while (low2 <= high2)
{
brr[i++] = arr[low2++];
}
//更新指针
low1 = high2 + 1;
high1 = low1 + gap - 1;
low2 = high1 + 1;
high2 = low2 + gap - 1 < len ? low2 + gap - 1 : len - 1;
}
//上面的外层循环跳出,可能存在这种情况
//即右边组没有数据了,左边还有,则把左边数据放下来
while (low1 < len)
{
brr[i++] = arr[low1++];
}
//再把排好序后的数据放回原始数组
for (int j = 0; j < len; ++j)
{
arr[j] = brr[j];
}
//再把额外空间释放掉
free(brr);
brr = NULL;
}
void Merge_Sort(int* arr, int len)
{
assert(arr != NULL);
for (int i = 1; i < len; i *= 2)
{
Merge(arr, len, i);
}
}
方法2
和递归方法一样,先将Merge函数写出来。
void Merge(int* src, int* dest, int left, int m, int right)
{
int i = left, j = m + 1;
int index = left;
while (i <= m && j <= right)
{
dest[index++] = src[i] <= src[j] ? src[i++] : src[j++];
}
while (i <= m)
{
dest[index++] = src[i++];
}
while (j <= right)
{
dest[index++] = src[j++];
}
}
void MergePass(int* src, int* dest, int n, int s)
{
int i = 0;
for (i = 0; i + 2 * s - 1 <= n - 1; i = i + 2 * s)
{
Merge(src, dest, i, i + s - 1, i + 2 * s - 1);
}
if (n - 1 > i + s - 1)
{
Merge(src, dest, i, i + s - 1, n - 1);
}
else
{
for (int j = i; j < n; ++j)
{
dest[j] = src[j];
}
}
}
void MergeSort(int* nums, int n)
{
if (nums == nullptr || n < 2) return;
int* tmp = new int[n];
int s = 1; // 块的元素个数
while (s < n)
{
MergePass(nums, tmp, n, s);
s += s;
MergePass(tmp, nums, n, s);
s += s;
}
delete[]tmp;
}
分析
对于MergePass函数,for循环每次迭代,i的增值是两个s块的大小减一。这个s是MergeSort函数传递进来的大小,表示本次调用MergePass函数就是按s进行划分的。MergeSort函数内的while循环,调用Pass后让s翻一倍,再调用Pass,使两个数组中元素来回倒换。Pass函数的for循环肯定有终止的时候,那么终止时,可能会出现这样的问题:划分后数组末尾仍然有数据,只是没有满足增量s那么多。所以需要将其考虑进去,判断还有数据时再调用Merge函数一次,如果不满足,那么就拷贝一次,将排序好的数组一次赋值给另一个数组中。