当前位置: 首页 > news >正文

7.5 分治:归并:LeetCode 剑指 Offer 51.数组中的逆序对


归并排序巧解逆序对问题:LeetCode 剑指 Offer 51.数组中的逆序对


1. 题目链接

剑指 Offer 51. 数组中的逆序对
题目要求:给定一个整数数组 record,返回数组中逆序对的总数。逆序对定义为 i < jrecord[i] > record[j]


2. 题目描述
  • 输入:整数数组 record,例如 [7,5,6,4]
  • 输出:逆序对总数,例如 5(对应逆序对:(7,5), (7,6), (7,4), (5,4), (6,4))。
  • 约束条件
    • 0 ≤ 数组长度 ≤ 50000
    • 结果可能很大,需对 1e9+7 取模(本题代码未处理,需根据题目要求补充)。

3. 示例分析

示例
输入:record = [7,5,6,4]
输出:5

分步解析

  1. 分解数组

    • 初始数组分为 [7,5][6,4]
    • 左子数组 [7,5] 的逆序对为 1(7 > 5)。
    • 右子数组 [6,4] 的逆序对为 1(6 > 4)。
  2. 合并过程统计

    • 合并左数组 [5,7] 和右数组 [4,6]
      • 比较 545 > 4 → 逆序对增加 2(左数组剩余元素 5,7 均大于 4)。
      • 比较 565 ≤ 6 → 无新增。
      • 比较 767 > 6 → 逆序对增加 1(左数组剩余元素 7 大于 6)。
    • 合并阶段总逆序对:2 + 1 = 3
  3. 总计1(左) + 1(右) + 3(合并) = 5


4. 算法思路
归并排序与逆序对统计的结合
  1. 分治策略

    • 分解数组为左右子数组,递归计算各自的逆序对数量。
    • 合并时统计跨越左右子数组的逆序对数量。
  2. 合并阶段的关键操作

    • 当左子数组元素 nums[cur1] > nums[cur2] 时,左子数组剩余元素(从 cur1mid)均大于 nums[cur2],逆序对增加 mid - cur1 + 1
步骤拆解
  1. 递归分解:将数组二分至单个元素。
  2. 逆序对累加
    • 左子数组内部逆序对数目。
    • 右子数组内部逆序对数目。
    • 合并时左右之间的逆序对数目。
  3. 合并排序:确保合并后的子数组有序,为后续合并提供正确状态。

5. 边界条件与注意事项
  1. 递归终止条件: 当 left >= right 时,子数组长度为0或1,无逆序对。

  2. 索引越界问题:中间点计算 mid = (left + right) / 2 可能导致溢出,建议改为 left + (right - left) / 2

  3. 临时数组管理: 预分配临时数组 tmp 的空间,避免递归中频繁分配内存。

  4. 稳定性处理:合并时若 nums[cur1] == nums[cur2],优先插入左元素,但逆序对不增加。

  5. 复杂度分析

    • 时间复杂度:O(n log n),归并排序的时间复杂度。
    • 空间复杂度:O(n),临时数组和递归栈空间。

6. 代码实现与解析
class Solution {
public:
    vector<int> tmp; // 全局临时数组,用于合并阶段
    int reversePairs(vector<int>& record) {
        tmp.resize(record.size()); // 预分配空间
        return mergeSort(record, 0, record.size() - 1);
    }

    int mergeSort(vector<int>& nums, int left, int right) {
        if (left >= right) return 0; // 终止条件:子数组长度为0或1

        int mid = (left + right) / 2; // 计算中间点
        int ret = 0;
        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]) {
                tmp[i++] = nums[cur1++]; // 左元素小,无逆序对
            } 
            else {
                ret += mid - cur1 + 1;   // 统计逆序对
                tmp[i++] = nums[cur2++];
            }
        }

        // 处理剩余元素(不产生新逆序对)
        while (cur1 <= mid) tmp[i++] = nums[cur1++];
        while (cur2 <= right) tmp[i++] = nums[cur2++];

        // 将排序后的数据复制回原数组
        for (int j = left; j <= right; j++) {
            nums[j] = tmp[j - left];
        }

        return ret;
    }
};
代码逐行解析
  1. 全局临时数组 tmp

    • 预分配空间,避免递归中频繁申请内存。
  2. 递归分解与统计

    • mergeSort 返回当前区间的逆序对总数,累加左右子数组的结果。
  3. 合并阶段的核心逻辑

    • 逆序对统计:当 nums[cur1] > nums[cur2] 时,左子数组中剩余元素均构成逆序对,数量为 mid - cur1 + 1
    • 稳定性处理:使用 <= 保证相等元素不触发逆序对统计。
  4. 剩余元素处理

    • 剩余元素直接追加到临时数组,不产生新逆序对。
  5. 数据回写

    • 将临时数组的数据复制回原数组的 [left, right] 区间,确保后续合并正确。

总结

归并排序在合并阶段天然适合统计逆序对:左、右子数组有序时,可以高效计算跨越两部分的逆序对数量。通过分治策略,算法的时间复杂度为 O(n log n),优于暴力法的 O(n²)。代码中需注意中间点计算的潜在溢出问题,以及临时数组的预分配优化。掌握这一方法,可解决多数分治类统计问题(如翻转对、右侧更小元素等)。

相关文章:

  • docker 搭建部署知识库XWIKI
  • 【嵌入式学习3】信息安全 - SSH协议
  • JAVA反序列化深入学习(六):CommonsCollections4
  • jQuery 入门到精通
  • Vue中使用antd-table组件时,树形表格展开配置不生效-defaultExpandedRowKeys-默认展开配置不生效
  • 经典算法 排列的字典序问题
  • 为什么需要 Node.js 的 URL 处理工具?
  • JavaScript 函数参数详解
  • 后端实现加解密工具类(记录)
  • MySQL增删改查(CRUD)操作详解与实战指南
  • Java EE(17)——网络原理——IP数据报结构IP协议解析(简述)
  • TabularDataset
  • 蓝桥杯备考----》完全背包模板
  • HarmonyOs学习 实验四:开发一个登录界面
  • Electron 系统托盘与屏幕捕捉深度解析:从基础到企业级实践
  • 【Linux】网络概念
  • STM32F103_LL库+寄存器学习笔记13 - 梳理外设CAN与如何发送CAN报文(串行发送)
  • [Vue2]侦听器watch(监视器)
  • Android JobScheduler调度任务面试题及参考答案
  • 01_现代C++特殊成员函数
  • php网站后台上传图片有没有推荐到首页的功能/电商运营方案计划书
  • 社交网站开发注意事项/资源企业网站排名优化价格
  • 公司网站建设费用怎么入账/泉州关键词优化排名
  • 青岛网站建设/新闻 最新消息
  • 公司网站的建设怎么做/深圳网络营销推广专员
  • 直播网站开发方案ppt/互联网营销师考试