目录
[1. 归并排序的基本步骤](#1. 归并排序的基本步骤)
归并排序是一种高效的排序算法,采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序的基本思想是将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
一、递归实现归并排序
1. 归并排序的基本步骤
- 分解:将数组分解成两个较小的子数组,直到子数组的大小为1。
- 递归进行排序并合并:递归地对子数组进行排序,并将已排序的子数组合并成一个大的有序数组,直到合并为1个完整的数组。
2.动图演示
3.基本思路
归并排序是采用分治法的一个非常典型的应用。其基本思想是:将已有序的子序合并,从而得到完全有序的序列,即先使每个子序有序,再使子序列段间有序。
那么如何得到有序的子序列呢?当序列分解到只有一个元素或是没有元素时,就可以认为是有序了,这时分解就结束了,开始合并。
4.代码
void _MergSort(int* a, int left,int right,int* tmp)
{
if (left >= right)
return;
int mid = left + (right - left) / 2;
_MergSort(a, left, mid, tmp);
_MergSort(a, mid+1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
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++];
}
for (int j = 0; j <= right; j++)
{
a[j] = tmp[j];
}
}
void MergSort(int* a, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
_MergSort(a,0, n-1, tmp);
free(tmp);
}
二、非递归实现
归并排序的非递归算法并不需要借助栈来完成,我们只需要控制每次参与合并的元素个数即可,最终便能使序列变为有序:
1.部分代码
void MergSortNonF(int* a, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int j = 0; j < n; j += 2 * gap)
{
//*****注意边界处理*******
int begin1 = j, end1 = j + gap - 1;
int begin2 = j + gap, end2 = j + gap * 2 - 1;
printf("[%d %d] [%d %d]", begin1, end1, begin2, end2);
int i = j;
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++];
}
}
printf("\n");
memcpy(a, tmp, n * sizeof(int));
gap *= 2;
}
free(tmp);
tmp = NULL;
}
2.代码分析
这是一个有问题的代码,当我们存放的数据是2的次方时,可以正常排序,为了便于观察,我们把它的下标给打印下来
如果我们存放的数据不是2的次方个就会出现一些越界。
情况一:第二组部分越界
当最后一个小组进行合并时,第二个小区间存在,但是该区间元素个数不够gap个,这时我们需要在合并序列时,对第二个小区间的边界进行控制。
情况二:第二组全部越界
当最后一个小组进行合并时,第二个小区间不存在,此时便不需要对该小组进行合并。
情况三:第一组end1越界
当最后一个小组进行合并时,第二个小区间不存在,并且第一个小区间的元素个数不够gap个,此时也不需要对该小组进行合并。(可与情况二归为一类)
只要把控好这三种特殊情况,写出归并排序的非递归算法便轻而易举了。
修正后代码:
void MergSortNonF(int* a, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int j = 0; j < n; j += 2 * gap)
{
//*****注意边界处理*******
int begin1 = j, end1 = j + gap - 1;
int begin2 = j + gap, end2 = j + gap * 2 - 1;
//第一组越界
if (end1 >= n)
{
printf("[%d %d]", begin1,n-1);
break;
}
//第二组全部越界
if (begin2 >= n)
{
printf("[%d %d]", begin1, end1);
break;
}
//第二组部分越界,越界之前的那一部分依然要归
if (end2 >= n)
{
//修正end2
end2 = n - 1;
}
printf("[%d %d] [%d %d]", begin1, end1, begin2, end2);
int i = j;
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+j, tmp+j, (end2-j+1)* sizeof(int));
}
printf("\n");
//memcpy(a, tmp, n * sizeof(int));
gap *= 2;
}
free(tmp);
tmp = NULL;
}
归并过程打印
性能分析
复杂度分析
- 时间复杂度:归并排序的时间复杂度为O(n log n),其中n是数组的长度。这主要是由于归并排序将问题分解为两个子问题(分解),递归地解决它们(递归),然后将解决方案合并(合并)。
- 空间复杂度:归并排序的空间复杂度为O(n),其中n是数组的长度。这是因为归并排序在合并过程中需要与原数组同样大小的额外空间来存放临时数组。
归并排序是稳定的排序算法,即相等的元素在排序后的序列中仍然保持原来的顺序。这个特性在某些应用中非常重要。
如有错误,劳烦各位指正