1. 基本思想
在数列排序中,如果只有一个数,那么它本身就是有序的;如果只有两个数,那么一次比较就可以完成排序。也就是说,数越少,排序越容易。那么,如果有一个由大量数据组成的数列,我们很难快速地完成排序,该怎么办呢?可以考虑将其分解为很小的数列,直到只剩一个数时,本身已有序,再把这些有序的数列合并在一起,执行一个和分解相反的过程,从而完成整个数列的排序。
归并排序与快速排序的思想基本一致,唯一不同的是归并排序的基准值是数组的中间元素
快排 Link:[排序算法] 6. 快速排序多种递归、非递归实现及性能对比(交换排序)
合久必分,分久必合~
分治算法离不开递归啊,首先看看分解的递归操作:
void __MergeSort(int array[], int left, int right, int extra[]) {
// 终止条件
if (right == left + 1) {
// 只剩一个数
return;
}
if (right <= left) {
// 区间内没有数了
return;
}
int mid = left + (right - left) / 2;
// [left, mid) [mid, right)
__MergeSort(array, left, mid, extra);
__MergeSort(array, mid, right, extra);
// 左右区间都有序
Merge(array, left, mid, right, extra);
}
如果要排序的区间已经有序了,即满足size == 1
时,直接返回,否则先找到中间位置,对左边部分进行排序,对右边部分进行排序,当满足左右区间都有序的前提了,合并两个有序数组,使整个数组有序。
合并也很简单,设置三个工作指针,分别为left_index
,right_index
,这两个指向两个待排序子序列中待比较的位置,extra_index
指向辅助数组extra[]
待放置元素的位置,比较array[left_index]
与array[right_index]
的大小,将小的赋给extra[extra_index]
,同时相应的指针向后移动。由于分解的递归调用,调用栈调用完毕后即所有的元素处理完毕。最后将辅助数组中元素复制给原数组即可。下面来看个例子,这个来自《趣学算法》-陈小玉老师编写,讲解的很清楚~:
重要:别漏了这点啊
由于分治法涉及到了递归、调用栈等,确实不好理解,建议这个归并排序与快排一起看看,都是较难的排序方法,却刚好有相同之处。
2. 代码实现
2.1 递归实现
// 归并排序-递归(递增)
// 合并两个有序序列
void Merge(int array[], int left, int mid, int right, int extra[]) {
int size = right - left;
int left_index = left;
int right_index = mid;
int extra_index = 0;
while (left_index < mid && right_index < right) {
if (array[left_index] <= array[right_index]) {
extra[extra_index] = array[left_index];
++left_index;
}
else {
extra[extra_index] = array[right_index];
++right_index;
}
extra_index++;
}
while (left_index < mid) {
extra[extra_index++] = array[left_index++];
}
while (right_index < right) {
extra[extra_index++] = array[right_index++];
}
for (int i = 0; i < size; i++) {
array[left + i] = extra[i];
}
}
void __MergeSort(int array[], int left, int right, int extra[]) {
// 终止条件
if (right == left + 1) {
// 只剩一个数
return;
}
if (right <= left) {
// 区间内没有数了
return;
}
int mid = left + (right - left) / 2;
// [left, mid) [mid, right)
__MergeSort(array, left, mid, extra);
__MergeSort(array, mid, right, extra);
// 左右区间都有序
Merge(array, left, mid, right, extra);
}
// 归并排序-递归(递增)
void MergeSort(int array[], int size) {
int *extra = (int *)malloc(sizeof(int)* size);
__MergeSort(array, 0, size, extra);
free(extra);
}
测试数据:int array[] = { 3, 9, 1, 4, 2, 8, 2, 7, 5, 3, 6, 11, 9, 4, 2, 5, 0, 6 };
2.2 优化—非递归实现
// 归并排序(递增)
// 合并两个有序序列
void Merge(int array[], int left, int mid, int right, int extra[]) {
int size = right - left;
int left_index = left;
int right_index = mid;
int extra_index = 0;
while (left_index < mid && right_index < right) {
if (array[left_index] <= array[right_index]) {
extra[extra_index] = array[left_index];
++left_index;
}
else {
extra[extra_index] = array[right_index];
++right_index;
}
extra_index++;
}
while (left_index < mid) {
extra[extra_index++] = array[left_index++];
}
while (right_index < right) {
extra[extra_index++] = array[right_index++];
}
for (int i = 0; i < size; i++) {
array[left + i] = extra[i];
}
}
void MergeSortNor(int array[], int size) {
int *extra = (int *)malloc(sizeof(int)* size);
for (int i = 1; i < size; i = i * 2) {
for (int j = 0; j < size; j = j + 2 * i) {
int left, mid, right;
left = j;
mid = j + i;
right = mid + i;
if (mid >= size) {
// 没有右边的区间 [mid, right)
continue;
}
if (right > size) {
right = size;
}
Merge(array, left, mid, right, extra);
}
}
free(extra);
}
3. 性能分析
时间复杂度
- 最坏 O ( n l o g n ) O(nlogn) O(nlogn)
- 平均 O ( n l o g n ) O(nlogn) O(nlogn)
- 最好 O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度
- O ( n ) O(n) O(n)
排序稳定性
- 稳定
外部排序
- 支持大数据排序,多路归并要记住这个名词23333~
这时间复杂度的推导发现我是没这个习惯,主要原因平时见的太多,都已经默认成习惯了,但却忽略掉了它的推导过程。学习任何算法课程,首先就是学习时间、空间复杂度的计算,尤其是递归的推导时间复杂度,还是很有意思的,在这就先贴上陈小玉老师的推导结果,希望能够给自己一点警示吧:
非递归形式在上面已经实现了,确实是个不错的优化~