归并排序 (BM20 数组中的逆序对)

61 篇文章 2 订阅
22 篇文章 1 订阅

一 、算法实现

A1: 首先回答一下第一个问题,为什么归并排序需要额外空间?
显然我们知道,归并排序的过程就是,递归划分整个区间为基本相等的左右区间,之间左右区间各只有一个数字,然后就合并两个有序区间。
问题就出在了合并两个有序区间上,需要额外的空间。
为什么呢?
这里我举个例子,比如需要合并的两个有序区间为[3 4] 和 [1 2]
我们需要得到最后的结果为[1 2 3 4], 如果不需要额外的空间的话,是做不到的,
当比较1 和 3 的时候, 1 比 3 小,就会覆盖原来的位置。

A2:回答第二个问题之前,先了解一下归并排序的过程,主要有以下两个操作:

·递归划分整个区间为基本相等的左右两个区间
·合并两个有序区间

#include<iostream>
#include<vector>
using namespace std;

void merge_(vector<int>& arr, int left, int mid, int right) {
	vector<int> temp(right - left +1);//辅助数组,不写长度报错

	int i = left, j = mid + 1 , k = 0;
	while (i<= mid && j <= right) //较小值存入temp
		if (arr[i] > arr[j]) temp[k++] = arr[j++];
		else temp[k++] = arr[i++];

	while (i <= mid) //若左侧有剩余,复制到temp
		temp[k++] = arr[i++];
	while (j <= right)//若右侧有剩余,复制到temp
		temp[k++] = arr[j++];

	for (int i = left, k = 0; i <= right; i++,k++)
		arr[i] = temp[k];
}

void MergeSort(vector<int>& arr, int left, int right) {
	if (left < right) {
		int mid = (right + left )/2;//从中间划分子序列;
		MergeSort(arr, left, mid);  //对左侧子序列进行递归排序;
		MergeSort(arr, mid + 1, right); //对右侧子序列进行递归排序
		merge_(arr, left, mid, right); //合并两个有序区间
	}
}

int main() {
	vector<int> a = {1,4,7,2,5,8,3,6,9,10};
	MergeSort(a,0,a.size()-1);

	for(int i = 0; i < 10; i++)
	cout << a[i] << endl;

在这里插入图片描述

二 应用

1 数组中的逆序对

描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P mod 1000000007

数据范围: 对于50% 的数据, size≤10^4

对于 100% 的数据, size≤10^5

数组中所有数字的值满足 0≤val≤1000000

要求:空间复杂度 O(n),时间复杂度 O(nlogn)
输入描述:
题目保证输入的数组中没有的相同的数字

示例1
输入:[1,2,3,4,5,6,7,0]
返回值:7
(说明,00和前面7个数,组成7个逆序对)

示例2
输入:[1,2,3]
返回值:0

在这里插入图片描述

1.1 暴力方法

对于此题,按住一个arr[i], 依次判断{i+1 … n-1]是否满足条件。n为数组的大小。

int InversePairs(vector<int> data) {
         const int kmod = 1000000007;
        int ret = 0;
        int n = data.size();
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                if (data[i] > data[j]) {
                    ret += 1;
                    ret %= kmod;
                }
            }
        }
        return ret;
    }

对于很大的数组会超时

1.2 归并排序思想

明白了归并排序的过程,那么回答问题2.
如果两个区间为[4, 3] 和[1, 2]
那么逆序数为(4,1),(4,2),(3,1),(3,2),同样的如果区间变为有序,比如[3,4] 和 [1,2]的结果是一样的,也就是说区间有序和无序结果是一样的。
但是如果区间有序会有什么好处吗?当然,如果区间有序,比如[3,4] 和 [1,2]
如果3 > 1, 显然3后面的所有数都是大于1, 这里为 4 > 1, 明白其中的奥秘了吧。左区间arr[0]及以后的元素都可以和右区间的arr[2]逆序对,即左区间和右区间的元素arr[2]可以构成(mid-i+1)个逆序对

所以我们可以在合并的时候利用这个规则。

在上述归并排序中,增加了res参数,用于记录,逆序对数量

  void merge_(vector<int>&arr , int left ,int mid ,int right ,int& res);
  void MergeSort(vector<int>&arr ,int left ,int right, int &res);

具体实现,在下面

 while(i <= mid && j <= right){
            if(arr[i] > arr[j]) { 
                temp[k++] = arr[j++];
                res += (mid - i + 1);//比如[3,4] 和 [1,2],当 arr[0]=3, arr[2]=1,此时构成逆序对,
                                     //因为各子区间有序,所以左区间arr[0]及以后的元素都可以和右区间的arr[2]逆序对,即左区间和右区间的元素arr[2]可以构成(mid-i+1)个逆序对
                res %= kmod; //为什么每次都要取余? 直接在最后一次取余不行吗?提交有以下哪个不通过,怀疑是res的累加值非常大超过kmod了,所以最一次性取余效果不一样
            }
            else temp[k++] = arr[i++];
        }
class Solution {
private:
    const int kmod = 1000000007;      
public:
    void merge_(vector<int>&arr , int left ,int mid ,int right ,int& res){
        vector<int> temp(right- left +1);
        int i = left, j = mid + 1, k = 0;
        while(i <= mid && j <= right){
            if(arr[i] > arr[j]) { 
                temp[k++] = arr[j++];
                res += (mid - i + 1);//比如[3,4] 和 [1,2],当 arr[0]=3, arr[2]=1,此时构成逆序对,
                                     //因为各子区间有序,所以左区间arr[0]及以后的元素都可以和右区间的arr[2]逆序对,即左区间和右区间的元素arr[2]可以构成(mid-i+1)个逆序对
                res %= kmod; //为什么每次都要取余? 直接在最后一次取余不行吗?提交有以下哪个不通过,怀疑是res的累加值非常大超过kmod了,所以最一次性取余效果不一样
            }
            else temp[k++] = arr[i++];
        }
        while(i <= mid)   temp[k++] = arr[i++];
        while(j <= right) temp[k++] = arr[j++];
        
        for(int i = left, k = 0; i <= right; i++,k++)
            arr[i] = temp[k];
    }
    
    void MergeSort(vector<int>&arr ,int left ,int right, int &res){
        if(left < right){
            //int mid = (left + right )/2; //这种写法和下面一样,但是当数字非常大时时会溢出(超出int范围),鼓将加法改成减法形式计算,
            int mid = left + (right - left)/2; //从中间划分子序列;
            MergeSort(arr,left,mid,res);   //对左侧子序列进行递归排序;
            MergeSort(arr,mid+1,right,res); //对右侧子序列进行递归排序
            merge_(arr,left,mid,right,res);  //合并两个有序区间
        }
    }
    
    int InversePairs(vector<int> data) {
        int res = 0;
        MergeSort(data,0,data.size() - 1,res);
        return res;
    }
};

1.2 mid = (left + right )/2 溢出问题

 //int mid = (left + right )/2; //这种写法和下面一样,但是当数字非常大时时会溢出(超出int范围),鼓将加法改成减法形式计算,
int mid = left + (right - left)/2; //从中间划分子序列;

1.3 优化

上面程序在函数内部(merge函数)开辟额外空间的做法很不好。

因为这样会涉及到频繁的构建 vector 和析构vector,

所以比较好的做法是:直接在最外层开辟一个足够大的数组,然后传引用到函数。

即,把辅助数组temp,作为形参,,,在调用接口函数里申请temp数组:

void merge_(vector<int>&arr , vector<int>&temp, int left ,int mid ,int right ,int& res);
void MergeSort(vector<int>&arr ,vector<int>&temp, int left ,int right, int &res);
int InversePairs(vector<int> data) {
    int res = 0;
     vector<int> temp(data.size());  // 在最外层开辟辅助数组数组
    MergeSort(data,temp, 0,data.size() - 1,res);
    return res;
}

完整实现

class Solution {
private:
    const int kmod = 1000000007;      
public:
    void merge_(vector<int>&arr , vector<int>&temp, int left ,int mid ,int right ,int& res){
        int i = left, j = mid + 1, k = 0;
        while(i <= mid && j <= right){
            if(arr[i] > arr[j]) { 
                temp[k++] = arr[j++];
                res += (mid - i + 1);//比如[3,4] 和 [1,2],当 arr[0]=3, arr[2]=1,此时构成逆序对,
                                     //因为各子区间有序,所以左区间arr[0]及以后的元素都可以和右区间的arr[2]逆序对,即左区间和右区间的元素arr[2]可以构成(mid-i+1)个逆序对
                res %= kmod; //为什么每次都要取余? 直接在最后一次取余不行吗?提交有以下哪个不通过,怀疑是res的累加值非常大超过kmod了,所以最一次性取余效果不一样
            }
            else temp[k++] = arr[i++];
        }
        while(i <= mid)   temp[k++] = arr[i++];
        while(j <= right) temp[k++] = arr[j++];
        
        for(int i = left, k = 0; i <= right; i++,k++)
            arr[i] = temp[k];
    }
    
    void MergeSort(vector<int>&arr ,vector<int>&temp, int left ,int right, int &res){
        if(left < right){
            //int mid = (left + right )/2; //这种写法和下面一样,但是当数字非常大时时会溢出(超出int范围),鼓将加法改成减法形式计算,
            int mid = left + (right - left)/2; //从中间划分子序列;
            MergeSort(arr,temp,left,mid,res);   //对左侧子序列进行递归排序;
            MergeSort(arr,temp,mid+1,right,res); //对右侧子序列进行递归排序
            merge_(arr,temp,left,mid,right,res);  //合并两个有序区间
        }
    }
    
    int InversePairs(vector<int> data) {
        int res = 0;
         vector<int> temp(data.size());  // 在最外层开辟辅助数组数组
        MergeSort(data,temp, 0,data.size() - 1,res);
        return res;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

R-G-B

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值