更新
2024.10.12 初版
2024.10.13 更新关于merge()函数中对于temp数组返回值到a数组的理解,纠正错误。
前言
上一期介绍了快速排序的基本用法,包括了快速排序的写法,以及C++库中sort()函数的调用,它使得我们对于数据的处理变得更快了,快速排序有着不需要开辟额外空间,运行速度快等特点,但它本身有着不稳定的特点,遇到极端的数据时间复杂度容易从O(nlogn)变为O(n^2),使得运行速度变慢,那么今天让我们来介绍一个性格比较稳定的排序方法——————归并排序。
上一期链接:那个更优秀的助手:快速排序(小白专用0v0) - 掘金 (juejin.cn)
归并排序
简介
归并排序(Merge Sort)是一种基于分治法的高效、稳定的排序算法,主要用于将一个无序的数组排序。它通过将数组反复拆分成更小的子数组,直到每个子数组只有一个元素,然后将这些子数组逐步合并为有序数组,最终形成排序好的整体数组。
核心思想
此算法的核心思想和前面的快速排序一样,也是分而治之,将一整个数组分成两个数组,无限划分到每个数组只包含一个数据(因为只有含有一个数据的数组是有序的),之后再使它与相邻的数组合并排序,直到所有数组被合并完毕。
举个栗子(0v0)
让我们直接通过一个栗子来了解一下它的运作过程吧!
假设有如下一组数组:
分离
要通过归并排序,我们需要将它一分为二,直到分成每个数组都含有一个数据为止。
(以分割后左侧数据为例)
可以看到最终一个大数组被分成了一个个只含有一个数的小数组,即a[0]=8,a[1]=3,a[2]=5,a[3]=7......
右侧a[4]-a[7]同理。这样“分家”就完成了。
合并
随后就是合并的环节,分家的a[0]和a[1]相比较大小,小的在前大的在后,两两合并,之后a[2]和a[3]合并,再由a[0]-a[3]合并。
a[4]-a[7]同理,最后一整个数组都会变成有序的。
源码:
void merge(int a[], int temp[], int left, int mid, int right) {
int l_pos = left;
int r_pos = mid + 1;
int t_pos = left;
while (l_pos <= mid && r_pos <= right) {
if (a[l_pos] < a[r_pos]) {
temp[t_pos++] = a[l_pos++];
} else
temp[t_pos++] = a[r_pos++];
}
while (l_pos <= mid) {
temp[t_pos++] = a[l_pos++];
}
while (r_pos <= right) {
temp[t_pos++] = a[r_pos++];
}
while (left <= right) {
a[left] = temp[left];
left++;
}
} //合并使得数据有序所用算法
void mergesort(int a[], int temp[], int l, int r) {
int mid = (l + r) / 2;
if (l < r) {
mergesort(a, temp, l, mid);
mergesort(a, temp, mid + 1, r);
merge(a, temp, l, mid, r);
} else
return;
} //分离数组所用算法
在这一段的合并代码中,我们将被分离的数组两两比较,在两个数组中各设一个指针l_pos和r_pos,小的数字先进入temp数组,大的后进入temp数组,达到排序合并的目的,当我们这么做之后,其中一组的指针一定会超出界限,而另一组指针会停留在剩余的数字上,所以我们利用两个while循环来使得a数组剩余的数据全部进入temp数组。
纠错:temp数组的值返回给a数组这一步不可省略。
在这之后,要用left和right使得temp数组的值返回到a数组,使得a数组有序。这一步不可省略!!!! 有的时候直接输出temp[]数组也可能是正确的,但会有出错的可能。 举例 : a[]={3,5,1,2,4,6},如果直接输出temp[]数组,答案会为2,3,4,5,1,6。
while (left <= right) {
a[left] = temp[left];
left++;
}
省略了如上代码后,我们每一次仅仅将temp数组里的数字排了序,但到最终由于a数组没有被覆盖成有序的数组,最后合并我们仍然用的3 5 1 2 4 6的顺序,所以答案是错误的,正常情况下,应该是每一次调用都将temp数组排序好的数字返回给a数组,在最后一次排序时,a[]应该为{1,3,5,2,4,6},再进行merge()后就会得到a[]={1,2,3,4,5,6}。
我们分离数组的目的就是让它一步步由无序到有序,如果省略了覆盖a数组的这一步,我们做的分组其实毫无意义。
递归分析
void mergesort(int a[], int temp[], int l, int r) {
int mid = (l + r) / 2;
if (l < r) {
mergesort(a, temp, l, mid);
mergesort(a, temp, mid + 1, r);
merge(a, temp, l, mid, r);
} else
return;
} //分离数组所用算法
最开始带入 int a[],int temp[],和 l,r,l=0,r=7(数组最后一个元素的下标)
进入函数后执行第一句mergesort(a,temp,l,mid) l=0,mid=3
符合l<r的条件,于是再执行mergesort(a,temp,l,mid). l=0,mid=1
执行mergesort(a,temp,l,mid). l=0, mid=0
由于此时不符合条件于是返回上一函数。继续执行上一个函数的语句。
继续返回执行上个函数的语句。
执行merge函数,开始在a[0]和a[1]的范围内排序合并。
void merge(int a[], int temp[], int left, int mid, int right) {
int l_pos = left;
int r_pos = mid + 1;
int t_pos = left;
while (l_pos <= mid && r_pos <= right) {
if (a[l_pos] < a[r_pos]) {
temp[t_pos++] = a[l_pos++];
} else
temp[t_pos++] = a[r_pos++];
}
while (l_pos <= mid) {
temp[t_pos++] = a[l_pos++];
}
while (r_pos <= right) {
temp[t_pos++] = a[r_pos++];
}
while (left <= right) {
a[left] = temp[left];
left++;
}
} //合并使得数据有序所用算法
a[0]与a[1]合并后:
merge函数执行完毕,返回上一函数。
执行mergesort(a,temp,mid+1,r),
mid+1=2,r=3.
执行mergesort(),由于l=mid,mid+1=r,所以在执行时全部返回到当前函数,继续向下执行
merge(a,temp,l,mid,r)
l=2,mid=2,r=3
a[2],a[3]成功排序合并。
之后返回上一级函数
进行a[0]-a[3]的合并。
合并完成后返回到上一级函数
继续执行mergesort(a,temp,mid+1,r),将a[4]-a[7]排序合并,最后a[0]-a[7]排序合并。整个归并的过程就此结束。
归并排序时间复杂度
归并排序的优点就是比较稳定,适合处理大量数据,其时间复杂度一直是O(nlogn),每次割一半数据分组的时间复杂度为O(logn),排序合并所用时间复杂度为O(n),另外和快排不一样的是,归并排序还需要开辟出一段空间来存放数据。所以其总时间复杂度为O(nlogn),即分logn次数组,每次要排序n次,T(n)=O(n)+O(n)+....(logn次)。
结尾
今天的内容就是这样啦,归并排序的重点就在于其递归的使用,对于小白来说算是一个稍微难理解的一个点呢,建议去Acwing或者其他平台多加练习,也可以通过写文章的方式巩固自己的理解。 如果本期内容对你有帮助,那就点个赞吧!如有错误,欢迎大家指正,一起进步!
素材来源:排序算法:归并排序【图解+代码】_哔哩哔哩_bilibili
二路归并排序算法 递归手动执行过程_哔哩哔哩_bilibili
Acwing
源码:
#include <bits/stdc++.h>
using namespace std;
const int n = 1e6 + 7;
int a[n];
int temp[n];
void merge(int a[], int temp[], int left, int mid, int right) {
int l_pos = left;
int r_pos = mid + 1;
int t_pos = left;
while (l_pos <= mid && r_pos <= right) {
if (a[l_pos] < a[r_pos]) {
temp[t_pos++] = a[l_pos++];
} else
temp[t_pos++] = a[r_pos++];
}
while (l_pos <= mid) {
temp[t_pos++] = a[l_pos++];
}
while (r_pos <= right) {
temp[t_pos++] = a[r_pos++];
}
while (left <= right) {
a[left] = temp[left];
left++;
}
}
void mergesort(int a[], int temp[], int l, int r) {
int mid = (l + r) / 2;
if (l < r) {
mergesort(a, temp, l, mid);
mergesort(a, temp, mid + 1, r);
merge(a, temp, l, mid, r);
} else
return;
}
int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
mergesort(a, temp, 0, n - 1);
for (int i = 0; i < n; i++) {
printf("%d ", a[i]);
}
printf("\n");
for (int i = 0; i < n; i++) {
printf("%d", temp[i]);
}
}
#include <bits/stdc++.h>
using namespace std;
const int n = 1e6 + 7;
int a[n];
int temp[n];
long long int res = 0;
void merge(int a[], int temp[], int left, int mid, int right) {
int l_pos = left;
int r_pos = mid + 1;
int t_pos = left;
while (l_pos <= mid && r_pos <= right) {
if (a[l_pos] <= a[r_pos]) {
temp[t_pos++] = a[l_pos++];
} else {
temp[t_pos++] = a[r_pos++];
res += (mid - l_pos + 1);
}
}
while (l_pos <= mid) {
temp[t_pos++] = a[l_pos++];
}
while (r_pos <= right) {
temp[t_pos++] = a[r_pos++];
}
while (left <= right) {
a[left] = temp[left];
left++;
}
}
void mergesort(int a[], int temp[], int l, int r) {
int mid = (l + r) / 2;
if (l < r) {
mergesort(a, temp, l, mid);
mergesort(a, temp, mid + 1, r);
merge(a, temp, l, mid, r);
} else
return;
}
int main() {
int N;
scanf("%d", &N);
for (int i = 0; i < N; i++) {
scanf("%d", &a[i]);
}
mergesort(a, temp, 0, N - 1);
printf("%lld\n", res);
//求逆序对的算法
}