分治 归并排序
- [一、排序数组 / 归并排序的复习](#一、排序数组 / 归并排序的复习)
- 二、逆序对的总数
- 三、计算右侧小于当前元素的个数
- 四、翻转对
一、排序数组 / 归并排序的复习
归并排序的主要步骤:
①:就是找一个数组里面的中间数mid,将这个数组分为两部分,左半部分和右半部分。然后再从左半部分和右半部分里面再找一个中间数mid,依次不断的递归下去,直到分到最小单位为止,排完序之后从下往上返回,这就是归并排序的递归。
②:合并两个有序数组(双指针法)
分别对左半部分数组和右半部分数组,定义一个下标移动的变量cur1和cur2,然后进行循环比较。
1、题目解析
题目链接:排序数组
2、算法原理
分治的思想:归并排序类似于二叉树的后序遍历
主要步骤:
1. 中间点的划分
2. 对左右区间的递归
3. 合并两个有序数组
4. 还原
3、代码
cpp
class Solution
{
vector<int> tmp;
// (2)第二种方法,在全局开一个临时数组tmp(用来保存每次递归合并的两个有序数组),
// 然后就不用每次递归还创建tmp
public:
vector<int> sortArray(vector<int>& nums)
{
tmp.resize(nums.size());// 开空间
mergeSort(nums,0,nums.size()-1);
return nums;
}
void mergeSort(vector<int>& nums,int left,int right)
{
// 处理边界情况
if(left>=right) return;
// 1、中间点划分区间
int mid=(left+right)>>1;
// [left,mid] [mid+1,right]
// 2、归并的递归排序
mergeSort(nums,left,mid);
mergeSort(nums,mid+1,right);
// 3、合并两个有序数组
int cur1=left,cur2=mid+1,i=0;
while(cur1<=mid&&cur2<=right)
tmp[i++]=nums[cur1]<=nums[cur2]?nums[cur1++]:nums[cur2++];
// 排的是升序
// 处理没有遍历完的数组
while(cur1<=mid) tmp[i++]=nums[cur1++];
while(cur2<=right) tmp[i++]=nums[cur2++];
// 4、还原
for(int i=left;i<=right;i++)
{
nums[i]=tmp[i-left];
}
}
};
二、逆序对的总数
1、题目解析
2、算法原理
(1)将整个数组分为左半部分和右半部分,
(2)分别在左半部分找逆序对的个数,排完序。
(3)然后再在右半部分找逆序对的个数再排排序,
(4)最后再一左一右利用双指针的算法,固定一个然后移动另外一个在:一左一右->这两个数组里面分别找逆序对的个数,最后加起来就是整个数组的逆序对个数。
这样的解法的时间复杂度:N*logN
关键的细节:
3、代码
cpp
class Solution
{
int tmp[50010];
public:
int reversePairs(vector<int>& nums)
{
return mergeSort(nums,0,nums.size()-1);
}
int mergeSort(vector<int>& nums,int left,int right)
{
// 处理边界情况
if(left>=right) return 0;
// 1、找中间值mid划分为左右两部分
int mid=(left+right)>>1;
// [left,mid] [mid+1,right]
int ret=0;// 记录逆序对个数
// 2、在递归排序前多做一件事:左半部分找"逆序对",右半部分找"逆序对"
ret+=mergeSort(nums,left,mid);
ret+=mergeSort(nums,mid+1,right);
// 3、一左一右找逆序对个数
int cur1=left,cur2=mid+1,i=0;
while(cur1<=mid&&cur2<=right)
{
if(nums[cur1]<=nums[cur2])
{
tmp[i++]=nums[cur1++];
}
else
{
//ret+=cur2-(mid+1)+1; 这种情况不行,因为我们是以左边为基准的,只要cur1遍历完,一左一右这个过程就结束了!
ret+=mid-cur1+1;// 因为是升序,所以在左半部分,只要此时的cur1>cur2,那么位于cur1右侧~mid之间的数据全部>cur2
tmp[i++]=nums[cur2++];
}
}
// 3、处理没遍历完的
while(cur1<=mid) tmp[i++]=nums[cur1++];
while(cur2<=right) tmp[i++]=nums[cur2++];
// 4、还原
for(int j=left;j<=right;j++)
{
nums[j]=tmp[j-left];// 始终不太理解 还原的时候,为啥要用j-left???不能直接nums[j]=tmp[j]吗???
}
return ret;
}
};
三、计算右侧小于当前元素的个数
1、题目解析
2、算法原理
这道题仍然是利用归并排序的分治思想
但是这次我们要排的是:降序!
但是有一点需要特别注意,这是一个重点也是一个难点。
题目中需要返回的是一个数组!
一个什么样的数组呢?原始数组下标所对应位置,有多少个右面比我小的元素。
这里的重点和难点就在于:我们如何找到我们正在遍历的数组(是打乱排完序的)中当前元素的原始下标!
解决办法就是:在进行分治归并排序之前,对原始数组利用哈希思想进行下标绑定。
就是你进行递归排序的时候,你的下标跟着你的数字是一起移动的!!!
3、代码
cpp
class Solution
{
vector<int> ret;// 返回答案的数组
vector<int> index;// 创建index数组的原因是:对排序前的 原始数组 进行下标"绑定"!
int tmpNums[500010];// 这两个都是临时数组
int tmpIndex[500010];
public:
vector<int> countSmaller(vector<int>& nums)
{
int n=nums.size();
ret.resize(n);
index.resize(n);
// 排序前的 原始数组 进行下标"绑定"!
for(int i=0;i<n;i++)
{
index[i]=i;
}
mergeSort(nums,0,n-1);
return ret;
}
void mergeSort(vector<int>& nums,int left,int right)
{
// 处理边界情况
if(left>=right) return ;
// 1、找中间数mid
int mid=(left+right)>>1;
//[left,mid] [mid+1,right]
//2、归并排序
mergeSort(nums,left,mid);
mergeSort(nums,mid+1,right);
// 3、找
int cur1=left,cur2=mid+1,i=0;
while(cur1<=mid&&cur2<=right)
{
// 降序排序
if(nums[cur1]<=nums[cur2])
{
tmpNums[i]=nums[cur2];
tmpIndex[i++]=index[cur2++];
}
else
{
//tmpNums[index[i]]+=right-cur2+1;
// 重点!(统计此时cur1位置有多少符合条件的,因为cur1原本是在nums中遍历的,但是由于,index跟nums是一一对应的关系,所以index[cur1]就代表着原始数据的下标)
ret[index[cur1]]+=right-cur2+1;
tmpNums[i]=nums[cur1];
tmpIndex[i++]=index[cur1++];
}
}
// 处理没遍历完的
while(cur1<=mid)
{
tmpNums[i]=nums[cur1];
tmpIndex[i++]=index[cur1++];// 绑定的下标数组 也要跟着还原。++只需要进行一次即可
}
while(cur2<=right)
{
tmpNums[i]=nums[cur2];
tmpIndex[i++]=index[cur2++];
}
// 4、还原
for(int j=left;j<=right;j++)
{
nums[j]=tmpNums[j-left];
index[j]=tmpIndex[j-left];
}
}
};
四、翻转对
1、题目解析
链接:翻转对
2、算法原理
主要思想还是用归并排序的分治思想
1、运用递归分别:处理左半部分、右半部分
2、运用" 单调性+同向的双指针 " 处理一左一右的情况
3、代码
cpp
class Solution
{
int tmp[50010];
public:
int reversePairs(vector<int>& nums)
{
return mergeSort(nums,0,nums.size()-1);
}
int mergeSort(vector<int>& nums,int left,int right)
{
// 处理边界情况
if(left>=right) return 0;
int ret=0;
// 1、中间数mid
int mid=(left+right)>>1;
// 2、处理左半部分、右半部分
ret+=mergeSort(nums,left,mid);
ret+=mergeSort(nums,mid+1,right);
// 3、一左一右(固定cur1不动,cur2移动)
int cur1=left,cur2=mid+1,i=left;
while(cur1<=mid)
{
while(cur2<=right&&nums[cur1]/2.0<=nums[cur2])// 因为 *2会溢出,所以我们用/2.0
{
cur2++;
}
// 接下来就是符合条件的
ret+=right-cur2+1;
cur1++;
}
// 4、合并两个有序数组
cur1=left,cur2=mid+1;
while(cur1<=mid&&cur2<=right)
{
tmp[i++]=nums[cur1]>=nums[cur2]?nums[cur1++]:nums[cur2++];//降序排序,把大的放前面
}
// 处理 未遍历完的
while(cur1<=mid) tmp[i++]=nums[cur1++];
while(cur2<=right) tmp[i++]=nums[cur2++];
// 4、还原
for(int j=left;j<=right;j++)
{
nums[j]=tmp[j];
}
return ret;
}
};