跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

分治算法实战:归并排序与逆序对问题

讲解分治算法在排序及逆序对问题中的应用。内容包括归并排序原理,以及三道 LeetCode 题目:交易逆序对的总数、计算右侧小于当前元素的个数、翻转对。通过递归分治和归并合并过程统计逆序对或翻转对,将时间复杂度从 O(N^2) 优化至 O(NlogN)。提供 Java 完整代码实现及思路分析。

嘘发布于 2026/3/27更新于 2026/5/2834 浏览
分治算法实战:归并排序与逆序对问题

一、归并排序

题目描述:

给定一个整数数组,将其排序。

题目解析:

  • 就是排序数组。

解题思路:

  • 分: 将数组一分为二为两部分,一直分解到数组的长度为 1,使整个数组的排序过程被分为「左半部分排序」+「右半部分排序」;
  • 治: 将两个较短的「有序数组合并成一个长的有序数组」,一直合并到最初的长度。

解题代码:

// 时间复杂度:O(NlogN)
// 空间复杂度:O(n)
class Solution {
    int[] tmp;

    public int[] sortArray(int[] nums) {
        tmp = new int[nums.length];
        mergeSort(nums, 0, nums.length - 1);
        return nums;
    }

    // 归并排序
    public void mergeSort(int[] nums, int l, int r) {
        if (l >= r) return;
        int left = l;
        int right = r;
        int mid = (left + right) / 2;
        // 分
        mergeSort(nums, l, mid);
        mergeSort(nums, mid + 1, r);
        // 治:合并两个有序数组
        int cur1 = left;
        int cur2 = mid + 1;
        int i = left;
        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 (int j = left; j <= right; j++) {
            nums[j] = tmp[j];
        }
    }
}

二、LCR170.交易逆序对的总数

题目描述:

计算数组中逆序对的总数。逆序对是指在该数组元素之后与比它小的元素组成的两个数字。

2.1 分治思想

解题思路:

  • 两段子数组的逆序对和,我们可以通过递归实现。
  • 核心是求一左一右选取逆序对的和。
  • 直接遍历求解复杂度高,如果我们将两段子数组排序,就可以简化统计逻辑。
  • 这与归并排序的逻辑类似。

降序排序:找后一个数组中小的元素个数即可(当 nums[cur2] < nums[cur1] 时统计),即 [mid+1, right]。如果求前一个数组大元素,会有重复。

升序排序:找前一个子数组中大的元素个数即可(当 nums[cur2] < nums[cur1] 时统计),即 [cur1, mid]。如果求后一个数组小元素,会有重复。

数组总逆序对和等于:两段子数组的逆序对和 + 前面一段数组中每个元素在后面一段元素组成的逆序对和(简称为一左一右选逆序对)。

解题代码:

// 时间复杂度:O(NlogN)
// 空间复杂度:O(n)
// 升序版本:
class Solution {
    int[] tmp;

    public int reversePairs(int[] nums) {
        int n = nums.length;
        tmp = new int[n];
        return mergeSort(nums, 0, n - 1);
    }

    public int mergeSort(int[] nums, int left, int right) {
        if (left >= right) return 0;
        int ret = 0;
        int mid = (left + right) / 2;
        // 分:两段子数组的逆序对和
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        // 治:合并两个有序数组 + 统计一左一右逆序对个数
        int cur1 = left, cur2 = mid + 1;
        int i = left;
        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];
        return ret;
    }
}

// 降序版本:
class Solution {
    int[] tmp;

    public int reversePairs(int[] nums) {
        int n = nums.length;
        tmp = new int[n];
        return mergeSort(nums, 0, n - 1);
    }

    public int mergeSort(int[] nums, int left, int right) {
        if (left >= right) return 0;
        int ret = 0;
        int mid = (left + right) / 2;
        // 分:两段子数组的逆序对和
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        // 治:合并两个有序数组 + 统计一左一右逆序对个数
        int cur1 = left, cur2 = mid + 1;
        int i = left;
        while (cur1 <= mid && cur2 <= right) {
            if (nums[cur1] <= nums[cur2]) tmp[i++] = nums[cur2++];
            else {
                ret += (right - cur2 + 1);
                tmp[i++] = nums[cur1++];
            }
        }
        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];
        return ret;
    }
}

2.2 暴力枚举

解题思路:

  • 直接两个 for 循环,第一层遍历数组,第二层遍历后续元素比该元素小的数组个数。
  • 会超时。

解题代码:

// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
    public int reversePairs(int[] record) {
        int ret = 0;
        for (int i = 0; i < record.length; i++) {
            for (int j = i + 1; j < record.length; j++) {
                if (record[i] > record[j]) ret++;
            }
        }
        return ret;
    }
}

三、315.计算右侧小于当前元素的个数

题目描述:

返回一个数组,其中第 i 个元素表示在原始数组中,位于索引 i 右侧且小于 nums[i] 的元素个数。

题目解析:

  • 这道题跟上一题类似,但是需要记录下标对应的结果。
  • 最后返回存个数的数组。

3.1 分治思想

解题思路:

  • 思路与上一题一致,将数组分为两段。每个下标的数组对应值等于:前一段个数 + 后一段个数。
  • 由于求小的个数,使用降序求法更直观,使用升序需考虑边界情况。
  • 当 nums[cur2] < nums[cur1] 时统计 [mid+1, right] 个数,放入结果数组的 nums[cur1] 对应下标位置。
  • 需要将数组中的值与其下标对应,由于数组中元素可以重复,不能使用哈希表。使用一个数组记录每个元素的下标,两个数组绑定,同时移动。
  • 下标数组的移动和原数组的移动是一样的,所以合并时也需要引入临时数组。

解题代码:

// 时间复杂度:O(NlogN)
// 空间复杂度:O(n)
// 降序版本:
class Solution {
    int[] tmpNums;
    int[] tmpIndex;
    int[] ret;
    int[] index;

    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        tmpIndex = new int[n];
        tmpNums = new int[n];
        ret = new int[n];
        // 下标数组
        index = new int[n];
        for (int i = 0; i < n; i++) index[i] = i;
        mergeSort(nums, 0, n - 1);
        List<Integer> list = new ArrayList<>();
        for (int x : ret) list.add(x);
        return list;
    }

    public void mergeSort(int[] nums, int left, int right) {
        if (left >= right) return;
        int mid = (left + right) / 2;
        // 分:两段子数组
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);
        // 治:合并两个有序数组,下标数组同时合并 + 统计个数
        int cur1 = left, cur2 = mid + 1;
        int i = left;
        while (cur1 <= mid && cur2 <= right) {
            if (nums[cur2] >= nums[cur1]) {
                tmpNums[i] = nums[cur2];
                tmpIndex[i++] = index[cur2++];
            } else {
                ret[index[cur1]] += (right - cur2 + 1);
                tmpNums[i] = nums[cur1];
                tmpIndex[i++] = index[cur1++];
            }
        }
        while (cur1 <= mid) {
            tmpNums[i] = nums[cur1];
            tmpIndex[i++] = index[cur1++];
        }
        while (cur2 <= right) {
            tmpNums[i] = nums[cur2];
            tmpIndex[i++] = index[cur2++];
        }
        // 还原
        for (int j = left; j <= right; j++) {
            nums[j] = tmpNums[j];
            index[j] = tmpIndex[j];
        }
    }
}

3.2 暴力枚举

解题思路:

  • 两层 for 循环即可。
  • 会超时。

解题代码:

// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
    public List<Integer> countSmaller(int[] nums) {
        List<Integer> ret = new ArrayList<>();
        for (int i = 0; i < nums.length; i++) {
            int a = 0;
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[j] < nums[i]) a++;
            }
            ret.add(a);
        }
        return ret;
    }
}

四、493.翻转对

题目描述:

计算数组中满足 i < j 且 nums[i] > 2 * nums[j] 的翻转对数量。

4.1 分治思想

解题思路:

  • 依旧是将数组分为两段:数组的总翻转对和等于:两段子数组的翻转对和 + 前面一段数组中每个元素在后面一段元素组成的翻转对和(简称为一左一右选翻转对)。
  • 依旧可以是分升序和降序来求:
    • 升序排序:立足 cur2,找前一个子数组中大的元素个数即可,即 [cur1, mid]。
    • 降序排序:立足 cur1,找后一个数组中小的元素个数即可,即 [mid+1, right]。
  • 由于判断条件与合并是不一样的,所以单独写一个循环来计算。
  • 每个元素不会超出 int 范围,但是乘 2 就有可能会超出,所以判断条件写 nums[cur1] / 2.0 <= nums[cur2]。

解题代码:

// 时间复杂度:O(NlogN)
// 空间复杂度:O(n)
// 升序版本:
class Solution {
    int[] tmp;

    public int reversePairs(int[] nums) {
        tmp = new int[nums.length];
        return mergeSort(nums, 0, nums.length - 1);
    }

    public int mergeSort(int[] nums, int left, int right) {
        if (left >= right) return 0;
        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;
        int i = left;
        while (cur2 <= right) {
            while (cur1 <= mid && nums[cur1] / 2.0 <= nums[cur2]) cur1++;
            if (cur1 > mid) break;
            ret += (mid - cur1 + 1);
            cur2++;
        }
        // 治:合并两个有序数组
        cur1 = left; cur2 = mid + 1;
        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 (int j = left; j <= right; j++) {
            nums[j] = tmp[j];
        }
        return ret;
    }
}

// 降序版本:
class Solution {
    int[] tmp;

    public int reversePairs(int[] nums) {
        tmp = new int[nums.length];
        return mergeSort(nums, 0, nums.length - 1);
    }

    public int mergeSort(int[] nums, int left, int right) {
        if (left >= right) return 0;
        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;
        int i = left;
        while (cur1 <= mid) {
            while (cur2 <= right && nums[cur1] / 2.0 <= nums[cur2]) cur2++;
            if (cur2 > right) break;
            ret += (right - cur2 + 1);
            cur1++;
        }
        // 治:合并两个有序数组
        cur1 = left; cur2 = mid + 1;
        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 (int j = left; j <= right; j++) {
            nums[j] = tmp[j];
        }
        return ret;
    }
}

4.2 暴力枚举

解题思路:

  • 直接两层 for 循环即可。
  • 会超时。

解题代码:

// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
    public int reversePairs(int[] nums) {
        int ret = 0;
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] / 2.0 > nums[j]) ret++;
            }
        }
        return ret;
    }
}

目录

  1. 一、归并排序
  2. 二、LCR170.交易逆序对的总数
  3. 2.1 分治思想
  4. 2.2 暴力枚举
  5. 三、315.计算右侧小于当前元素的个数
  6. 3.1 分治思想
  7. 3.2 暴力枚举
  8. 四、493.翻转对
  9. 4.1 分治思想
  10. 4.2 暴力枚举
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 字节跳动前端一面深度解析:React 原理与浏览器渲染
  • OpenClaw 构建飞书 AI 办公机器人:本地 Ollama 接入与 Skills 自动化
  • DooTask 轻量级项目管理与 AI 协同功能解析
  • 执行式 AI 入门:API 调用与网络请求基础
  • MySQL 数据类型核心指南:选型、实战与避坑
  • Seq2Seq 模型实战:ScheduledEmbeddingTrainingHelper 原理与使用
  • SpringBoot 4.0 新特性整合项目实战应用详解
  • Cursor 集成 MCP 服务实战指南:从配置到自动化任务执行
  • pyenv-win Python 多版本管理实战与效率优化方案
  • JavaScript 调试技巧与实用工具指南
  • 基于 GLM4.7 的 Claude Code GitHub 代码自动审查
  • AI 对话页流式处理架构:Web Streams 与 Fetch API 实践
  • AIGC 产品经理工作职责与职位要求解析
  • Spring Boot 全局异常处理与日志监控实战
  • Linux 命名管道(FIFO)通信:原理与跨进程实战
  • Java 大数据量 Excel 导入导出实现方案
  • Rust 异步微服务架构最佳实践与反模式规避
  • Mac mini M4 部署 OpenClaw + Ollama 本地大模型接入飞书机器人
  • Z-Image-Turbo 孙珍妮模型使用指南
  • Python 3.8+ 海象运算符详解

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online