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

归并排序的三重境界

博客标题:一招鲜,吃遍天:归并排序的三重境界

在学习算法的道路上,我们总会遇到一些“瑞士军刀”般的工具,它们看似简单,却蕴含着解决一类问题的通用思想。今天,我们要聊的主角就是这样一个算法——归并排序

很多人对归并排序的印象可能停留在“哦,一个时间复杂度O(N logN)的稳定排序算法”。没错,但如果仅仅如此,就太小看它了。它的真正威力在于其“分而治之”思想和独特的merge(合并)过程。这个过程天然地为我们提供了一个上帝视角,去处理那些跨越数组左右两部分的元素关系。

今天,我将通过三道经典的编程题,带你一步步领略归并排序的三重境界:从排序,到简单计数,再到复杂计数


第一重境界:万物之始 —— 排序数组 (LeetCode 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 的值不一定唯一。

问题描述:给你一个整数数组 nums,请你将该数组升序排列。要求时间复杂度为 O(nlog(n)),空间复杂度尽可能小。

这是归并排序最本源,最核心的应用。它的思想非常纯粹:

  1. 分解 (Divide):不断地把数组一分为二,直到每个子数组只剩一个元素。一个元素的数组天然就是有序的。
  2. 合并 (Merge):将两个已经有序的子数组,合并成一个大的有序数组。

这里的灵魂就在于merge函数。我们用两个指针分别指向两个有序子数组的开头,比较指针所指元素的大小,将较小的那个放入一个临时的辅助数组,然后移动相应的指针。重复这个过程,直到一个子数组被完全遍历,再将另一个子数组剩下的部分直接复制过去。最后,把辅助数组中的有序结果复制回原数组。

【核心代码】

class Solution {
public:int help[50008]; // 辅助数组,避免在递归中频繁创建vector<int> sortArray(vector<int>& nums) {mergeSort(0, nums.size() - 1, nums);return nums;}// 递归分解void mergeSort(int l, int r, vector<int>& nums) {if (l >= r) return; // 当子数组只有一个或没有元素时,返回int m = l + (r - l) / 2; // 防止 l+r 溢出mergeSort(l, m, nums);mergeSort(m + 1, r, nums);merge(l, m, r, nums); // 合并}// 合并两个有序子数组void merge(int l, int m, int r, vector<int>& nums) {int a = l, b = m + 1, index = l;while (a <= m && b <= r) {if (nums[a] <= nums[b]) {help[index++] = nums[a++];} else {help[index++] = nums[b++];}}// 处理剩余元素while (a <= m) help[index++] = nums[a++];while (b <= r) help[index++] = nums[b++];// 复制回原数组for (int i = l; i <= r; i++) nums[i] = help[i];}
};

境界小结:这是归并排序的基本功。理解并能熟练写出这个模板,是迈向更高境界的基石。这里的merge过程,只关心元素间的大小关系,目的是为了排序


第二重境界:初窥门径 —— 计算数组的小和

在这里插入图片描述

问题描述:对于一个数组中的每个数,求其左侧所有小于或等于它的数的和。整个数组的小和定义为所有数的小和之和。

这个问题要求我们计算一种特定的“贡献”。暴力解法是O(N^2)的,显然不满足要求。这时,归并排序的机会来了。

我们思考一下,在merge过程中,当我们比较左半部分[l...m]nums[a]和右半部分[m+1...r]nums[b]时,我们能获得什么信息?

nums[a] <= nums[b] 时,我们不仅知道 nums[a]nums[b] 小,更重要的是,因为右半部分 [m+1...r] 是有序的,所以我们知道 nums[a] 小于等于 nums[b]nums[b+1]、…、nums[r] 这所有的元素!

这启发了我们:一个数的小和,可以转化为在merge过程中,它作为较小数时,右侧有多少个数比它大(或等于)。

于是,我们可以在merge时“顺便”完成计算:
nums[a] <= nums[b] 时,我们准备将nums[a]放入help数组。此时,nums[a]对整个数组的小和产生了贡献。它的贡献值是 nums[a] 乘以右半部分所有比它大的数的个数,即 r - b + 1

【核心代码】
注:以下代码实现了“小和”的逻辑,与你提供的代码逻辑略有不同,但思想一致,都是在merge过程中完成统计。

#include <iostream>
using namespace std;
int s[100004];
int help[100004];
long long sum = 0;void merge(int l, int m, int r) {int a = l, b = m + 1, index = l;while (a <= m && b <= r) {if (s[a] <= s[b]) {// s[a] 比右边 [b...r] 的所有数都小// 这些数都在 s[a] 的右边,所以 s[a] 产生了贡献sum += (long long)s[a] * (r - b + 1);help[index++] = s[a++];} else {// s[b] 比 s[a] 小,但我们无法确定 s[b] 和 s[a] 左边数的关系// 所以在 s[b] < s[a] 时不计算贡献help[index++] = s[b++];}}while (a <= m) help[index++] = s[a++];while (b <= r) help[index++] = s[b++];for (int i = l; i <= r; i++) s[i] = help[i];
}void mergeSort(int l, int r) {if (l >= r) return;int m = l + (r - l) / 2;mergeSort(l, m);mergeSort(m + 1, r);merge(l, m, r);
}int main() {int n;cin >> n;for (int i = 0; i < n; i++) cin >> s[i];mergeSort(0, n - 1);cout << sum << '\n';
}

境界小结:我们从归并排序中挖掘出了新的价值。merge不再仅仅是为了排序,它成了一个信息处理和统计的平台。通过在比较大小的同时增加一行计算代码,我们巧妙地解决了问题。


第三重境界:登堂入室 —— 翻转对 (LeetCode 493)

在这里插入图片描述

问题描述:给定一个数组 nums ,如果 i < jnums[i] > 2 * nums[j],我们就将 (i, j) 称作一个重要翻转对。返回重要翻转对的数量。

这个问题是“逆序对”问题的加强版。条件从 nums[i] > nums[j] 变成了 nums[i] > 2 * nums[j]

如果我们还想用第二重境界的方法,在merge排序的同时进行计数,会遇到一个大麻烦:
merge排序的依据是nums[a] <= nums[b],但计数的依据是 nums[a] > 2 * nums[b]。这两个条件不一致!如果我们按计数条件来移动指针,数组就无法正确排序,那么整个归并排序的根基就动摇了。

怎么办?答案是:解耦!将计数和排序合并分为两步!

merge函数内部,我们利用左右两个子数组已经分别有序的黄金特性,先完成计数,再完成标准的排序合并。

  1. 计数阶段
    • 使用两个指针 iji 遍历左半部分[l...m]j 遍历右半部分[m+1...r]
    • 对于每个 nums[i],我们向右移动 j,直到找到第一个不满足 nums[i] > 2 * nums[j] 的位置。
    • 由于数组的单调性,j 指针无需回退。所以,这一步的时间复杂度是线性的 O(N)。
  2. 排序合并阶段
    • 计数完成后,忘记刚才的 ij
    • 重新用两个指针 ab,从头开始,执行一次标准的归并排序合并操作。

此外,还有一个陷阱:2 * nums[j] 可能会导致整数溢出。我们需要使用long long来确保计算的正确性。

【核心代码】

class Solution {
public:int help[50003];int sum = 0;int reversePairs(vector<int>& nums) {if (nums.empty()) return 0;mergeSort(0, nums.size() - 1, nums);return sum;}void mergeSort(int l, int r, vector<int>& nums) {if (l >= r) return;int m = l + (r - l) / 2;mergeSort(l, m, nums);mergeSort(m + 1, r, nums);merge(l, m, r, nums);}void merge(int l, int m, int r, vector<int>& nums) {// --- 步骤一:先完成计数,不影响排序逻辑 ---int j = m + 1;for (int i = l; i <= m; i++) {// 使用 long long 防止溢出while (j <= r && (long long)nums[i] > 2LL * nums[j]) {j++;}sum += j - (m + 1);}// --- 步骤二:再执行标准的排序合并 ---int a = l, b = m + 1;int index = l;while (a <= m && b <= r) {if (nums[a] <= nums[b]) {help[index++] = nums[a++];} else {help[index++] = nums[b++];}}while (a <= m) help[index++] = nums[a++];while (b <= r) help[index++] = nums[b++];for (int i = l; i <= r; i++) {nums[i] = help[i];}}
};

境界小结:这是归并排序思想的升华。我们认识到,merge过程提供的“左右子数组均有序”这个前提,比merge本身的操作更宝贵。我们可以利用这个前提,在排序之前,先做一些其他有意义的事情。这种“先利用特性,后恢复结构”的思路,是解决很多复杂分治问题的关键。


总结

我们从一个简单的排序需求出发,一步步深入,最终将归并排序打造成了一个解决复杂计数问题的利器:

  • 境界一:将merge作为排序工具
  • 境界二:将merge作为伴随计算的平台,在排序的同时完成简单的计数。
  • 境界三:将merge前的有序状态作为独立的计算窗口,实现计数与排序的解耦,解决更复杂的计数问题。

希望这次的旅程能让你对归并排序有一个全新的认识。它不仅仅是一个算法,更是一种强大的思维框架。当你下次遇到涉及“左边/右边”、“之前/之后”的计数问题时,不妨问问自己:归并排序能帮上忙吗?

感谢阅读!

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

相关文章:

  • 刷网站软件微信网站建设开发
  • 论坛网站设计wordpress 启动wordpress mu
  • 大数据离线数仓之业务域设计
  • wordpress 主题 建站网站seo 最好
  • 电脑格式化了还能恢复数据吗?硬盘格式化恢复教程分享
  • 网站空间租用多少钱练手网站开发
  • Docker经典安装命令失效排查:Ubuntu/CentOS多系统测试与解决方案
  • 慧知开源重卡充电桩平台建设方案 - 慧知开源充电桩平台(我们是有真实上线案例的)
  • 做教育机构中介网站百度小程序制作网站
  • 软件设计师——03 数据结构(上)
  • 专业定制网站需要什么技能便捷的网站建设
  • 深圳html5网站制作个人网站 商业
  • 为什么建站之前要进行网站策划成都h5模板建站
  • KV cache原理
  • Global cpu Load
  • 【Linux lesson1】初识Linux系统
  • 怎么搭建自己的网站后台ftp服务器
  • 理解Word2Vec
  • 北京网站开发网络公司开平小学学生做平网站
  • 网站开发语言htmlWordPress封面生成
  • 推荐医疗网站建设北京企业网站建设方
  • interface vlanif vlan-id 概念及题目
  • 如何查询网站主机信息网页设计课程报告
  • 济南网站建设 小程序网站公司建设网站价格
  • Vala编程语言高级特性-异步方法
  • 跟业务合作做网站给多少提成哪个网站可以学做包包
  • 销售网站建设的会计分录怎么在阿里巴巴做网站
  • 华清远见25072班C++学习day4
  • Springboot集成Flowable
  • 使用langgraph创建工作流系列5:创建一个服务