归并排序 (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;
    }
};
相关推荐
少许极端2 小时前
算法奇妙屋(十二)-优先级队列(堆)
数据结构·算法·leetcode·优先级队列··图解算法
kupeThinkPoem3 小时前
哈希表有哪些算法?
数据结构·算法
小白程序员成长日记3 小时前
2025.11.16 力扣每日一题
算法
爬山算法3 小时前
Redis(127)Redis的内部数据结构是什么?
数据结构·数据库·redis
Kuo-Teng4 小时前
LeetCode 118: Pascal‘s Triangle
java·算法·leetcode·职场和发展·动态规划
Greedy Alg4 小时前
LeetCode 32. 最长有效括号(困难)
算法
ShineWinsu5 小时前
对于数据结构:链式二叉树的超详细保姆级解析—中
数据结构·c++·算法·面试·二叉树·校招·递归
合方圆~小文5 小时前
高性能20倍变焦球机转动功能监控设备
数据结构·数据库·数码相机·模块测试
野蛮人6号5 小时前
力扣热题100道之207课程表
算法·leetcode·职场和发展