我爱学算法之—— 分治-归并
一、排序数组
题目解析

这道题就是一个简单的排序数组(与分治-快排中排序数组那道题一样)
算法思路
这里就回顾一下归并排序,使用归并排序来解决这道题。
简单来说归并排序就分为两步:
- 递归划分数组
- 合并两个有序数组

合并两个有序数组:
归并排序中,划分数组是提供递归分治实现的;在整个代码逻辑中,我们只需要实现合并两个有序数组即可。
以升序排序为例:
- 定义两个指针
cur1、cur2遍历两个数组(升序的);定义一个tmp数组,用来记录排序完的数组。- 谁小就将谁放到
tmp数组中。- 遍历完之后,如果
cur1/cur2(其中一个)没有遍历完,就将其后面的元素一次放入数组tmp数组中。- 最后,还原数组即可。
代码实现
class Solution {
public:void msort(vector<int>& nums, int left, int right) {if (left >= right)return;// 递归划分数组int mid = left + (right - left) / 2;msort(nums, left, mid);msort(nums, mid + 1, right);// 合并两个有序数组int sz = right - left + 1;int cur1 = left, cur2 = mid + 1;vector<int> tmp(sz);int i = 0;while (cur1 <= mid && cur2 <= right) {if (nums[cur1] <= nums[cur2])tmp[i++] = nums[cur1++];elsetmp[i++] = nums[cur2++];}while (cur1 <= mid) {tmp[i++] = nums[cur1++];}while (cur2 <= right) {tmp[i++] = nums[cur2++];}// 还原数组for (i = 0; i < sz; i++) {nums[left + i] = tmp[i];}}vector<int> sortArray(vector<int>& nums) {msort(nums, 0, nums.size() - 1);return nums;}
};
二、交易逆序对的总数
题目解析

这道题,给定一个数组nums,要我们求出这个数组nums中的逆序对个数。(逆序对就是左边的数大于右边的数)
算法思路
解法一:暴力枚举
枚举出数组
nums中的所有的二元组,判断是否是逆序对,然后统计逆序对的个数。时间复杂度
O(n^2)
解法二 :归并排序
第一次看到这道题,可能会很疑惑,这跟归并排序有什么关系呢?
归并排序是先进行数组划分,然后再合并数组中有序的两部分。
数组
nums中的逆序对个数 = 左边期间逆序对个数 + 右边区间逆序对个数 + 左区间选一个数、右区间选一个数组成逆序对的个数
而在归并排序中,是要进行合并数组中两个有序的部分的,合并的过程不就是从left、right区间各选取一个数进行比较大小吗?
所以,在遍历过程中,left、right区间中的逆序对个数通过递归返回值过去;我们只需在合并两个有序数组的过程中,统计left、right区间各选取一个数能组成逆序对的数量。
合并两个有序的数组,并统计逆序对数量(这里按升序排序):
定义
cur1遍历left区间、cur2遍历右区间。
nums[cur1] <= nums[cur2]:tmp[i++] = nums[cur1++]
nums[cur1] > nums[cur2]:ret += (mid - cur1 + 1); tmp[i++] = nums[cur2++]因为数组是升序排序的,
nums[cur1] > nums[cur2],所以[cur1,mid]区间的所有元素都大于nums[cur2]都能和nums[cur2]组成一个逆序对。因为这里是归并排序,在统计逆序对个数时,
left区间和right区间是有序的;且left区间中任意一个元素一定在right中元素的左边。
这里使用升序排序,在合并两个有序数组部分时:
如果
nums[cur1] > num[cur2],区间[cur1, mid]中的所有元素都大于nums[cur2];以nums[cur2]为右边值就存在mid-cur1+1个逆序对。
代码实现
class Solution {
public:int msort(vector<int>& nums, int left, int right) {if (left >= right)return 0;int mid = left + (right - left) / 2;int ln = msort(nums, left, mid);int rn = msort(nums, mid + 1, right);// 合并两个有序数组、并统计逆序对数量int cnt = 0, sz = right - left + 1;vector<int> tmp(sz, 0);int cur1 = left, cur2 = mid + 1;int i = 0;while (cur1 <= mid && cur2 <= right) {if (nums[cur1] <= nums[cur2])tmp[i++] = nums[cur1++];else {cnt += (mid - cur1 + 1);tmp[i++] = nums[cur2++];}}while (cur1 <= mid)tmp[i++] = nums[cur1++];while (cur2 <= right)tmp[i++] = nums[cur2++];// 复原数组for (i = 0; i < sz; i++) {nums[left + i] = tmp[i];}return ln + rn + cnt;}int reversePairs(vector<int>& record) {int ret = msort(record, 0, record.size() - 1);return ret;}
};
三、计算右侧小于当前元素的个数
题目解析

这道题,和上一道逆序对的题可以说非常相似,逆序对那道题是让我们求逆序对的总数;
而这道题,则是让我们求出某一个位置右边有多少小于该位置的值的;也就是以某一位置为左边值,能组成逆序对的个数。
算法思路
对于这道题,整体思路是和求逆序对总数是相同的。
暴力解法:遍历数组,枚举所有的二元组,求出每一个位置右侧小于自己的元素个数。
分治-归并:
和求逆序对一样,但是这道题要注意要求出每一个位置右侧有多少元素小于自己(也就是以某一个位置为左边值,能形成逆序对的个数)
所以,在进行归并排序的过程中,我们不仅要对数组进行排序,并且还要维护下标。
这里使用降序排序,合并两个有序的数组部分时:
如果
nums[cur1] > nums[cur2]时,区间[cur2, right]中的所有元素都能和nums[cur1]形成逆序对,此时找到nums[cur1]对应原始数组中下标,统计逆序对个数即可。
维护nums中每一个元素的值,以及它的原始下标;在排序的过程中,不仅要排序数组,还是更修对应的原始下标
代码实现
class Solution {
public:void msort(vector<int>& ret, vector<int>& nums, vector<int>& index,int left, int right) {if (left >= right)return;int mid = left + (right - left) / 2;msort(ret, nums, index, left, mid);msort(ret, nums, index, mid + 1, right);int sz = right - left + 1;int cur1 = left, cur2 = mid + 1, i = 0;vector<int> tmp_n(sz), tmp_i(sz);while (cur1 <= mid && cur2 <= right) {if (nums[cur1] <= nums[cur2]) {tmp_n[i] = nums[cur2];tmp_i[i++] = index[cur2++];} else {ret[index[cur1]] += (right - cur2 + 1);tmp_n[i] = nums[cur1];tmp_i[i++] = index[cur1++];}}while (cur1 <= mid) {tmp_n[i] = nums[cur1];tmp_i[i++] = index[cur1++];}while (cur2 <= right) {tmp_n[i] = nums[cur2];tmp_i[i++] = index[cur2++];}for (int i = 0; i < sz; i++) {nums[left + i] = tmp_n[i];index[left + i] = tmp_i[i];}}vector<int> countSmaller(vector<int>& nums) {int n = nums.size();vector<int> ret(n, 0), index(n);for (int i = 0; i < n; i++)index[i] = i;msort(ret, nums, index, 0, n - 1);return ret;}
};
四、翻转对
题目解析

这道题和上述逆序对的题稍稍不一样,这里如果i<j并且nums[i] > 2*nums[j],(i,j)就是一个重要翻转对。
给定一个数组nums要我们求出数组中重要翻转对的数量。
算法思路
这道题的整体思路和上述是一模一样的,还是在归并排序的过程中去求重要翻转对的个数。
在分治-归并排序的过程,合并两个有序数组时,可以保证左边区间中的元素下标都是小于右边区间元素的下标的;
所以,只需要统计坐标区间中任意元素和右边区间中的任意元素,满足nums[i] > 2*nums[j]即可。
而两个数组区间还是有序的 :(降序排序)如果nums[i] >nums[j]*2,则区间[j, right]中所有元素都可以和元素i形成一个重要翻转对。
至于左、右区间内的重要翻转对个数,在递归的过程中求出即可。
这里因为判断重要翻转对的条件时
nums[i] > 2*nums[j],而排序数组的条件是nums[i] > nums[j]。所以,在归并排序的过程中,要先统计重要翻转对的个数,再合并两个有序的数组部分。
代码实现
class Solution {
public:int msort(vector<int>& nums, int left, int right) {if (left >= right)return 0;int mid = left + (right - left) / 2;int ln = msort(nums, left, mid);int rn = msort(nums, mid + 1, right);int cnt = 0, cur1 = left, cur2 = mid + 1;while (cur1 <= mid && cur2 <= right) {if (nums[cur1] <= 2 * (long long)nums[cur2])cur2++;else {cnt += (right - cur2 + 1);cur1++;}}int sz = right - left + 1, i = 0;cur1 = left, cur2 = mid + 1;vector<int> tmp(sz);while (cur1 <= mid && cur2 <= right) {if (nums[cur1] <= nums[cur2])tmp[i++] = nums[cur2++];elsetmp[i++] = nums[cur1++];}while (cur1 <= mid)tmp[i++] = nums[cur1++];while (cur2 <= right)tmp[i++] = nums[cur2++];for (i = 0; i < sz; i++)nums[left + i] = tmp[i];return cnt + ln + rn;}int reversePairs(vector<int>& nums) {return msort(nums, 0, nums.size() - 1);}
};
本篇文章到这里就结束了,感谢支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws


