天天看点

最简练写法实现归并排序【C++代码】

目录

归并排序

C++代码实现

稳定性分析

求逆序对

归并排序

归并排序的思想是分治。其过程也就是“分”和“治”的两个步骤。

对于一个序列,我们先将其一分为二,分别排好序,然后再合并这两个有序数列。

对于被平分成的两部分怎么排序呢?再一分为二,递归下去,直至分成单个元素时,即自然有序了。

最简练写法实现归并排序【C++代码】

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++; 
		}
	}
	for (int k=l; k<=r; k++) a[k]=t[k]; //复制
}
           

继续阅读