算法入门-归并排序 & 逆序对

91 阅读4分钟

前言: 本文章用于介绍一种常用排序——归并排序,并附带讲解如何使用归并排序求解逆序对计数问题。

归并排序

归并排序比较好理解 , 其实就是先分再和。

基本思路:先不断划分一个原始序列,划分到一个子序列只有一个元素了。这样的一个子序列就是天然有序的。 再合 , 合的想法就是子问题解决了,总的问题解决的分治的思想。 如下图:

image.png


总的规则:
1、第一个序列的当前这个待加入新序列的元素和第二个序列当前待加入新序列的元素进行对比谁小就放谁进去,指针后移
2、当某个一个序列的元素都放完了,就停止。
3、但是可能存在一种情况就是剩下一个序列的元素没有放完 , 那么就需要将还有元素剩余的那个序列的元素全部放进新序列中。
4、最后一步就是将新序列的元素copy进原序列,就完成了一趟归并了。
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int a[N] , tem[N];


void merge_sort(int l , int r){
    
    if(l == r) return ;
    
    int mid = l + r >> 1;
    merge_sort(l , mid);
    merge_sort(mid + 1 , r);
    
    int i = l , j = mid + 1, k = 0;
    
    while(i <= mid && j <= r){
        if(a[i] < a[j]) tem[++k] = a[i++];
        else
            tem[++k] = a[j++];
    }
    
    while(i <= mid) tem[++k] = a[i++];
    while(j <= r) tem[++k] = a[j++];
    
    for (int t = 1; t <= k; t ++ )
        a[l + t - 1] = tem[t];          // 注意这里的下标变换
    
}

int main(){
    
    scanf("%d" , &n);
    
    for (int i = 1; i <= n; i ++ ) scanf("%d" , &a[i]);
    
    merge_sort(1 , n);
    
    for (int i = 1; i <= n; i ++ )
        printf("%d " , a[i]);
        
    return 0;
}

逆数对

逆数对 ,其实是基于归并排序的过程。就像是快选一样,基于快速排序。

逆数对的定义: ai > aj 且 i < j , { i , j } ∈ [1 , n]
统计逆数对的过程是发生在合并的时候。对于两个待合并的子序列(下面将直接说成是第一个序列和第二序列)。
如图 :

image.png 这个时候就需要我们进行统计
(sl - i + 1) 其中 sl 是 第一个序列总的元素个数, 就是此时第一个序列与与aj 形成逆数对的元素个数。
然后不断地在这个过程中, 只要有第二个序列放进去元素,就拿第一个序列的剩下元素个数统计一下。加入到我们的逆数对个数里面。

count+=(sli+1) count += (sl - i + 1)

为什么这么做是合法的呢 ? 因为此时两个序列有序,然后我们按顺序将两个序列未加入新的序列(右边那个有指针k的序列) 。那具体按什么顺序呢?其实就是两个序列中未加入新序列中最小的那个元素 比如 第一个序列元素: 1 4 5 7 第二个序列元素: 1 2 3 7

先将 第一个序列的 1 加入
先将 第一个序列的 1 加入 再将 第二个序列的 1 加入

这里就要统计一次了 , 4 5 7 这三个和 1 形成逆序对. 逆序对个数 (4 - 2 + 1) = 3
再将 第二个序列的 2 加入 统计逆数对个数加上 4 5 7 , (4 - 2 +1) = 3
再将 第二个序列的 3 加入 同样的 (4 - 2 + 1) = 3 然后就将第一个序列的 4 5 7 全部放入新序列 , 再将 第二个序列的 7 放入新序列
再回溯回去 , 回溯完所有(也就是合) 就完成了

新序列 : 1 1 2 3 4 5 7 7

参考代码如下:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long ll;
const int N = 100010;

int n;
ll cnt;
int a[N] , tem[N];

void merger(int l , int r){
	
	if(l == r)
		return ;
	
	int mid = l + r >> 1;
	
	merger(l , mid);
	merger(mid + 1 , r);
	
	int i = l , j = mid + 1;
	
	int k = l;
	while(i <= mid && j <= r){
		if(a[i] <= a[j]) tem[k ++] = a[i ++];
		else tem[k ++] = a[j ++] , cnt += mid - i + 1;
	}
	
	
	while(i <= mid) tem[k ++] = a[i ++];
	while(j <= r) tem[k ++] = a[j ++];
	
	for (int t = l; t <= r; t ++)
		a[t] = tem[t];
	
}

int main(){
	
	scanf("%d" ,&n);
	for (int i = 1; i <= n; i ++) scanf("%d" , &a[i]);
	
	merger(1 , n);
	
	printf("%lld" , cnt);
	
	return 0;
}