目录
- 0.原理讲解
- 1.排序数组
- 1.题目链接
- 2.代码实现
- 2.交易逆序对的总数
- 1.题目链接
- 2.算法原理详解
- 3.代码实现
- 3.计算右侧小于当前元素的个数
- 1.题目链接
- 2.算法原理详解
- 3.代码实现
- 4.翻转对
- 1.题目链接
- 2.算法原理详解
- 3.代码实现
0.原理讲解
- 归并排序的流程充分的体现了**「分⽽治之」**的思想,⼤体过程分为两步
- 分:将数组⼀分为⼆为两部分,⼀直分解到数组的⻓度为1 ,使整个数组的排序过程被分为「左半部分排序」+「右半部分排序」
- 治:将两个较短的**「有序数组合并成⼀个⻓的有序数组」**,⼀直合并到最初的⻓度
- 本质类似二叉树的后序遍历
1.排序数组
1.题目链接
- 排序数组
2.代码实现
- 递归时,创建vector开销挺大的 -> 辅助数组放全局
class Solution
{vector<int> assist; // 归并时的辅助数组
public:vector<int> sortArray(vector<int>& nums) {assist.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 - left) / 2;// 2.排序左右区间// [left, mid] [mid + 1, right]MergeSort(nums, left, mid);MergeSort(nums, mid + 1, right);// 3.合并两个有序数组int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid && cur2 <= right){assist[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];}// 4.处理没有遍历完的数组while(cur1 <= mid){assist[i++] = nums[cur1++];}while(cur2 <= right){assist[i++] = nums[cur2++];}// 5.还原for(int i = left; i <= right; i++){nums[i] = assist[i - left];}}
};
2.交易逆序对的总数
1.题目链接
- 交易逆序对的总数
2.算法原理详解
-
解法:利用归并排序的分治过程
- 左半部分 -> 左派序 -> 右半部分 -> 右排序 -> 一左一右 -> 排序
- 在归并排序的合并过程中统计出逆序对的数量
-
注意:默认都是升序,如果掌握升序,结合问题,降序的归并过程也是可以解决问题的
-
为什么可以利用归并排序?
- 如果将数组从中间划分成两个部分,那么可以将逆序对产⽣的⽅式划分成三组
- 全部从左数组中选择
- 全部从右数组中选择
- ⼀个选左数组另⼀个选右数组
- 根据排列组合的分类相加原理,三种情况下产⽣的逆序对的总和,正好等于总的逆序对数量
- ⽽这个思路正好匹配归并排序的过程
- 先排序左数组
- 再排序右数组
- 左右数组合二为一
- 因此,可以利⽤归并排序的过程
- 先求出左半数组中逆序对的数量
- 再求出右半数组中逆序对的数量
- 最后求出⼀个选择左边,另⼀个选择右边情况下逆序对的数量,三者相加即可
- 如果将数组从中间划分成两个部分,那么可以将逆序对产⽣的⽅式划分成三组
-
为什么要这么做?
- 在归并排序合并的过程中,得到的是两个有序的数组
- 可以利⽤数组的有序性,快速统计出逆序对的数量,⽽不是将所有情况都枚举出来
-
如何在合并两个有序数组的过程中,统计出逆序对的数量?
- 合并两个有序序列时求逆序对的⽅法有两种
- 快速统计出某个数前⾯有多少个数⽐它⼤ <- 升序
- 降序无法实现
- 快速统计出某个数后⾯有多少个数⽐它⼩ <- 降序
- 升序无法实现
- 快速统计出某个数前⾯有多少个数⽐它⼤ <- 升序
- 合并两个有序序列时求逆序对的⽅法有两种
-
法一:快速统计出某个数前⾯有多少个数⽐它⼤
- 在合并有序数组的时候,遇到左数组当前元素 > 右数组当前元素时,可以通过计算左数组中剩余元素的⻓度,就可快速求出右数组当前元素前⾯有多少个数⽐它⼤
- 在合并有序数组的时候,遇到左数组当前元素 > 右数组当前元素时,可以通过计算左数组中剩余元素的⻓度,就可快速求出右数组当前元素前⾯有多少个数⽐它⼤
-
法二:快速统计出某个数后⾯有多少个数⽐它⼩
- 在合并有序数组的时候,遇到左数组当前元素 <= 右数组当前元素时,可以通过计算右数组已经遍历过的元素的⻓度,快速求出左数组当前元素后⾯有多少个数⽐它⼤
- 在合并有序数组的时候,遇到左数组当前元素 <= 右数组当前元素时,可以通过计算右数组已经遍历过的元素的⻓度,快速求出左数组当前元素后⾯有多少个数⽐它⼤
3.代码实现
// v1.0 升序
class Solution
{vector<int> assist;
public:int ReversePairs(vector<int>& nums) {assist.resize(nums.size());return MergeSort(nums, 0, nums.size() - 1);}int MergeSort(vector<int>& nums, int left, int right){if(left >= right){return 0;}int ret = 0;// 选择中点,划分数组int mid = left + (right - left) / 2;// 左边的个数 + 排序 + 右边的个数 + 排序// [left, mid] [mid + 1, right]ret += MergeSort(nums, left, mid);ret += MergeSort(nums, mid + 1, right);// 一左一右的个数 + 排序int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){assist[i++] = nums[cur1++];}else{ret += mid - cur1 + 1;assist[i++] = nums[cur2++];}}// 处理未遍历完的数组while(cur1 <= mid){assist[i++] = nums[cur1++];}while(cur2 <= right){assist[i++] = nums[cur2++];}// 还原for(int i = left; i <= right; i++){nums[i] = assist[i - left];}return ret;}
};
----------------------------------------------------------------------------
// v2.0 降序
class Solution
{vector<int> assist;
public:int reversePairs(vector<int>& nums) {assist.resize(nums.size());return MergeSort(nums, 0, nums.size() - 1);}int MergeSort(vector<int>& nums, int left, int right){if(left >= right){return 0;}int ret = 0;// 选择中点,划分数组int mid = left + (right - left) / 2;// 左边的个数 + 排序 + 右边的个数 + 排序// [left, mid] [mid + 1, right]ret += MergeSort(nums, left, mid);ret += MergeSort(nums, mid + 1, right);// 一左一右的个数 + 排序int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){assist[i++] = nums[cur2++];}else{ret += right - cur2 + 1;assist[i++] = nums[cur1++];}}// 处理未遍历完的数组while(cur1 <= mid){assist[i++] = nums[cur1++];}while(cur2 <= right){assist[i++] = nums[cur2++];}// 还原for(int i = left; i <= right; i++){nums[i] = assist[i - left];}return ret;}
};
3.计算右侧小于当前元素的个数
1.题目链接
- 计算右侧小于当前元素的个数
2.算法原理详解
-
思路:解法与「求数组中的逆序对」的解法类似,但是本题要返回⼀个数组,记录每⼀个元素的右边有多少个元素⽐⾃⼰⼩
-
在归并排序的过程中,元素的下标是会跟着变化的
- 因此需要⼀个辅助数组,来将数组元素和对应的下标绑定在⼀起归并
- 也就是在归并元素的时候,顺势将下标也转移到对应的位置上
-
创建两个全局的数组
vector<int> ret
-> 记录结果vector<int> index
-> 记录下标
3.代码实现
class Solution
{vector<int> ret;vector<int> index;vector<int> assistNums;vector<int> assistIndex;
public:vector<int> CountSmaller(vector<int>& nums) {int n = nums.size();ret.resize(n);index.resize(n);assistNums.resize(n);assistIndex.resize(n);// 初始化indexfor(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;}// 中间点,划分数组int mid = left + (right - left) / 2;// [left, mid] [mid + 1, right]// 先处理左右子数组MergeSort(nums, left, mid);MergeSort(nums, mid + 1, right);// 处理一左一右 + 排序(降序)// 元素和下标同步迁移int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){assistNums[i] = nums[cur2];assistIndex[i++] = index[cur2++];}else{ret[index[cur1]] += right - cur2 + 1; // 统计 -> 重点assistNums[i] = nums[cur1];assistIndex[i++] = index[cur1++];}}// 处理未遍历完数组while(cur1 <= mid){assistNums[i] = nums[cur1];assistIndex[i++] = index[cur1++];}while(cur2 <= right){assistNums[i] = nums[cur2];assistIndex[i++] = index[cur2++];}// 还原for(int i = left; i <= right; i++){nums[i] = assistNums[i - left];index[i] = assistIndex[i - left];}}
};
4.翻转对
1.题目链接
- 翻转对
2.算法原理详解
-
思路解析:
- ⼤思路与求逆序对的思路⼀样,利⽤归并排序的思想,将求整个数组的翻转对的数量,转换成三部分:
- 左半区间翻转对的数量,右半区间翻转对的数量,⼀左⼀右选择时翻转对的数量
- 重点就是在合并区间过程中,如何计算出翻转对的数量
- 与上个问题不同的是,上⼀道题可以⼀边合并⼀遍计算,但是这道题要求的是左边元素⼤于右边元素的两倍,如果直接合并的话,是⽆法快速计算出翻转对的数量的
- 与上个问题不同的是,上⼀道题可以⼀边合并⼀遍计算,但是这道题要求的是左边元素⼤于右边元素的两倍,如果直接合并的话,是⽆法快速计算出翻转对的数量的
- ⼤思路与求逆序对的思路⼀样,利⽤归并排序的思想,将求整个数组的翻转对的数量,转换成三部分:
-
计算翻转对:利用单调性,使用同向双指针
- 法一:计算当前元素后面,有多少元素的两倍比我小 -> 降序
- 法二:计算当前元素之前,有多少元素的一半比我大 -> 升序
3.代码实现
// v1.0 降序
class Solution
{vector<int> assist;
public:int reversePairs(vector<int>& nums) {assist.resize(nums.size());return MergeSort(nums, 0, nums.size() - 1);}int MergeSort(vector<int>& nums, int left, int right){if(left >= right){return 0;}int ret = 0;// 中间点,划分两区间int mid = left + (right - left) / 2;// [left, mid] [mid + 1, right]// 先计算左右子区间翻转对ret += MergeSort(nums, left, mid);ret += MergeSort(nums, mid + 1, right);// 计算一左一右翻转对的数量int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid) // 降序 固定cur1{// * -> / 防溢出// / 2.0确保能除尽while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0){cur2++;}// 优化if(cur2 > right){break;}ret += right - cur2 + 1;cur1++;}// 合并两个有序数组cur1 = left, cur2 = mid + 1;while(cur1 <= mid && cur2 <= right){assist[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];}// 处理未遍历完数组while(cur1 <= mid){assist[i++] = nums[cur1++];}while(cur2 <= right){assist[i++] = nums[cur2++];}// 还原for(int i = left; i <= right; i++){nums[i] = assist[i - left];}return ret;}
};
-------------------------------------------------------------------------
// v2.0 升序
class Solution
{vector<int> assist;
public:int reversePairs(vector<int>& nums) {assist.resize(nums.size());return MergeSort(nums, 0, nums.size() - 1);}int MergeSort(vector<int>& nums, int left, int right){if(left >= right){return 0;}int ret = 0;// 中间点,划分两区间int mid = left + (right - left) / 2;// [left, mid] [mid + 1, right]// 先计算左右子区间翻转对ret += MergeSort(nums, left, mid);ret += MergeSort(nums, mid + 1, right);// 计算一左一右翻转对的数量int cur1 = left, cur2 = mid + 1, i = 0;while(cur2 <= right) // 升序 固定cur2{while(cur1 <= mid && nums[cur2] >= nums[cur1] / 2.0){cur1++;}// 优化if(cur1 > mid){break;}ret += mid - cur1 + 1;cur2++;}// 合并两个有序数组cur1 = left, cur2 = mid + 1;while(cur1 <= mid && cur2 <= right){assist[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];}// 处理未遍历完数组while(cur1 <= mid){assist[i++] = nums[cur1++];}while(cur2 <= right){assist[i++] = nums[cur2++];}// 还原for(int i = left; i <= right; i++){nums[i] = assist[i - left];}return ret;}
};