目录
归并排序
归并排序的思想是分治。其过程也就是“分”和“治”的两个步骤。
对于一个序列,我们先将其一分为二,分别排好序,然后再合并这两个有序数列。
对于被平分成的两部分怎么排序呢?再一分为二,递归下去,直至分成单个元素时,即自然有序了。
C++代码实现
网上很多代码会把归并排序的分和治两个环节分开写,这样可能好理解,但是函数太多了,冗余。
我更习惯将其合并起来写,看起来更简洁。
int t[50005]; //用于暂时存放合并的元素的空数组
void merge_sort(int* a,int l,int r){
//分
if (l==r) return; //递归出口
int mid=(l+r)/2;
merge_sort(a,l,mid);
merge_sort(a,mid+1,r);
//治
int i=l;
int j=mid+1;
for (int k=l; k<=r; k++){ //合并
if ( (j>r) || (i<=mid && a[i]<=a[j]) ) {
t[k]=a[i];
i++;
} else {
t[k]=a[j];
j++;
}
}
for (int k=l; k<=r; k++) a[k]=t[k]; //复制
}
函数调用 void merge_sort(int* a,int l,int r),传入参数分别为 待排序数组,以及左右边界l,r。
要注意当数组用vector时,需要传入的是引用,也就是说,必须修改原数组。
代码分成两大部分
第一部分是“分”:
先计算出区间[l,r]的中点mid,然后一直递归下去。
递归出口是 (l==r),也就是一个数的时候,自然是有序的,也就可以执行第二部分的“治”的步骤。
第二部分是“治”:
对于排好序的两个数组,进行合并。
合并的元素需要暂时放到一个空的数组,最后再复制回来。t就是那个暂时的空数组。
具体合并的方法是用两个指针 i ,j 分别指向待合并的两部分。其中判断a[i]<=a[j]的条件需要特别注意,要考虑周全。
if ( (j>r) || (i<=mid && a[i]<=a[j]) ) 的意思是: 如果右区间越界(也就是合并完了),或者左区间没越界并且满足小于条件,才选择左区间的数。
稳定性分析
归并排序是稳定的吗?答:上述版本是稳定的。
我们分析一下,在合并两个有序部分时,当 a[i]==a[j] ,我们总是先取 a[i] ,而指针 i 是永远小于 j 的,所以是稳定的。
而如果判断条件 if ( (j>r) || (i<=mid && a[i]<a[j]) ) 也就是缺少这个等号的话,就不是稳定的。
求逆序对
利用归并排序求逆序对只需要加一个统计变量即可。
在合并两个有序部分的过程中,当选择了左区间的指针 i 时,我们查看下这时的 j 的大小,计算出已经选择了几个数, 也就是 j-(mid+1)。
因为 i 是小于 j 的,但是这几个数已经被选择就说明 a[i]>a[j] ,所以有几个数,就是有几个逆序对。
int ans=0; //用于统计逆序对
int t[50005]; //用于暂时存放合并的元素的空数组
void merge_sort(vector<int>& a,int l,int r){
//分
if (l==r) return; //递归出口
int mid=(l+r)/2;
merge_sort(a,l,mid);
merge_sort(a,mid+1,r);
//治
int i=l;
int j=mid+1;
for (int k=l; k<=r; k++){ //合并
if ( (j>r) || (i<=mid && a[i]<=a[j]) ) {
t[k]=a[i];
i++;
ans+=j-(mid+1); //统计逆序对
} else {
t[k]=a[j];
j++;
// ans += mid - i + 1; //逆序对也可以在此统计
}
}
for (int k=l; k<=r; k++) a[k]=t[k]; //复制
}
逆序对放在else分支好像更好理解, 当前要选择 j,但是在顺序上,当前的 i ~ mid 都排在 j 前面,也就是说 这几个数都是逆序对。