归并排序(Merge Sort)
基本思想
归并排序(MERGE-SORT)是一种基于归并操作 的高效排序算法,采用经典的 分治法(Divide and Conquer) 策略:
- 分解(Divide):将待排序的数组递归地分成两个子数组,直到每个子数组只包含一个元素(天然有序)。
- 解决(Conquer):递归地对两个子数组进行排序。
- 合并(Combine) :将两个已排序的子数组合并成一个有序数组。
核心操作是"二路归并"------将两个有序序列合并为一个有序序列。根本逻辑与二叉树的后序遍历相似。
核心步骤
- 分割 :将长度为
n的数组不断二分,直到子数组长度为 1。 - 归并:从底层开始,两两合并相邻的有序子数组,逐步构建更大的有序数组。
- 重复:持续向上归并,最终得到整个数组的有序结果。
示例(伪代码逻辑):
text
mergeSort(arr, left, right):
if left < right:
mid = (left + right) // 2
mergeSort(arr, left, mid) // 排序左半部分
mergeSort(arr, mid+1, right) // 排序右半部分
merge(arr, left, mid, right) // 合并左右两部分
递归归并
c
//归并排序
void _MergeSort(int* arr,int*temp, int begin, int end)
{
//归的过程(递归到单个元素1)
if (begin == end)return;
int mid = (begin + end) / 2;
_MergeSort(arr, temp, begin, mid);
_MergeSort(arr, temp, mid + 1, end);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;//// i 是 temp 中当前写入位置的索引,从 begin 开始,确保只覆盖 [begin, end] 区间
//并的过程(从单个元素开始合并)// 在合并前,左右子数组 [begin, mid] 和 [mid+1, end] 已分别被递归排序为有序
while (begin1 <= end1 && begin2 <= end2)//相当于将被分为两部分的数组放在一起进行排序,也就是回到"根",按顺序写入temp里,将该数组的划分看成逻辑树也就是从底层开始将一个根的两个子树合并在一起排序,总体上与二叉树的后序遍历相似,是左右根的操作顺序,左右子树递归完毕后排序根。
{
if (arr[begin1] > arr[begin2])//三个if将arr中从begin到end的数按序排列好,排到temp中对应的位置。
{
temp[i++] = arr[begin2++];
}
else if (arr[begin1] < arr[begin2])
{
temp[i++] = arr[begin1++];
}
else if(arr[begin1] == arr[begin2])//// 注意:相等时取左半部分元素(arr[begin1]),保证排序的稳定性(stable),因为排序顺序一般都是从左至右。
{
temp[i++] = arr[begin1++];
}
}
while (begin1 <= end1)//上一个循环结束后,左部分或者右部分会有一个提前排完,此时将未排完的有序数组直接插入temp后面的空位置即可。未排完的部分一定是有序的,因为递归时会递归到底层,也就是全是单元素的情况,再往上访问根进行排序,所以执行到某一次合并的时候,左部分一定有序,右部分也一定有序只是左+右此时相对无序,所以其中一个排列完后,剩下的一定是有序。
{
temp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
temp[i++] = arr[begin2++];
}
memcpy(arr + begin, temp + begin, sizeof(int) * (end - begin + 1));
}
// [0,3]
// / \
// [0,1] [2,3]
// / \ / \
//[0,0] [1,1] [2,2] [3,3]递归示意图,元素只有一是无需排序,直接收束。这里为了便于观察使用了有序数组,无序数组也是一样的,合并之后,各个根的左右数组自身内部都是有序的。
void MergeSort(int *arr,int n)
{
int* temp = (int*)malloc(sizeof(int) * n);
if (temp == NULL)
{
perror("malloc error");
return;
}
_MergeSort(arr,temp, 0, n - 1);
free(temp);
}
递归实现的核心就是利用递归实现数列伪二叉树的后序遍历,通过"递"利用mid,begin,end不断划分数列,直至将数列划分成若干单个元素后开始"归",在"归"时,也就是在访问"根"时,对其两棵子树进行合并排序,并将他复制到新的数组中的相应位置。
begin和end永远是当前排序部分的首尾,通过递归函数的参数控制
非递归实现
c
void CyclicMergeSort(int *arr,int n)
{
int* temp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)//gap是对于序列的分组中每一组的元素数,从1开始,直到n时代表合并完成。gap==n时,已经分组完毕,故而不需要包含
{
for (int i = 0;i < n;i+=2*gap)//非递归实现的本质是直接送递归的底层开始循环,将所有数从若干组不断由二并一,最终合成一组。因为每次循环完毕,都已经有两组数被合并了,所以下次应该从第三组开始合并
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = i;//用于储存此时应该存入temp的位置,递归是现实是直接用i标记,是因为当时i也是专门定义的索引值,但是这里i标记了每次归并时的开始索引,所以不能直接改变,要用其他的变量专门存储。
//begin2越界,所以右组无合法值不需要排序
if (begin2 >= n)break;//begin2大于等于n时,后序的访问会越界。在[begin1,end1][begin2,end2]中,有三种越界可能,begin2越界,end2越界,end1越界。如果end1越界,begin2必然越界,所以这两种情况都可以归为一种,在begin2越界时停止归并即可。对于非2的指数的元素数列,一定不可能恰好分完,比如[1,11],第一次归并时11为[10,10],需与[11,11]下标的元素合并,但begin2越界,所以没有执行合并。
//对于右组有部分下标合法,即begin2未越界,end2越界,需要进行边界修正。
if (end2 >= n)end2 = n - 1;//第二次归并时下标为10作为其中一组归并的begin2,此时end2下标为11,越界,为了使得此时在begin2和end2之间的元素得以进入排序,手动调整end2的界限范围,使其成为合法索引。
while (begin1 <= end1 && begin2 <= end2)
{
if(arr[begin1] > arr[begin2])
{
temp[j++] = arr[begin2++];
}
else if (arr[begin1] < arr[begin2])
{
temp[j++] = arr[begin1++];
}
else if (arr[begin1] == arr[begin2])
{
temp[j++] = arr[begin1++];
}
}
while (begin1 <= end1)
{
temp[j++] = arr[begin1++];
}
while (begin2 <= end2)
{
temp[j++] = arr[begin2++];
}
memcpy(arr + i, temp + i, sizeof(int) * (end2 - i + 1));//for 循环中的每一组处理是独立的,它们操作的数组区间不重叠,因此不会相互覆盖或干扰。
}
gap *= 2;//gap是每一序列的元素数,合并后,由1->2,所以gap是呈2的指数级递增的。
}
free(temp);
}
非递归归并排序采用"自底向上"的策略,通过迭代方式模拟递归的"归"过程。它不依赖函数调用栈,而是手动控制合并的粒度:从长度为 1 的子数组开始,逐轮将相邻的有序段两两合并,每次将子数组长度(gap)翻倍,直到覆盖整个数组。