我爱学算法之—— 分治-归并

一、排序数组

题目解析

这道题就是一个简单的排序数组(与分治-快排中排序数组那道题一样)

算法思路

这里就回顾一下归并排序,使用归并排序来解决这道题。

简单来说归并排序就分为两步:

  1. 递归划分数组
  2. 合并两个有序数组

合并两个有序数组

归并排序中,划分数组是提供递归分治实现的;在整个代码逻辑中,我们只需要实现合并两个有序数组即可。

以升序排序为例

  1. 定义两个指针cur1cur2遍历两个数组(升序的);定义一个tmp数组,用来记录排序完的数组。
  2. 谁小就将谁放到tmp数组中。
  3. 遍历完之后,如果cur1/cur2(其中一个)没有遍历完,就将其后面的元素一次放入数组tmp数组中。
  4. 最后,还原数组即可。

代码实现

cpp 复制代码
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++];
            else
                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];
        }
    }
    vector<int> sortArray(vector<int>& nums) {
        msort(nums, 0, nums.size() - 1);
        return nums;
    }
};

二、交易逆序对的总数

题目解析

这道题,给定一个数组nums,要我们求出这个数组nums中的逆序对个数。(逆序对就是左边的数大于右边的数)

算法思路

解法一:暴力枚举

枚举出数组nums中的所有的二元组,判断是否是逆序对,然后统计逆序对的个数。

时间复杂度O(n^2)

解法二 :归并排序

第一次看到这道题,可能会很疑惑,这跟归并排序有什么关系呢?

归并排序是先进行数组划分,然后再合并数组中有序的两部分。

数组nums中的逆序对个数 = 左边期间逆序对个数 + 右边区间逆序对个数 + 左区间选一个数、右区间选一个数组成逆序对的个数

而在归并排序中,是要进行合并数组中两个有序的部分的,合并的过程不就是从leftright区间各选取一个数进行比较大小吗?

所以,在遍历过程中,leftright区间中的逆序对个数通过递归返回值过去;我们只需在合并两个有序数组的过程中,统计leftright区间各选取一个数能组成逆序对的数量

合并两个有序的数组,并统计逆序对数量这里按升序排序):

定义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个逆序对。

代码实现

cpp 复制代码
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中每一个元素的值,以及它的原始下标;在排序的过程中,不仅要排序数组,还是更修对应的原始下标

代码实现

cpp 复制代码
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]

所以,在归并排序的过程中,要先统计重要翻转对的个数,再合并两个有序的数组部分。

代码实现

cpp 复制代码
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++];
            else
                tmp[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

相关推荐
、、、、南山小雨、、、、3 小时前
pytorch下对各种超参调整效果
1024程序员节
风清再凯3 小时前
02_prometheus监控&Grafana展示
prometheus·1024程序员节
Gorgous—l3 小时前
数据结构算法学习:LeetCode热题100-链表篇(下)(随机链表的复制、排序链表、合并 K 个升序链表、LRU 缓存)
数据结构·学习·算法
仰泳的熊猫3 小时前
LeetCode:200. 岛屿数量
数据结构·c++·算法·leetcode
流星5211223 小时前
GC 如何判断对象该回收?从可达性分析到回收时机的关键逻辑
java·jvm·笔记·学习·算法
sulikey3 小时前
Qt 入门简洁笔记:从框架概念到开发环境搭建
开发语言·前端·c++·qt·前端框架·visual studio·qt框架
心灵宝贝3 小时前
申威服务器安装Java11(swjdk-11u-9.ky10.sw_64.rpm)详细操作步骤(附安装包)
1024程序员节
傻童:CPU3 小时前
C语言需要掌握的基础知识点之链表
c语言·1024程序员节
defaulter3 小时前
Codeforces Round 1049 (Div. 2)C. Ultimate Value
算法·codeforces