归并排序之翻转对(hard)

归并排序解决重要翻转对

题目描述

给定一个数组 nums,如果满足:

text 复制代码
i < j 且 nums[i] > 2 * nums[j]

那么 (i, j) 就是一对重要翻转对。

请返回数组中重要翻转对的数量。

例如:

text 复制代码
输入:[1,3,2,3,1]
输出:2

满足条件的下标对为:

text 复制代码
(1, 4):3 > 2 * 1
(3, 4):3 > 2 * 1

小记:今天这题的hard难题和前几天类似,但是没有了下标这种比较难操作的元素,只需要我在比较完二倍大小后再重新归并排序,简单了很多,今天过的也很充实,还不错

基本思路

最直接的方法是枚举所有下标对:

cpp 复制代码
for (int i = 0; i < n; i++)
{
    for (int j = i + 1; j < n; j++)
    {
        if (nums[i] > 2 * nums[j])
            ret++;
    }
}

这种方法的时间复杂度是 O(N^2)

当数组长度达到 50000 时,效率无法接受。

这道题可以使用归并排序,将数组分成左右两个有序区间,然后统计跨越左右区间的重要翻转对。

核心思想是:

利用归并排序,把逐个比较转化为按段统计。


归并过程中的统计

假设左右两个区间已经按照降序排列:

text 复制代码
左区间:[10, 6, 3]
右区间:[4, 2, 1]

对于左区间中的某个数字 nums[cur1],从右区间的 cur2 开始判断。

如果:

cpp 复制代码
nums[cur1] > 2LL * nums[cur2]

由于右区间是降序排列的,所以 cur2 后面的所有数字都不大于当前数字。

因此,后面的所有数字也都满足条件,可以一次性统计:

cpp 复制代码
ret += right - cur2 + 1;

如果当前条件不满足,就让 cur2 向后移动,尝试右区间中更小的数字。


为什么使用降序排列

本代码在归并时选择较大的数字:

cpp 复制代码
if (nums[cur11] > nums[cur22])
{
    tmp[i++] = nums[cur11++];
}

因此左右区间最终都是降序排列。

在降序数组中:

  • 左区间指针向后移动,左边的数字变小;
  • 右区间指针向后移动,右边的数字也变小。

当某个左边数字满足:

cpp 复制代码
nums[cur1] > 2LL * nums[cur2]

右区间从 cur2right 的数字全部满足条件,所以可以直接统计这一整段。


完整代码

cpp 复制代码
class Solution
{
    int ret = 0;
    vector<int> tmp;

public:
    int mergeSort(vector<int>& nums, int left, int right)
    {
        if (left >= right)
            return 0;

        int mid = (right - left) / 2 + left;

        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        // 统计跨越左右区间的重要翻转对
        int cur1 = left;
        int cur2 = mid + 1;

        while (cur1 <= mid && cur2 <= right)
        {
            if (nums[cur1] > 2LL * nums[cur2])
            {
                ret += right - cur2 + 1;
                cur1++;
            }
            else
            {
                cur2++;
            }
        }

        // 合并两个降序区间
        int cur11 = left;
        int cur22 = mid + 1;
        int i = 0;

        while (cur11 <= mid && cur22 <= right)
        {
            if (nums[cur11] > nums[cur22])
            {
                tmp[i++] = nums[cur11++];
            }
            else
            {
                tmp[i++] = nums[cur22++];
            }
        }

        while (cur11 <= mid)
        {
            tmp[i++] = nums[cur11++];
        }

        while (cur22 <= right)
        {
            tmp[i++] = nums[cur22++];
        }

        // 将临时数组中的内容复制回原数组
        for (int u = left; u <= right; u++)
        {
            nums[u] = tmp[u - left];
        }

        return ret;
    }

    int reversePairs(vector<int>& nums)
    {
        ret = 0;

        int n = nums.size();
        tmp.resize(n);

        return mergeSort(nums, 0, n - 1);
    }
};

代码中的关键细节

1. 使用 2LL 防止溢出

题目中的数字可能达到 32 位整数范围。

如果直接写:

cpp 复制代码
nums[cur1] > 2 * nums[cur2]

那么乘法可能在 int 范围内发生溢出。

因此写成:

cpp 复制代码
nums[cur1] > 2LL * nums[cur2]

让运算转换为 long long 类型,更安全。

2. 统计和归并使用不同的指针

统计重要翻转对时使用:

cpp 复制代码
cur1、cur2

真正归并时使用:

cpp 复制代码
cur11、cur22

这是因为统计过程会移动指针,但不能影响后面的正常归并。

3. 必须先统计,再归并

递归完成后,左右区间已经有序。

此时先统计跨区间的重要翻转对,再将两个区间合并。

如果先合并,就会破坏原来的左右区间边界,导致统计逻辑混乱。


复杂度分析

归并排序会将数组不断二分,并在每一层进行线性合并。

因此:

text 复制代码
时间复杂度:O(N log N)
空间复杂度:O(N)

其中 O(N) 的额外空间主要来自临时数组 tmp


总结

这道题不能直接使用普通的两重循环,否则时间复杂度为 O(N^2)

归并排序的关键在于:

  1. 递归地将数组划分为左右两个区间;
  2. 保证左右区间有序;
  3. 利用有序性批量统计重要翻转对;
  4. 最后合并两个区间。

本题的核心不是单纯排序,而是利用排序后的区间关系,减少不必要的逐个比较。

相关推荐
白山编程大哥1 小时前
Java 集合算法:从排序、查找到底层原理的实战指南
java·python·算法
shehuiyuelaiyuehao1 小时前
算法34,位运算符操作,总结
算法
Navigator_Z1 小时前
LeetCode //C - 1224. Maximum Equal Frequency
c语言·算法·leetcode
Navigator_Z2 小时前
LeetCode //C++ - 1226. The Dining Philosophers
c语言·算法·leetcode
FOORIR2 小时前
3D视觉+AI客流系统怎么做:从Sensor Pipeline到数据事件引擎
算法
Omics Pro4 小时前
Arc研究所Cell|全新虚拟细胞官方基准评测框架
大数据·人工智能·深度学习·算法·机器学习
白狐_7984 小时前
408 数据结构|红黑树插入:只记两大类
数据结构
CoderYanger6 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先
小雨笙笙6 小时前
机器学习:评估模型与选择
人工智能·算法·机器学习