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

C++ 分治 归并排序解决问题 力扣 LCR 170. 交易逆序对的总数 题解 每日一题

文章目录

  • 题目描述
  • 为什么这道题值得你花几分钟时间看懂?
  • 算法原理
    • 为什么可以用归并来解决这个问题
      • 细节分析:排序不会影响计数准确性吗?
    • 如何用归并来解决
  • 代码实现
    • 代码优化思路:全局复用临时数组
    • 时间复杂度与空间复杂度分析
  • 总结
  • 下题预告

在这里插入图片描述
在这里插入图片描述

题目描述

题目链接:力扣 LCR 170. 交易逆序对的总数

题目描述:
在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。

示例 1:
输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。

提示:
0 <= record.length <= 50000

为什么这道题值得你花几分钟时间看懂?

这道题是归并排序的“进阶实战题”,更是帮你打通“分治思想”从“排序”到“实际统计问题”的关键桥梁,花几分钟吃透它,收获远不止解对一道题:

  1. 深化分治思想的灵活应用:普通归并排序只需要“拆分-合并”完成排序,这道题则要求在合并过程中“顺带”统计逆序对,能帮你理解分治思想的核心——不仅能解决排序这类“处理型问题”,还能解决逆序对统计这类“计数型问题”,拓宽算法应用思路。

  2. 掌握“排序+统计”的高效结合技巧:暴力解法O(n²)超时是必然,这道题的核心价值在于教会你“如何利用排序的有序性优化统计效率”——把原本O(n²)的计数操作,通过归并排序的有序特性降为O(nlogn),这种“借排序优化统计”的思路,能迁移到很多类似的“成对计数”问题中。

  3. 攻克归并排序的核心疑问:很多人刚学归并解决逆序对时,都会困惑“排序会不会打乱原始顺序,导致计数出错”。吃透这道题,能彻底搞懂“归并排序中子数组排序不影响跨数组逆序对计数”的本质,帮你真正掌握归并排序的底层逻辑,而不是死记硬背代码。

这道题不是简单的“排序变种”,而是检验你是否真正理解分治思想的“试金石”,学会它,你对归并排序的理解会直接上一个台阶~

特别忠告:一定要先把归并排序的基础逻辑(拆分规则、合并步骤、递归终止条件)完全弄明白,再看这篇博客。归并排序是理解逆序对统计的前提,基础不牢固会导致后续算法原理的讲解出现理解断层,反而浪费更多时间,对归并排序有遗忘的朋友可以看我的上一篇博客C++ 分治 归并排序 力扣 912. 排序数组 其中详细讲解了归并排序的原理及实现步骤。

算法原理

为什么可以用归并来解决这个问题

在最开始的时候,我只想到了暴力解法但是很显然一个困难题目如果能用两个for暴力解出来就太不正常了,事实证明这道题的数据范围是 0 <= record.length <= 50000 暴力时间复杂度是O(n2)大概率超时,所以一定有别的方法来解决这道题。

而归并排序的“拆分-合并”逻辑,恰好能高效统计逆序对,核心在于“利用有序性优化计数”,我们一步步拆解其中的逻辑,至于原因我们一点一点说(最开始属实我也没想到)。

1.逆序对的统计逻辑:拆分后分三类计数
我们计算逆序对数无非就是两个元素对比计数,那么一个数组可以计算逆序数,那么我们也可以计算两个数组的逆序数,如下图👇:
在这里插入图片描述
此时,我们就可以和归并排序的思路进行了简单重合,别急最终答案一定不是reta + retb,对我们还差一步统计数组a和b两个数组中所有的逆序对数等于retc,那么我们就可以得到最终逆序对数为reta + retb + retc(左边的逆序对 + 右边的逆序对 + (左子数组元素 > 右子数组元素的跨数组逆序对))但是这样本质还是暴力枚举时间并没有一点改进,关键在于下一步的优化。

2.优化计数:有序子数组让跨数组计数变高效
我们目前的困境就是解决效率问题,基于上面我们的摸索其实可以在 一左一右的逆序对 的时候进行优化,暴力中我们是左面找一个元素右面找一个元素比较计数,时间复杂度是O(n2),但是如果数组是有序的那么计数可以通过规律变成O(n),往往相较于数组无序的一个一个比较O(n2)计数的效率高,如下图👇:
在这里插入图片描述
那么我们这样改进呢?左边的逆序对 ➡ 对左边进行排序 ➡ 右边的逆序对 ➡ 对右边进行排序 ➡ 一左一右的逆序对,那就能够将时间复杂度降低到O(nlogn),我知道你现在一定有疑问我们下面来解答。

细节分析:排序不会影响计数准确性吗?

那么现在你现在一定右上面的疑问,我最开始也有这个疑问,我们明明求得是固定顺序的数组中的逆序数,你给排序了岂不是打乱了顺序吗,计数还对吗?
答案是:完全不影响计数的准确度

这个问题的核心在于理解:归并排序中对「子数组的排序」只改变了子数组内部的元素顺序,但完全不影响「原始数组中逆序对的总数统计」。这看似反直觉,其实我们从「逆序对的定义」和「排序的作用范围」两个角度看就能理解:

逆序对的定义是「基于原始数组的位置关系」,而非元素值本身
逆序对的本质是「原始数组中,位置在前的元素 > 位置在后的元素」。例如 [9,7,5,4,6] 中,(9,7) 是逆序对,因为在原始数组中 9 位于 7 之前且更大。

归并排序的递归分割始终基于「原始数组的位置」:

  • 无论子数组如何排序,左子数组的所有元素在原始数组中都位于右子数组元素的左侧(例如左子数组 [9,7] 的元素在原始数组中都在右子数组 [5,4,6] 之前)。
  • 因此,「左子数组元素 > 右子数组元素」的逆序对,其「前后位置关系」是原始数组决定的,与子数组是否排序无关。

子数组排序仅影响「内部顺序」,但内部逆序对已提前统计
归并排序的步骤是 左边的逆序对 ➡ 对左边进行排序 ➡ 右边的逆序对 ➡ 对右边进行排序 ➡ 一左一右的逆序对,所以并不影响内部因为我们是统计完才排序:

  • 第一步:递归处理左、右子数组,统计它们内部的逆序对(例如 [9,7] 内部的 (9,7) 在排序前就已被统计)。
  • 第二步:对左、右子数组排序(例如 [9,7] 排为 [7,9][5,4,6] 排为 [4,5,6])。此时子数组内部的顺序改变了,但内部逆序对已经统计完毕,后续不会再处理。
  • 第三步:合并两个排序后的子数组,统计「左元素 > 右元素」的跨数组逆序对。由于子数组已排序,此时可以高效计算(例如左 [7,9] 和右 [4,5,6] 中,7>4 时,左数组中 7 及其右侧的 9 都大于 4,对应原始数组中 (7,4)(9,4) 两个逆序对)。

排序的作用:让「跨数组逆序对的统计」变高效,而非改变逆序对本身
子数组排序后,元素的相对顺序改变了,但:

  • 左子数组中任意元素在原始数组中的位置,仍然都在右子数组元素之前(位置关系不变)。
  • 因此,「左元素 > 右元素」的数量,与原始数组中左子数组元素 > 右子数组元素的数量完全一致(只是通过排序让统计更方便)。

例如,原始左子数组 [9,7] 和右子数组 [5,4,6] 中,跨数组逆序对是 (9,5), (9,4), (9,6), (7,5), (7,4), (7,6) 共 6 个。
排序后左 [7,9]、右 [4,5,6] 合并时,统计的仍是这 6 个(只是计算方式从逐个比较变成了利用有序性批量计算)。

所以按照我们的思路,子数组的排序只影响内部元素的顺序,但不改变原始数组中「左子数组元素在右子数组元素之前」的位置关系。而逆序对的统计基于:

  • 子数组内部的逆序对(排序前已统计)。
  • 跨数组的逆序对(位置关系不变,排序后高效统计)。

因此,排序操作既不会遗漏也不会重复计算逆序对,最终结果与原始数组的逆序对总数完全一致,可以拿示例一来对比的画下👇:
在这里插入图片描述

如何用归并来解决

我们上面已经说明了,这个方法是正确,在解法和思路上与归并高度重合,这就是为什么这道题可以用归并来做,不同点就是我们要进行计数,如下图👇我们会有两种可能:
在这里插入图片描述

  1. 拆分(Divide):找到数组中间位置mid,将数组拆分为左子数组(left ~ mid)和右子数组(mid+1 ~ right),递归处理左右子数组,统计各自内部的逆序对。
  2. 合并(Merge)
    • 准备两个指针cur1(左子数组起始)、cur2(右子数组起始),以及临时数组tmp存储合并结果。
    • 对比record[cur1]和record[cur2]:
      • 若record[cur1] <= record[cur2]:直接将record[cur1]加入临时数组,cur1后移(无逆序对)。
      • 若record[cur1] > record[cur2]:将record[cur2]加入临时数组,cur2后移,同时统计逆序对——左子数组中cur1及右侧所有元素都大于record[cur2],逆序对数量增加“mid - cur1 + 1”。
    • 将剩余元素加入临时数组,再把临时数组的结果复制回原数组,完成合并。
  3. 返回总计数:递归过程中累计左、右、跨数组的逆序对数量,最终返回总数。

代码实现

```cpp
class Solution {
public:vector<int> tmp; // 全局临时数组,避免递归中频繁创建,优化空间开销int reversePairs(vector<int>& record) {tmp.resize(record.size()); // 初始化临时数组,仅分配一次内存return Msort(record, 0, record.size() - 1);}// 递归函数:处理[left, right]区间,返回该区间的逆序对总数int Msort(vector<int>& record, int left, int right) {if (left >= right) return 0; // 递归终止:子数组长度为1或0,无逆序对// 1. 拆分:递归处理左右子数组,统计内部逆序对int mid = left + (right - left) / 2; // 避免(left+right)溢出int ret = 0;ret += Msort(record, left, mid);     // 左子数组内部逆序对ret += Msort(record, mid + 1, right); // 右子数组内部逆序对// 2. 合并:统计跨数组逆序对,并合并为有序数组int cur1 = left, cur2 = mid + 1; // cur1:左子数组指针,cur2:右子数组指针int i = 0; // 临时数组的索引while (cur1 <= mid && cur2 <= right) {if (record[cur1] > record[cur2]) {// 左元素>右元素,统计逆序对:左子数组cur1及右侧元素都大于当前右元素ret += mid - cur1 + 1;tmp[i++] = record[cur2++];} else {// 无逆序对,直接加入左元素tmp[i++] = record[cur1++];}}// 处理左子数组剩余元素while (cur1 <= mid) {tmp[i++] = record[cur1++];}// 处理右子数组剩余元素while (cur2 <= right) {tmp[i++] = record[cur2++];}// 将合并后的有序结果复制回原数组for (int k = left; k <= right; k++) {record[k] = tmp[k - left]; // 临时数组从0开始,对应原数组left位置}return ret;}
};

代码优化思路:全局复用临时数组

归并排序的空间开销主要来自临时数组,这里采用“全局复用”的优化策略,相比“递归内创建临时数组”更高效:

  • 优化点:在reversePairs函数中初始化一次临时数组,递归过程中反复复用,避免每次合并都创建新数组,减少内存分配和释放的开销。
  • 优势:对于题目中“50000”的数组规模,这种优化能明显提升执行效率,同时保证空间复杂度仍为O(n)(临时数组长度固定为原数组长度)。

时间复杂度与空间复杂度分析

  • 时间复杂度:O(nlogn)。数组拆分的深度为logn(如长度n的数组需拆log2n层),每一层的合并与计数操作都需要遍历当前层的所有元素(O(n)),因此总时间复杂度为O(nlogn)。
  • 空间复杂度:O(n)。核心开销是全局临时数组(O(n)),递归栈的开销为O(logn)(远小于临时数组),因此总空间复杂度由临时数组决定,为O(n)。

总结

  1. 核心思路:利用归并排序的“拆分-合并”逻辑,将逆序对统计拆分为“左内部、右内部、跨数组”三类,借助有序子数组的特性,把跨数组计数从O(n²)优化为O(n),最终实现O(nlogn)的高效解法。
  2. 关键理解:子数组排序不影响逆序对计数的准确性,因为排序在“内部逆序对统计之后”,且不改变“左子数组元素在右子数组元素之前”的原始位置关系。
  3. 优化技巧:全局复用临时数组,避免递归中频繁创建销毁数组,降低内存操作开销,提升执行效率。
  4. 迁移价值:这种“排序+统计”的结合思路,可迁移到其他需要统计“成对元素关系”的问题中,比如统计数组中“右边元素比左边小k”的对数等,是分治思想的典型应用。

下题预告

下一篇我们将聚焦 力扣 315. 计算右侧小于当前元素的个数——这道题堪称分治思想的“深度应用场”!刚掌握归并排序解决逆序对的核心逻辑,正好用它来进阶实战:它不仅能帮你进一步强化“排序中统计”的解题思维,还能让你学会如何将“右侧小于当前元素”的问题,转化为更熟悉的逆序对相关场景,打通算法灵活转化的关键链路。

Doro 又又又带着小花🌸来啦!🌸奖励🌸看到这里的你!如果这篇内容帮你理清了归并排序的实战细节,或是让你对“排序+统计”的思路更清晰,别忘了点赞支持呀!把它收藏起来,以后复习这类问题时翻出来,就能快速回忆起核心逻辑~关注我,我会持续更新算法系列内容,有什么解题思路或疑问我们随时讨论,对算法感兴趣的朋友也可以去我的算法专辑看看,里面还有更多经典算法题等着你解锁!

http://www.dtcms.com/a/549801.html

相关文章:

  • 贵州省住房与城乡建设部网站国内论坛网站有哪些
  • React 状态管理库相关收录
  • 深圳手机网站设计公司wordpress外链404
  • C/C++中的二级指针使用
  • 用dw做红米网站网站管理助手v3
  • 网站建设电话话术有趣软文广告经典案例
  • Fetch API 返回值获取方法
  • 机器学习-导师优选
  • 做视频网站要准备哪些资料阿里虚拟机建设网站
  • 使用局域网做网站百度手机助手网页
  • VMware-三种网络模式
  • 【weblogic】文件上传漏洞
  • 用 Rust 写一个前端项目辅助工具:JSON 格式化器
  • OrionX GPU池化社区版永久免费,算力管理焕新升级!
  • Rust 控制流深度解析:安全保证与迭代器哲学
  • 异常处理机制
  • 一元云淘网站开发android开发最全教程
  • 第 18 天:Web 服务器(Apache、Nginx、反向代理)
  • 郑州网站运营实力乐云seo如何从下载的视频查到原网站
  • nodejs有几种模块模式
  • 非法网站怎么推广海口专业的网站开发
  • 网站建设实训心得与建议安徽省工程建设信息网职称查询
  • 【高阶数据结构】AVL树
  • 三明 网站建设如何建立自己的
  • 可以做兼职的动漫网站公司网站想维护服务器
  • Go语言设计模式:桥接模式详解
  • 前端(Vue3)如何接收后端(SpringBoot)返回的文件并下载
  • 低空经济网络安全体系
  • 福建省建设资格注册中心网站东莞网站推广技巧
  • 汉阳做网站多少钱网站服务器时间查询工具