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

  • 一 、算法实现
  • 二 应用
    • 1 数组中的逆序对
      • 1.1 暴力方法
      • 1.2 归并排序思想
      • 1.2 mid = (left + right )/2 溢出问题
      • 1.3 优化

一 、算法实现

A1: 首先回答一下第一个问题,为什么归并排序需要额外空间?

显然我们知道,归并排序的过程就是,递归划分整个区间为基本相等的左右区间,之间左右区间各只有一个数字,然后就合并两个有序区间。

问题就出在了合并两个有序区间上,需要额外的空间。

为什么呢?

这里我举个例子,比如需要合并的两个有序区间为[3 4] 和 [1 2]

我们需要得到最后的结果为[1 2 3 4], 如果不需要额外的空间的话,是做不到的,

当比较1 和 3 的时候, 1 比 3 小,就会覆盖原来的位置。

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

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

·合并两个有序区间

cpp 复制代码
#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为数组的大小。

cpp 复制代码
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参数,用于记录,逆序对数量

cpp 复制代码
  void merge_(vector<int>&arr , int left ,int mid ,int right ,int& res);
  void MergeSort(vector<int>&arr ,int left ,int right, int &res);

具体实现,在下面

cpp 复制代码
 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++];
        }
cpp 复制代码
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 溢出问题

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

1.3 优化

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

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

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

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

cpp 复制代码
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);
cpp 复制代码
int InversePairs(vector<int> data) {
    int res = 0;
     vector<int> temp(data.size());  // 在最外层开辟辅助数组数组
    MergeSort(data,temp, 0,data.size() - 1,res);
    return res;
}

完整实现

cpp 复制代码
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;
    }
};
相关推荐
zl_vslam22 分钟前
SLAM中的非线性优-3D图优化之相对位姿Between Factor位姿图优化(十三)
人工智能·算法·计算机视觉·3d
Timmylyx051824 分钟前
CF 新年赛 Goodbye 2025 题解
算法·codeforces·比赛日记
闻缺陷则喜何志丹25 分钟前
【二分查找】P10091 [ROIR 2022 Day 2] 分数排序|普及+
c++·算法·二分查找
only-qi43 分钟前
leetcode2. 两数相加
算法·leetcode
鲨莎分不晴1 小时前
拯救暗淡图像:深度解析直方图均衡化(原理、公式与计算)
人工智能·算法·机器学习
DuHz1 小时前
242-267 GHz双基地超外差雷达系统:面向精密太赫兹传感与成像的65nm CMOS实现——论文阅读
论文阅读·物联网·算法·信息与通信·毫米波雷达
报错小能手1 小时前
数据结构 字典树
开发语言·数据结构
XLYcmy1 小时前
高级密码生成器程序详解:专门设计用于生成基于用户个人信息的密码猜测组合
开发语言·数据结构·python·网络安全·数据安全·源代码·口令安全
AI科技星2 小时前
时空的固有脉动:波动方程 ∇²L = (1/c²) ∂²L/∂t² 的第一性原理推导、诠释与验证
数据结构·人工智能·算法·机器学习·重构
2401_841495642 小时前
【LeetCode刷题】寻找重复数
数据结构·python·算法·leetcode·链表·数组·重复数