归并排序之翻转对(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]

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


完整代码

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. 最后合并两个区间。

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

相关推荐
倒头就睡的小比特2 天前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!2 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
猎头南楼3 天前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
m0_547486663 天前
《数据结构教程》全套 PPT课件2026
数据结构
旖旎夜光3 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark3 天前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her13 天前
并查集-听课笔记
笔记·算法·并查集
码流子3 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
another heaven3 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
wzdark3 天前
从算法设计模式看编程思维的抽象能力4
算法