跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

归并排序实战:计算右侧小于当前元素个数与翻转对

归并排序是解决此类问题的核心方法。详细讲解了如何计算数组中每个元素右侧比它小的数的个数,以及满足特定条件的翻转对数量。两者均利用分治思想,在合并有序子数组时进行计数。前者需维护原始索引以定位结果,后者需在合并前单独统计满足两倍关系的对数,时间复杂度均为 O(n log n)。

灵魂伴侣发布于 2026/3/20更新于 2026/4/251 浏览
归并排序实战:计算右侧小于当前元素个数与翻转对

归并排序实战:计算右侧小于当前元素个数与翻转对

在算法面试中,涉及数组统计的问题往往需要高效的解决方案。今天我们来深入探讨两个经典的 LeetCode 题目,它们都可以通过**归并排序(Merge Sort)**的分治思想来解决。

49. 计算右侧小于当前元素的个数

题目描述

给定一个整数数组 nums,按要求返回一个新数组 counts。其中 counts[i] 表示在 nums[i] 右边且比它小的元素个数。

例如输入 [5, 2, 6, 1],输出应为 [2, 1, 1, 0]。

核心思路

这道题和求逆序对非常相似,但区别在于我们需要知道每个位置对应的具体数量,而不是总数。

归并排序在合并两个有序子数组时,如果左边的元素大于右边的元素,说明左边这个元素后面有若干个比它小的数。但在排序过程中,元素的原始下标会发生变化,因此我们需要维护一个辅助数组来记录每个元素原本的索引。

实现代码

class Solution {
public:
    vector<int> counts;
    vector<int> index; // 记录 nums 中当前元素的原始下标
    vector<int> tmp;
    vector<int> tmpi; // 临时记录合并后对应元素位置变化下标随之变化的数组

    vector<int> countSmaller(vector<int>& nums) {
        counts.resize(nums.size());
        index.resize(nums.size());
        tmp.resize(nums.size());
        tmpi.resize(nums.size());
        
        // 初始化 index,使得 nums 每个元素下标和 index 一一对应
        for(int i = 0; i < nums.size(); i++) {
            index[i] = i;
        }
        mergesort(nums, 0, nums.size() - 1);
        return counts;
    }

    void mergesort(vector<int>& nums, int left, int right) {
        if(left == right) {
            return;
        }
        int mid = (right - left) / 2 + left;
        mergesort(nums, left, mid);
        mergesort(nums, mid + 1, right);
        
        int cur1 = left, cur2 = mid + 1, i = 0;
        while(cur1 <= mid && cur2 <= right) {
            if(nums[cur1] > nums[cur2]) {
                // 重点:因为 index[cur1] 始终是 cur1 当前元素的原始下标
                // 所以不管当前元素位置怎么变化,counts[index[cur1]] 的位置都不会变
                counts[index[cur1]] += right - cur2 + 1;
                tmpi[i] = index[cur1];
                tmp[i++] = nums[cur1++];
            } else {
                tmpi[i] = index[cur2];
                tmp[i++] = nums[cur2++];
            }
        }
        while(cur1 <= mid) {
            tmpi[i] = index[cur1];
            tmp[i++] = nums[cur1++];
        }
        while(cur2 <= right) {
            tmpi[i] = index[cur2];
            tmp[i++] = nums[cur2++];
        }
        for(int j = left; j <= right; j++) {
            nums[j] = tmp[j - left];
            index[j] = tmpi[j - left];
        }
    }
};

流程解析

在合并阶段,当 nums[cur1] > nums[cur2] 时,意味着 nums[cur1] 及其左侧剩余的所有元素都比 nums[cur2] 大。由于右半部分已经有序,从 cur2 到 right 的所有元素都小于 nums[cur1],数量为 right - cur2 + 1。通过 index 数组,我们可以准确地将这个计数累加到原数组对应位置的 counts 中。


50. 翻转对

题目描述

给定一个数组 nums,如果 i < j 且 nums[i] > 2 * nums[j],则称 (i, j) 为一个翻转对。返回数组中翻转对的总数。

核心思路

翻转对和逆序对的定义大同小异,只是条件变成了 nums[i] > 2 * nums[j]。我们依然可以使用归并排序的思想,将问题分解为三部分:左半区间翻转对的数量、右半区间翻转对的数量、一左一右选择时翻转对的数量。

关键区别在于:上一道题可以在合并的同时计算,但这道题要求的是左边元素大于右边元素的两倍。如果在合并过程中直接操作,可能会破坏顺序导致无法快速判断。因此,我们需要先统计满足条件的翻转对数量,再进行归并排序。

实现代码

class Solution {
public:
    vector<int> tmp;
    int count = 0;

    int reversePairs(vector<int>& nums) {
        tmp.resize(nums.size());
        mergesort(nums, 0, nums.size() - 1);
        return count;
    }

    void mergesort(vector<int>& nums, int left, int right) {
        if(left >= right) {
            return;
        }
        int mid = (right - left) / 2 + left;
        mergesort(nums, left, mid);
        mergesort(nums, mid + 1, right);
        
        // 先统计翻转对数量
        int cur1 = left, cur2 = mid + 1;
        while(cur1 <= mid && cur2 <= right) {
            // 当左边大于右边的两倍才会进行求值
            if((long long)nums[cur1] > 2LL * nums[cur2]) {
                count += right - cur2 + 1;
                cur1++;
            } else {
                cur2++;
            }
        }
        
        // 再执行标准的归并排序合并
        cur1 = left;
        cur2 = mid + 1;
        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(int i = left; i <= right; i++) {
            nums[i] = tmp[i - left];
        }
    }
};

流程解析

这里需要注意类型转换,使用 long long 防止乘法溢出。统计阶段利用了两个子数组分别有序的特性:对于左边的每一个 cur1,找到第一个满足 nums[cur1] <= 2 * nums[cur2] 的 cur2,那么 cur2 之后的所有元素都满足翻转对条件。统计完成后,再像普通归并排序一样合并数组,保证上一层递归能继续处理。


总结

这两道题展示了归并排序在统计类问题中的强大能力。49 题通过维护原始索引解决了动态位置下的计数问题;50 题则通过分离'统计'与'合并'步骤,巧妙处理了非单调的比较条件。掌握这种分治策略,面对类似的逆序对变种问题时就能游刃有余。

目录

  1. 归并排序实战:计算右侧小于当前元素个数与翻转对
  2. 49. 计算右侧小于当前元素的个数
  3. 题目描述
  4. 核心思路
  5. 实现代码
  6. 流程解析
  7. 50. 翻转对
  8. 题目描述
  9. 核心思路
  10. 实现代码
  11. 流程解析
  12. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • Linux 初始网络(下):局域网通信与跨网段传输原理
  • 前端核心面试题与实战知识点梳理
  • 算法基础:滑动窗口技巧与经典例题解析
  • AI Agent 新范式:FastGPT 集成 MCP 协议构建工具增强智能体
  • C++ 二叉搜索树实现详解
  • 数据结构基础:顺序表详解与动态实现
  • Qwen3+Qwen Agent 智能体开发实战:接入 MCP 工具(一)
  • C++ 二叉搜索树(BST)原理及核心操作实现
  • 数据结构详解:顺序表原理与实现
  • OpenClaw 安装部署与渠道接入指南
  • Linux 基础 IO:深入理解文件描述符机制
  • Linux 文件描述符与重定向实战:从原理到 minishell 实现
  • DeepSeek 深度使用指南:提示词技巧与本地知识库搭建
  • C++ 继承:面向对象代码复用的核心机制
  • 深入剖析 Spring 框架:架构、缺陷与演进之路
  • 数据结构与算法:链表分类详解与双向链表初始化实现
  • LeetCode Hot 100 链表经典题目实战解析
  • Python Pandas 库核心用法详解
  • C++ 继承机制详解:从基础概念到多继承实战
  • Linux 网络基础入门(上)

相关免费在线工具

  • 加密/解密文本

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

  • Gemini 图片去水印

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

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online