一 、算法实现
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;
}
};