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

C++ 分治 归并排序 归并排序VS快速排序 力扣 912. 排序数组 题解 每日一题

文章目录

  • 题目描述
  • 为什么这道题值得咱们二刷?
  • 算法原理
    • 归并排序基本思路
    • 归并排序 vs 快速排序:核心差异对比
  • 代码实现
    • 临时数组的复用策略
    • 两种方案效率对比
    • 时间复杂度与空间复杂度分析
  • 总结
  • 下题预告

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

题目描述

题目链接:力扣 912. 排序数组

题目描述:
给你一个整数数组 nums,请你将该数组升序排列。你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。

示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
解释:数组排序后,某些数字的位置没有改变(例如,2 和 3),而其他数字的位置发生了改变(例如,1 和 5)。

示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
解释:请注意,nums 的值不一定唯一。

提示:
1 <= nums.length <= 5 * 104
-5 * 104 <= nums[i] <= 5 * 104

为什么这道题值得咱们二刷?

在之前的博客 力扣 912. 排序数组 中我们用快排解决这道题时,我们掌握了“原地分区、空间高效”的排序思路,我们今天二刷这道题的目的是通过这道题来学习归并排序的思路,并且比较二者优劣,通过理解两种解法进而实现互补,让我们构建更完整的排序算法认知——快排和归并各有不可替代的价值。

1. 掌握两种“O(nlogn)”算法的场景适配能力
快排和归并的核心差异,决定了它们在不同场景下的适用性,二刷能帮我们建立“按需选算法”的意识:

  • 若场景要求“空间尽可能小”(如内存紧张的嵌入式开发),快排的平均O(logn)空间开销更有优势;
  • 若场景不能接受“时间退化风险”(如金融数据排序,需稳定高效),归并排序无论数据分布如何,都能稳定保持O(nlogn),更符合需求;
  • 若场景需要“稳定排序”(如按成绩排序后,保留同分数学生的报名顺序),归并的稳定性是快排无法替代的关键特性。
    只有同时掌握二者,才能在不同需求下做出最优选择,而不是只会一种解法“硬套”。

2. 补全“算法优化”的不同维度认知
快排和归并的优化方向各有侧重,二刷能帮你拓宽优化思路:

  • 快排的优化集中在“避免分区失衡”:比如随机选基准、三指针处理重复元素,核心是解决“极端场景下的时间退化”问题;
  • 归并的优化集中在“减少空间开销”:比如全局复用临时数组,避免递归中频繁创建销毁数组,核心是解决“内存操作效率”问题。
    两种优化思路覆盖了“时间稳定性”和“空间效率”两个关键维度,学会后能迁移到其他算法的优化中(如动态规划的空间压缩、搜索算法的剪枝)。

3. 为后续复杂问题铺垫互补的知识基础
快排和归并的应用场景各有延伸,二者都学能降低后续学习成本:

  • 快排的“分区思想”是解决Top K问题(如第K个最大元素)的核心,能实现O(n)的平均时间复杂度;
  • 归并的“拆分-合并逻辑”是解决链表排序(如排序链表)的最优选择——链表的指针特性可避免归并的数组空间开销,实现O(logn)的空间复杂度。
    现在通过同一道题掌握两种算法,后续遇到这些延伸问题时,能直接复用已学思路,无需从零开始理解。

二刷不是“重复做题”,而是通过归并排序这个“补充视角”,把快排未覆盖的“稳定性、场景适配、合并型分治”等知识点补全。快排和归并就像排序算法里的“左右手”,单独会一只不够,两只都会才能应对更多复杂需求——这才是二刷的核心价值。

算法原理

我们回归这道题在“不依赖内置函数、O(nlogn) 时间、空间尽可能小”的要求下,归并排序是除快排外的另一重要选择。它虽在空间开销上略高于快排,但胜在时间性能稳定、实现逻辑直观,且具备“稳定性”这一关键特性。

归并排序基本思路

归并排序的核心是“先拆分、再合并”,通过将大问题拆解为小问题,逐一解决后再整合,具体分为“拆分”和“合并”两步。

1. 拆分(Divide):将数组拆分为最小子问题
先找到数组的中间位置 mid,将数组分为左子数组(left ~ mid)和右子数组(mid+1 ~ right)。之后进行递归拆分左子数组和右子数组,直到子数组长度为 0 或 1(长度为 1 的数组天然有序,无需再拆分)。

如下图👇:
在这里插入图片描述
2. 合并(Merge):将有序子数组合并为大数组
准备一个临时数组mark,用于存储合并后的有序结果,用两个指针l,r分别指向左子数组和右子数组的起始位置,逐元素比较大小,将更小的元素通过指针i放入临时数组,当其中一个子数组遍历完后,将另一个子数组的剩余元素直接追加到临时数组末尾,将临时数组的有序结果复制回原数组的对应位置,完成合并。

如下图👇:
在这里插入图片描述

归并排序 vs 快速排序:核心差异对比

快排和归并虽然思路很相似并且还同属“分治法”(时间长不复习经常弄混),但是我们可以通过其递归顺序很容易的记忆区分二者,二者落地逻辑完全不同,我们进行对比记忆能让我们直观理解“分治”的灵活性:

1.快排是“先局部有序,再拆分处理”:

  • 通过基准值先将数组分成“左小右大”的区间(分块处理)
  • 再递归处理子区间(递归)

类似二叉树前序遍历,如下图(二叉树没画好见谅👇)。

在这里插入图片描述

2.归并是“先拆分到最小,再合并有序”:

  • 先把数组拆到长度为1的子数组(递归)
  • 再逐层合并成更大的有序数组(和并)

类似二叉树后序遍历,如下图👇:
在这里插入图片描述

两种路径没有好坏之分,学会后遇到“拆分-解决”类问题(如Top K、链表处理),能根据问题特性灵活选择思路,我们通过表格进行二者对比:

对比维度归并排序(Merge Sort)快速排序(Quick Sort)
分治顺序先分后治:先递归拆分数组为子数组,再合并子数组得到有序结果先治后分:先通过基准值分区(得到局部有序),再递归处理子分区
遍历顺序类似二叉树后序遍历:左子数组处理 → 右子数组处理 → 合并左右类似二叉树前序遍历:分区(处理当前) → 左子分区递归 → 右子分区递归
空间复杂度平均/最坏均为 O(n)(需临时数组存合并结果)平均 O(logn)、最坏 O(n)(递归栈开销)
稳定性稳定(相等元素相对位置不变)不稳定(分区交换可能打乱相等元素顺序)
极端场景性能始终 O(nlogn)(不受数据分布影响)最坏 O(n²)(如有序数组、大量重复元素)

代码实现

1.递归内创建临时数组

class Solution {
public:// 归并排序主函数:拆分+合并void Msort(vector<int>& nums, int left, int right) {// 递归终止条件:子数组长度为0或1,无需排序if (left >= right)return;// 1. 拆分:找到中间位置,递归处理左右子数组int mid = left + (right - left) / 2; // 避免(left+right)溢出Msort(nums, left, mid);     // 处理左子数组Msort(nums, mid + 1, right); // 处理右子数组// 2. 合并:用临时数组存储合并后的有序结果vector<int> mark(right - left + 1); // 临时数组,长度=当前子数组长度int l = left, r = mid + 1; // l:左子数组指针,r:右子数组指针int i = 0; // 临时数组的索引// 比较左右子数组元素,按从小到大放入临时数组while (l <= mid && r <= right) {mark[i++] = nums[l] <= nums[r] ? nums[l++] : nums[r++];}// 处理左子数组剩余元素while (l <= mid) {mark[i++] = nums[l++];}// 处理右子数组剩余元素while (r <= right) {mark[i++] = nums[r++];}// 将临时数组的有序结果复制回原数组for (int k = left; k <= right; ++k) {nums[k] = mark[k - left]; // mark的索引从0开始,需对应原数组的left位置}}vector<int> sortArray(vector<int>& nums) {Msort(nums, 0, nums.size() - 1);return nums;}
};

2.全局复用临时数组

class Solution {
public:// 全局临时数组:仅初始化一次,避免递归中频繁创建vector<int> mark;vector<int> sortArray(vector<int>& nums) {// 初始化临时数组,长度与原数组一致mark.resize(nums.size());Msort(nums, 0, nums.size() - 1);return nums;}// 归并排序函数:拆分+合并(复用全局mark数组)void Msort(vector<int>& nums, int left, int right) {if (left >= right)return;// 1. 拆分:递归处理左右子数组int mid = left + (right - left) / 2;Msort(nums, left, mid);Msort(nums, mid + 1, right);// 2. 合并:复用全局mark数组,无需重新创建int l = left, r = mid + 1;int i = left; // 临时数组的索引直接对应原数组的left位置,避免后续偏移计算// 比较并放入临时数组while (l <= mid && r <= right) {mark[i++] = nums[l] <= nums[r] ? nums[l++] : nums[r++];}// 处理剩余元素while (l <= mid) {mark[i++] = nums[l++];}while (r <= right) {mark[i++] = nums[r++];}// 将临时数组结果复制回原数组(从left到right区间)for (int k = left; k <= right; ++k) {nums[k] = mark[k]; // 索引直接对应,无需偏移}}
};

临时数组的复用策略

归并排序的空间开销主要来自“合并”步骤的临时数组,若每次递归都创建新的临时数组,会产生频繁的内存分配与释放,降低执行效率。因此,我们提供两种实现方案,对应不同的优化思路:

  1. 方案一:递归内创建临时数组
    每次合并时,在递归函数内部创建与当前子数组长度匹配的临时数组。

    • 优点:代码简洁,无需考虑全局变量的作用域问题,新手易理解;
    • 缺点:频繁创建/销毁数组,内存操作开销大,在数组长度较大(如 5*10^4)时,执行效率会明显下降。
  2. 方案二:全局复用临时数组
    在主函数中创建一个与原数组长度相同的全局(或类成员)临时数组,合并时直接复用该数组存储结果。

    • 优点:仅一次内存分配,避免频繁内存操作,执行效率更高,更适合本题“5*10^4”的数组规模;
    • 缺点:需额外维护全局变量,代码逻辑需注意临时数组的索引对应关系。

两种方案效率对比

对比维度方案一(递归内创建临时数组)方案二(全局复用临时数组)
内存操作每次合并都创建/销毁临时数组,内存分配释放频繁仅一次内存分配,全程复用,开销低
执行速度较慢(尤其数组长度大时,内存操作耗时占比高)较快(减少内存操作,专注元素比较与复制)
代码复杂度低(无需维护全局变量,逻辑独立)中(需注意全局数组的索引对应,避免越界)
空间峰值较高(递归栈+多个临时数组同时存在)较低(仅递归栈+一个全局临时数组)

通过力扣的执行用时分布我们能更加直观的感受到二者的效率差距:
1.递归内创建临时数组
在这里插入图片描述

2.全局复用临时数组
在这里插入图片描述

结论:对于本题“5*10^4”的数组规模,方案二的执行效率明显优于方案一,更能满足“空间尽可能小”的隐含需求;若数组规模较小(如小于 1000),方案一的简洁性更有优势,可根据实际场景选择。

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

时间复杂度:
所有场景均为 O(nlogn):归并排序的时间消耗主要在“合并”步骤,每次合并需遍历当前子数组的所有元素(O(n))。
而数组拆分的深度为 logn(如长度为 n 的数组需拆 log2n 层),因此总时间复杂度为 O(nlogn)。

空间复杂度
核心开销为临时数组:无论哪种方案,都需一个长度为 n 的临时数组存储合并结果,因此空间复杂度为 O(n);
额外开销为递归栈:递归深度为 logn,栈空间开销为 O(logn),远小于临时数组的 O(n),因此总空间复杂度由临时数组决定,为 O(n)。
对比快排的 O(logn) 空间,归并排序的空间开销更高,但胜在时间性能稳定且具备稳定性,适合对排序稳定性有要求的场景。

总结

  1. 掌握“分治”的不同落地逻辑:归并排序“先拆分后合并”的思路,与快排“先分区后递归”形成互补,理解这种差异能帮你在不同问题中灵活选择算法——需要稳定时间性能选归并,需要极致空间优化选快排。
  2. 优化空间开销的关键思路:归并排序的空间痛点是临时数组,通过“全局复用”而非“递归内重复创建”,可大幅减少内存操作开销,这一思路也适用于其他需要频繁使用临时存储的算法。
  3. 明确稳定性的应用价值:归并排序的“稳定性”是快排无法替代的优势,当排序场景需保留相等元素的原始相对位置时(如多关键字排序),归并排序是唯一的 O(nlogn) 选择,现在掌握可应对后续复杂需求。
  4. 权衡代码简洁与执行效率:方案一的“递归内创建数组”适合快速编码与调试,方案二的“全局复用数组”适合追求执行效率,实际开发中需根据数组规模、性能要求灵活选择,避免一刀切。

下题预告

下一篇我们将聚焦 力扣 LCR 170. 交易逆序对的总数 —— 这道题可是归并排序的 “进阶实战场”!刚吃透归并排序的 “拆分 - 合并” 逻辑,正好用这道题检验成果:它不仅能帮你深化对 “分治思想” 的理解,还能教会你如何在排序过程中 “顺带” 统计逆序对,打通 “排序算法” 到 “实际统计问题” 的应用链路。

Doro 又又又带着小花🌸来啦!🌸奖励🌸看到这里的你!如果这篇归并排序的博客帮你理清了 “拆分合并” 的细节,或是搞懂了临时数组的优化技巧,别忘了点赞支持呀!把它收藏起来,以后复习归并排序时翻出来,就能快速回忆起核心逻辑~关注我,我会持续更新算法系列的博客,有什么解题思路或疑问我们随时讨论,对算法感兴趣的朋友也可以去我的算法专辑看看,里面还有更多有意思的算法题等着你解锁!
在这里插入图片描述

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

相关文章:

  • 大语言模型发展脉络
  • Python元编程:理解__metaclass__和元类的力量
  • 快速排序和交换排序详解(含三路划分)
  • android如何在framework层禁止指定包名访问网络
  • 输电线路绝缘子缺陷检测数据集VOC+YOLO格式4061张5类别
  • Git 完全指南:入门篇
  • 上海牛巨微seo关键词优化怎么做网站的seo优化
  • 温州网站制作软件凌晨三点看的片免费
  • 【Java +AI |基础篇day4 数组】
  • 麒麟系统使用-在Sublime中达到滚屏效果
  • 泰州网站关键词优化软件咨询新网站友链
  • 行政还要负责网站建设新媒体吗7000元买一个域名做网站
  • 前端常用的环境 API 清单
  • CreArt 2.5.6| 无限AI图片生成,需要注意的是点创建之后切出去几秒再切回来
  • 现金流量表不平排查方法
  • 深入理解 HTTP Cookie 与 Session:会话管理的核心机制
  • 【Windows】CoInitializeEx 和 CoUninitialize() 必须在同一个线程中调用吗?
  • 网站建设职责要求saas建站平台
  • 优秀国外网站设计赏析郑州企业网站优化哪家便宜
  • 机器学习、深度学习、信号处理领域常用公式速查表
  • 各类服装网站建设软件正版化情况及网站建设情况
  • 服务器端护照识别技术:通过图像预处理、OCR字符识别和智能分析实现高效身份核验
  • 武胜网站建设敬请期待海报
  • 基于Vue的高校教师文件管理系统7h274l7n(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • Surface-Book-3 i7-1065G7-i5-1035G7 黑苹果 EFI
  • 北京汽车业务网站开发公司建设银行网站网址是什么
  • 多组分精准监测标杆:NHVOC-70 型系列挥发性有机物 (TVOC) 在线监测系统技术解析与场景落地
  • 动态人脸识别技术解析
  • 为迎战双十一,南凌科技发布「大促网络保障解决方案」,以确定性网络抵御不确定流量洪峰
  • 动作捕捉设备应用场景全解析:涵盖机器人开发与数字人交互的多元实践