一.归并排序原理
递归划分(递归的出口为子区间元素个数为1)
归并排序运用了 分治 的思想,将一个无序的数组按照中点 mid 划分成左右两个区间:
[ left , mid]和[ mid+1, right ] ,直到区间内元素个数为 1 的时候,此时这个子区间一定是有序的(毕竟只有一个元素)
回溯合并(持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束)
最后再把这些区间合并,这样保证了每一步之后的子区间一定有序
如下图是归并排序的简单实现过程
根据观察可知,形成了深度为 的递归树,每次要执行n次操作完成子区间的排序,综上,该算法的时间复杂度为
二.归并排序的C++代码实现
#include <bits/stdc++.h>
using namespace std;
int nums[] = {7, 3, 2, 4, 6, 1, 5, 0};
// 递归实现的归并排序
void msort(int l, int r) {
int mid = (l + r) / 2; // 找到中间点
if (l >= r) return; // 如果区间内只有一个元素,直接返回
msort(l, mid); // 递归排序左半部分
msort(mid + 1, r); // 递归排序右半部分
int tempSize = r - l + 1; // 计算临时数组的大小
int temp[tempSize]; // 创建临时数组存放合并后的有序数组
int i = l, j = mid + 1, t = 0; // 初始化指针
//i 是指向左半区间的指针
//j 是指向右半区间的指针
//t 是指向临时数组的指针
// 合并两个有序数组
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) {
temp[t++] = nums[i++]; // 将较小值复制到临时数组
} else {
temp[t++] = nums[j++]; // 将较小值复制到临时数组
}
}
// 复制剩余的元素(如果有的话)
while (i <= mid) temp[t++] = nums[i++];
while (j <= r) temp[t++] = nums[j++];
// 将临时数组中的元素复制回原数组
//相当于在nums中拿出来一段排序
//再将排序好的那一段放回去
for (int i = 0; i < tempSize; i++) {
nums[l + i] = temp[i];
}
}
int main() {
int len = sizeof(nums) / sizeof(nums[0]); // 计算数组长度
msort(0, len - 1); // 调用归并排序函数
// 打印排序后的数组
for (int i = 0; i < len; i++) {
cout << nums[i] << " ";
}
return 0;
}
三.归并排序的原理在题目中应用(洛谷P1908 逆序对)
P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1908根据归并排序的原理,我们可以发现此算法和求逆序对将有关联
因为当将这些数组在回溯合并的阶段,每一部分在合并成更长的区间前是有序的,所以可以运用这个性质进行求解。
比如这一部分,3 7和2 4都是有序的,对应的指针 指向左区间,
指向右区间,满足
<
了,只要再nums[ i ] > nums [ j ]即可构成一个逆序对。然后 i 指向 3,j 指向 2,此时贡献出了两个逆序对(3和2 7和2),因为3 7是有序的,较小数在前面(3),3可以与右区间构成逆序对,更大的数 7 自然也可以由此得出规律,当 i 所指向的左半边的数大于 j 所指向的右半边的数时,贡献出 (mid - i + 1 )个逆序对(mid是左区间最右侧对应下标,i 是左区间对应的当前元素的下标,+1是表示个数,消除索引偏移),完成这一部分后,将右区间中的 2 存入临时数组中,j 指针指向 4 ,然后进行下一步操作。(这便是归并排序中的回溯合并操作,逐步把最小的数字取出来再合并)
如果对应的不是nums[ i ] >nums[ j ],而是nums[ i ] <= nums[ j ],说明左区间中的最小的数和右区间的数构成不了逆序对,那么需要左区间出来一个更大的数,那么此时 i 指向的这个左区间内的较小的数应该放到临时数组中,i 指针指向左区间的下一位。(我们发现这一步也是把较小的元素存入临时数组,这正是归并排序的操作)
!!!:上述操作是在 i 没超过左区间边界 且 j 没超过右区间边界时进行的(i<=mid&&j<=r)
但是还有时候,当递归划分时,分左右区间,是将一个奇数平分,所以左右区间数不相等,对比之后可能会有剩余,或者就算左右区间数字个数相等时,也会有剩余,此时还需要对剩余的元素进行判断和操作
(依次看 i <=mid 和 j<=r是否成立,因为这个是在i<=mid&&j<=r对应的while循环之后进行的,那么它的作用便是看左右区间内是否有剩下的元素)
AC代码如下
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int Maxn = 5e5;
int n;
ll nums[Maxn+5];
ll b[Maxn+5];
ll ans = 0;
void msort(int l,int r){
int mid = (l+r)/2;
if(l==r) return ;
//只有一个元素了 此时递归结束
else{
msort(l,mid);//递归划分左半边
msort(mid+1,r);//递归划分右半边
}
int i = l ,j = mid+1 , t = l;
//i指向左半边 j指向右半边
//t代表临时数组的下标
while(i<=mid && j<=r){
if(nums[i]>nums[j]){
ans+=mid-i+1;
//因为是排好序再比较的,所以如果
//nums[i]都比nums[j]大,那么左半边所有元素中
//比nums[j]大的有 mid(左半边总个数)-i(当前元素下标)+1(索引偏移)
b[t++] = nums[j++];
}else{
//b中存入较小元素
//最后b中的元素都是有序的,再把b中元素复制到nums中
//这样就对应上面所说的"排好序再比较"
b[t++] = nums[i++];
}
}
//当左半边或者右半边都比较完了 另一边还有剩余的
while(i<=mid)//左半边还有剩余
{
b[t++] = nums[i++];
}
//右半边还有剩余
while(j<=r){
b[t++] = nums[j++];
}
//排序后的结果放到nums中
for(int i =l;i<=r;i++){
nums[i] = b[i];
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
//输入
cin>>n;
for(int i=1;i<=n;i++){
cin>>nums[i];
}
msort(1,n);
cout<<ans;
return 0;
}