七月挑战一个月重刷完Y总算法基础题,并且每道题写详细题解
进度:(3/106)
归并排序的思想也是分而治之
归并优点:速度稳定,排序也稳定
排序也稳定(数组中有两个一样的值,排序之后他们的前后顺序不发生变化,我们就说这个排序是稳定的)
缺点:比起快排,空间复杂度更高
使用场景:数据量巨大,寻求稳定
速度:稳定n(logn)
思想思路
第一步
找分界点(mid,中间值)
第二步
先递归排序两边
第三步
将两个有序数组合二为一(归并)
有序数组合并实现思路(归并)
利用双指针
准备三个数组和两个指针
三个数组其中两个是有序数组,一个是准备用来存储的
cpp
#include<iostream>
#include<algorithm>
using namespace std;
int n,arr[10000010],sum[10000010];
void m_sort(int arr[],int l,int r){
//到达边界,退出
if(l>=r)return;
//找到分界点
int mid=(l+r)>>1;
//利用分界点分成两个数组,递归两个数组
m_sort(arr,l,mid),m_sort(arr,mid+1,r);
//上面递归结束的,一定是两段有序数组
//遍历两个数组,把两个数组的值,按照归并顺序放到第三暂存个数组
//这个两个数组,其实指的是一个数组上的两个有序区间
//每次放入之前,先进行比较两个数组里的指针指向的数
//谁指向的数组元素小,先放谁进入第三个暂存数组
//然后那个数组的指针指向下个元素,循环往复,直至其中一个数组遍历完毕
int i=l,j=mid+1,k=0;
while(i<=mid&&j<=r){
if(arr[i]<arr[j])sum[k++]=arr[i++];
else sum[k++]=arr[j++];
}
//上面的循环只保证其中一个数组遍历完毕
//下面的两个循环,把未遍历完毕的那个数组剩下的元素,添加进暂存数组
while(i<=mid)sum[k++]=arr[i++];
while(j<=r)sum[k++]=arr[j++];
//把暂存数组,放入原数组,替代原数组元素,完成对arr[l-r]的排序
for(int i=l,j=0;i<=r&&j<=k; i++,j++){
arr[i]=sum[j];
}
}
int main(){
cin>>n;
for(int i=0;i<n;i++)cin>>arr[i];
m_sort(arr,0,n-1);
for(int i=0;i<n;i++)cout<<arr[i]<<' ';
return 0;
}
归并排序已完成
感悟思想:归并排序思想也是分而治之,但是和快排不同的是,快排是把大的放左边,小的放右边后
越递归,数组有序性越强,先分好再递归,归并排序是先递归,再排序
先确定下标的中间,中位点的下标,也就是数组长度是十的话,中位点的下标就是5
然后直接就分别递归中位点的左边和右边
直到数组变成一位的时候,递归停止,开始回溯
回溯到数组是两位的时候,现在的中位点左右两边,都是有序数组,只需要用两个指针,各指向两个数组
把小的,先放入新数组即可,后面同理,回溯时,中位点两边的数组一定都是有序数组
把两个有序数组合并成新数组,只需要使用双指针,把指向的数组的值较小的那个放入新数组,然后右移指针即可
直到回溯完毕,最后一次合并,原来的无序数组就排好了
有人叫回溯是回归,回归+合并,归并排序。
经典题:逆序对的数量
题目
思路
用分治的思想,mid将数组分成=两段
则逆序对只有三种可能:
红色,同时在左半边,绿色,同时在右半边,黄色,一半在左一半在右
我们先解决黄色情况
假设数组a[n],数组b[n]都是两个有序数组,且a[n]就是mid前0到mid的子串,b[n]是mid后mid+1到r的子串
使用双指针算法,i,j分别指向数组第一个元素
a[i]和b[j]逐位比较
一旦b[j]小于a[i]中一个数,那说明,i到mid所有数,都和b[j]组成逆序对
因为a[]数组有序,a[i]到a[mid]的数必大于a[i],也就必大于b[j],也就必和b[j]组成逆序对
那我们只需要在两者归并排序时,累加记录res+=mid-i+1;
res便是这两个有序数组里的逆序对个数
(mid就是a[n]的长度,-i是减去已经指向过的数,+1是因为i从0开始)
黄色情况可以解决了,那只需要把所有情况都递归分割成黄色情况,所有情况都解决了
结论,只需要在归并排序时,累加记录arr[j]放在arr[i]前的情况时mid-(i+1)的值
代码
这里只对改动部分注释,看归并排序点这归并排序
cpp
#include<iostream>
using namespace std;
//数可能很大,使用long long
typedef long long ll;
int a[100010],s[100010];
ll add(int a[],int l,int r){
if(l>=r)return 0;
int mid=l+r>>1;
//res接收值
ll res=add(a,l,mid);
//第二段的加上第一段的
res+=add(a,mid+1,r);
int k=0,i=l,j=mid+1;
while(i<=mid&&j<=r){
if(a[i]<=a[j])s[k++]=a[i++];
else{
//一旦a[j]需要放在a[i]前面
//说明a[j]和a[i]以及a[i]到a[mid]所有数,都组成逆序对
//mid-(i+1)的结果,就是a[i]到a[mid]的数的个数(+1是因为i从0开始)
//每次出现这个时,把这个结果累加即可
res+=mid-i+1;
s[k++]=a[j++];
}
}
while(i<=mid)s[k++]=a[i++];
while(j<=r)s[k++]=a[j++];
for(int i=0,j=l;j<=r;j++,i++){
a[j]=s[i];
}
//处理完这段l-r,把res结果返回出去,处理下一段l-r
return res;
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
ll res=add(a,0,n-1);
cout<<res<<endl;
return 0;
}