【算法练习】归并排序和归并分治

文章目录

    • 1.归并排序
      • 1.1 递归版本
      • 1.2 非递归版本
    • 2.归并分治
      • 2.1 计算数组的小和
      • 2.2 计算翻转对

1.归并排序

归并排序的核心步骤是:

拆分:将无序数组不断对半拆分成小块,直到每个小块只剩一个元素(自然有序)。
合并:将相邻的有序小块合并,逐步形成更大的有序块,直到整个数组有序。

1.1 递归版本

递归天然避免越界,代码简洁,但递归深度受限。

#include <vector>
using namespace std;// 合并两个有序子数组
void merge(int arr[], int left, int mid, int right) 
{vector<int> temp(right - left + 1);  // 临时数组int i = left, j = mid + 1, k = 0;// 双指针合并有序区间while (i <= mid && j <= right) {temp[k++] = (arr[i] <= arr[j]) ? arr[i++] : arr[j++];}// 处理剩余元素while (i <= mid) temp[k++] = arr[i++];while (j <= right) temp[k++] = arr[j++];// 拷贝回原数组for (int p = 0; p < k; p++) {arr[left + p] = temp[p];}
}// 递归归并排序
void mergeSort(int arr[], int left, int right) {if (left >= right) return;int mid = left + (right - left) / 2;mergeSort(arr, left, mid);//分解左半区mergeSort(arr, mid + 1, right);//分解右半区merge(arr, left, mid, right);//合并有序区间
}int main() 
{int arr[] = {12, 11, 13, 5, 6, 7};int n = sizeof(arr)/sizeof(arr[0]);mergeSort(arr, 0, n-1);return 0;
}

1.2 非递归版本

非递归版本通过步长控制,把数组看作由多个有序子数组组成,逐步扩大子数组长度,直到整个数组有序。
非递归循环效率更高,适合大数据量,但是需要控制越界。

与递归版本不同的是递归是自顶向下(通过递归函数先拆分再合并),非递归是自底向上(通过数组下标直接从小块开始合并)

假设原始数组为 [3,1,4,2,7,5]
执行步骤如下:
步长=1:把每个元素视为独立的有序数组,两两合并→ 合并后 [1,3] [2,4] [5,7]
步长=2:合并相邻的两个长度为2的子数组→ 合并后 [1,2,3,4] [5,7]
步长=4:继续合并更大的子数组→ 最终得到 [1,2,3,4,5,7]

void mergeSort(int arr[], int n) 
{// 预分配临时空间vector<int> temp(n);  // 按步长分组(1,2,4,8...)for (int gap = 1; gap < n; gap *= 2) {// 每两组进行比较 //[left, left+gap-1] [left+gap,left+2*gap-1]//[left,mid][mid+1, right]for (int left = 0; left < n; left += 2*gap) {// 计算子数组边界 (按l,m,r)int mid = min(left + gap - 1, n-1);int right = min(left + 2*gap - 1, n-1);// 合并相邻子数组int i = left, j = mid + 1, k = left;while (i <= mid && j <= right) {temp[k++] = (arr[i] <= arr[j]) ? arr[i++] : arr[j++];}// 处理剩余元素while (i <= mid) temp[k++] = arr[i++];while (j <= right) temp[k++] = arr[j++];// 拷贝回原数组for (int p = left; p <= right; p++) {arr[p] = temp[p];}}}
}int main() 
{int arr[] = {12, 11, 13, 5, 6, 7};int n = sizeof(arr)/sizeof(arr[0]);mergeSort(arr, n);return 0;
}

2.归并分治

实施原理:

  1. 思考问题在大范围的答案,是否等于左部分的答案+右部分的答案+跨越左右部分的答案。
  2. 计算跨越左右部分的答案时,如果左右部分各自有序,是否能让计算跨越左右部分答案时更加便利。

分治法的基本步骤:

  1. 分解:将原始数组通过递归的方式拆分成两个长度相近的子数组,一直拆分到单个元素为止(因为单个元素天生有序)
  2. 统计:根据题意进行相关的统计。
  3. 排序:根据题意思考,在将小部分合并成大部分之前,如果将小部分进行排序,是否能便于大部分进行统计。

2.1 计算数组的小和

计算数组的小和

首先让我们看一组示例 [1,3,5,2,4,6],这个小和答案为27,其暴力解法很好想,就是每个数和其他的数进行比较进行累加,但时间复杂度是O(n^2)所以不考虑。

下面看看这题归并分治的解法。

1. 根据上面说的原理,我们先看整个大范围部分[1,3,5,2,4,6]的答案,是否可以通过左部分[1,3,5]加上右部分[2,4,6],再加上跨越左右的答案。
首先,我们直接计算[1,3,5]小和是5,[2,4,6]小和是8。接下来计算跨越左右的答案[1,3,5] | [2,4,6],可以看到两边内部的已经各种计算好了,那么跨越的先看2对应的2>1再2<3,那么对应2的小和就只有1。再看4对应的4>1,4>3,4<5那么4对应的小和就是4,6类推就是9,那么跨越的小和加起来就是14,再和前面相加14+5+8就是27答案对应上了。
2. 那么如果再把大范围缩小到计算[1,3,5]的答案,可以看出,其左部分[1,3]小和为1,右部分[5]小和为0,跨越左右为4,相加后也对应上了小和为5。那么就可以看出小部分的解和大部分的解都是一样的,那么就可以考虑归并分治。
3.接下来考虑在计算跨越左右的答案时,如果左、右部分各自有序这个条件,计算会不会更简单。
我们看[1,3,5] | [2,4,6],如果其未排序[3,1,5] [6,2,4],那么对于未排序的计算,需要每个数和其他数进行比较累加,就是暴力解法。肯定是更复杂的!。
4.那么这道题,保持左右各有序后计算便利在哪?
比如这个例子[3,6,7] [5,6,9]在计算跨越左右的答案时,有两种算法。
(1)从右部分开始对左部分的数进行比对,对应5大于3,对应6>3,6>=6,对应9>3,9>6,9>7,我们可以发现,右部分下一个数的计算(如5之后的6,6之后的9)可以在上一个数的基础上继续计算并且加上上一个数的和。
  具体什么意思?就是比如右部分的5在和左部分3比较后再和左部分6比较,由于5<6那么左部分就到6停,下一个右部分的6直接和左部分的6进行比较,再和7比较然后停。右部分6的小和就直接加上5的小和和比较的6。右部分的9就直接加上6的小和以及比较的7。(这样就不用右部分每一个数都和左边的比了,因为有序)
(2)从左部分开始对右部分的数进行比对,如果5大于3,那么5后面所有的数都大于3,就直接3乘以5以及右边的个数就行了。

两个方法时间复杂度都是O(N),相当于把每个数都走了一遍。

对应从右部分开始对左部分的数进行比对

//代表整个跨左右的答案
long long ans = 0; 
//先固定右部分的数,sum代表每个数自己的小和
for(int j = m+1, i = l, sum = 0; j <= r; ++j)
{//每个数的小和 = 这一回的比较 + 上一个数的小和while(i <= m && s[i] <= s[j]) sum+=s[i++];ans += sum;
}

对应从左部分开始对右部分的数进行比对

//代表整个跨左右的答案
long long ans = 0; 
for(int j = m+1, i = l; j <= r; ++j)
{while(i <= m && s[i] <= s[j]){ans+=(r-j+1)*s[i];++i;}
}

完整代码

#include <iostream>using namespace std;const int MAXN = 100001;int s[MAXN];
int tmp[MAXN];long long Merge(int l, int m, int r)
{//1.先统计long long ans = 0;for(int j = m+1, i = l, sum = 0; j <= r; ++j){while(i <= m && s[i] <= s[j]) sum+=s[i++];ans += sum;}/*//计算方法二long long ans = 0; for(int j = m+1, i = l; j <= r; ++j){while(i <= m && s[i] <= s[j]){ans+=(r-j+1)*s[i++];}}*///2.再排序,方便后续部分的统计int i = l, k = l, j = m+1;while(i <= m && j <= r){tmp[k++] = (s[i] <= s[j] ? s[i++] : s[j++]);}while(i <= m) tmp[k++] = s[i++];while(j <= r) tmp[k++] = s[j++];for (int i = l; i <= r; ++i){s[i] = tmp[i];}return ans;
}long long Count(int l, int r)
{if(l == r) return 0;int m = (l+r) >> 1;//接下来进行细分,同时统计计算再排序return Count(l, m) + Count(m+1, r) + Merge(l, m, r);
}int main() {int n = 0;while(cin >> n){for(int i = 0; i < n; ++i) cin>>s[i];//首先对数组进行细分cout << Count(0, n-1) << endl;}return 0;
}

2.2 计算翻转对

计算翻转对

还是照着之前说的原理,拿[2,4,3,5,1],分成两个部分[2,4,3] [5,1],在假设两个部分分别计算好翻转对数量以及排序后,[2,4,3] 有0个翻转对,[5,1]是1个,统计完后因为各个内部翻转对已经计算好了,然后想排序后对于两边跨越的计算是否更便利,答案是肯定的,各自排序后。那么计算跨越左右的[2,3,4][1,5],也是有两种方法,简单的法一:3大于1*2了,那么排序后3之后都是大于3的,也就是都能和1能组成翻转对的。法二:3和1比完后,接着4和5比,然后再加上3对应的翻转对数。因为3满足的,4也满足。

class Solution {
public:int tmp[50001] = {0};int Merge(vector<int>& nums, int l, int m, int r){//1.统计int ans = 0;//法一// for(int i = l, j = m+1, count = 0; i <= m; ++i)// {//     while(j <= r && nums[i] > (long)2*nums[j]) count++, ++j;//     ans += count;// }//法二for(int i = l, j = m+1; i <= m; ++i){while(j <= r && nums[i] > (long)2*nums[j]){ans += (m-i+1);j++;}}//2.排序int a = l, b = l, c = m+1;while(a <= m && c <= r){tmp[b++] = (nums[a] <= nums[c] ? nums[a++] : nums[c++]);} while(a <= m) tmp[b++] = nums[a++];while(c <= r) tmp[b++] = nums[c++];for(int i = l; i <= r; ++i) nums[i] = tmp[i];return ans;}int Count(vector<int>& nums, int l, int r){if(l == r) return 0;int m = (l+r) >> 1;return Count(nums, l, m) + Count(nums, m+1, r) + Merge(nums, l, m, r);}int reversePairs(vector<int>& nums) {int len = nums.size();return Count(nums, 0, len-1);}
};


算法中有很多精妙又美丽的思想传统,请务必坚持下去!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/903452.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

域对齐是什么

域对齐&#xff08;Domain Alignment&#xff09;是在机器学习和计算机视觉等领域中常用的技术 定义 域对齐旨在将不同域&#xff08;Domain&#xff09;的数据映射到一个共同的特征空间中&#xff0c;使得来自不同域的数据在该空间中具有相似的分布。这里的“域”可以指代不…

【linux】git安装、升级

git安装、升级 一、快捷安装版本2.18.0二、自定义版本安装&#xff08;安装、升级&#xff09;1、移除旧文件2、安装所需依赖3、选择指定版本4、解压文件、编译5、增加环境变量&#xff0c;验证是否版本 三、升级 一、快捷安装版本2.18.0 yum install git git --version二、自…

编程日志4.24

栈的链表基础表示结构 #include<iostream> #include<stdexcept> using namespace std; //模板声明&#xff0c;表明Stack类是一个通用的模板&#xff0c;可以用于存储任何类型的元素T template<typename T> //栈的声明 //Stack类的声明&#xff0c;表示一…

《冰雪传奇点卡版》:探索冰雪世界的传奇旅程!

《冰雪传奇点卡版》以“纯净打金”为核心&#xff0c;摒弃复杂付费坑&#xff0c;回归经典传奇玩法。以下从核心玩法、资源获取、职业搭配、交易变现四维度展开&#xff0c;助你高效开启冰雪传奇之旅。 一、核玩法解析&#xff1a;如何高效获取资源&#xff1f; 1. 职业定位与…

DeepClaude开源程序可以实现代码生成、创作诗句以及内容创作等功能

一、软件介绍 文末提供程序和源码下载 DeepClaude开源程序是增强的 AI&#xff0c;可以实现代码生成&#xff1a;DeepSeek r1 Claude 3.7 十四行诗 - 无与伦比的性能&#xff01;内容创作&#xff1a;DeepSeek r1 Gemini 2.5 Pro - 卓越的质量&#xff01;OpenAI 兼容。流媒…

Java常用注解通俗解释

注解就像是给Java代码贴的"便利贴"&#xff0c;它们不会改变代码本身的逻辑&#xff0c;但能给编译器、开发工具或运行时环境提供额外信息。下面我用最通俗的方式解释Java中最常用的注解&#xff1a; 一、基础篇&#xff1a;人人必知的注解 1. Override - "我…

vscode chrome调试怎么在所有浏览器都好使

chrome调试时只能在打开的浏览器里进行调试&#xff0c;其它打开的chrome浏览器就不能调试了&#xff0c;怎么解决。 右键点击 Chrome 的快捷方式图标&#xff0c;选择属性 在目标一栏&#xff0c;最后加上--remote-debugging-port9222 注意要用空格隔开 lanch.json 文件配置 …

Unity PBR基础知识

PBR原理 基于物理的渲染&#xff08;Physically Based Rendering&#xff0c;PBR&#xff09;是指使用基于物理原理和微平面理论建模的着色/光照模型&#xff0c;以及使用从现实中测量的表面参数来准确表示真实世界材质的渲染理念。 PBR基础理念 微平面理论&#xff08;Micr…

COM组件使用方法

普通COM组件&#xff08;如DLL&#xff09;仅暴露方法/属性接口&#xff0c;而ActiveX控件&#xff08;如OCX&#xff09;需要可视化交互&#xff08;如按钮、表格&#xff09;&#xff0c;需通过 ​​AxInterop​​ 包装器实现宿主环境集成。 项目中引入ActiveX控件流程如下。…

在 Spring Boot 项目中如何使用索引来优化 SQL 查询?

在 Spring Boot 项目中使用索引来优化 SQL 查询是提升数据库性能最常用的方法之一。下面是详细的步骤和实践指南&#xff1a; 核心目标&#xff1a;让数据库能够通过扫描索引&#xff08;小范围、有序的数据结构&#xff09;快速定位到所需数据行&#xff0c;而不是扫描整个表…

Vue3生产环境与Vue Devtools

在 Vue 3 的生产环境中&#xff0c;默认情况下 Vue Devtools 是无法正常使用 的&#xff0c;但开发者可以通过配置强制启用。以下是关键信息总结&#xff1a; &#x1f4cc; 核心结论 默认不可用 Vue 3 生产构建会移除 Devtools 支持以优化性能和安全性。 可强制启用 通过构建…

ARP渗透学习1

ARP协议工作原理 1. 什么是ARP ARP定义: 地址解析协议&#xff08;Address Resolution Protocol&#xff09;&#xff0c;是根据IP地址获取物理地址的一个TCP/IP协议。 2. 工作原理 ARP表: 每台计算机都需要一个ARP表&#xff0c;用来保存IP地址和MAC地址的映射关系。查询过…

甲骨文云2025深度解析:AI驱动的云原生生态与全球化突围

一、战略转型&#xff1a;从数据库巨头到AI云服务先锋 1. 技术重心向AI与云深度迁移 甲骨文在2025年加速向AI原生云架构转型&#xff0c;其核心战略围绕生成式AI与量子计算展开。通过推出Oracle 23ai自治数据库&#xff0c;深度集成AI向量搜索功能&#xff0c;并重构云基础设…

【网络原理】TCP异常处理(二):连接异常

目录 一. 由进程崩溃引起的连接断开 二. 由关机引起的连接断开 三. 由断电引起的连接断开 四. 由网线断开引起的连接断开 一. 由进程崩溃引起的连接断开 在一般情况下&#xff0c;进程无论是正常结束&#xff0c;还是异常崩溃&#xff0c;都会触发回收文件资源&#xff0c;…

想做博闻强记的自己

2025年4月29日&#xff0c;13~25℃&#xff0c;还好 待办&#xff1a; 冶金《物理》期末测试 阅卷&#xff08;冶金《物理》期末测试试卷&#xff09; 重修《物理》《物理2》电子材料归档 规则变更&#xff0c;《高等数学2》期末试卷推倒重来 遇见&#xff1a;直播画面。 感受…

IP属地是实时位置还是自己设置

刷微博、抖音时&#xff0c;评论区总能看到“IP属地”&#xff1f;这个突然冒出来的小标签&#xff0c;让不少网友摸不着头脑&#xff1a;‌IP属地是实时位置&#xff0c;还是可以自己设置&#xff1f;‌别急&#xff0c;今天咱们就来聊聊这个话题&#xff01; 1、什么是IP属地…

水力压裂多裂缝扩展诱发光纤应变演化试验研究

1.概述 本文基于OFDR技术的光纤应变监测方法&#xff0c;监测了真三轴条件下人造岩石试样与页岩的水力压裂试验。结果表明&#xff0c;OFDR技术能以毫米级分辨率实时监测裂缝起裂、扩展及闭合全过程&#xff0c;并建立基于应变演化的裂缝判别准则&#xff0c;为光纤压裂监测的…

4、RabbitMQ的七种工作模式介绍

目录 一、Simple(简单模式) 1.1 概念 1.2 代码实现 消费者 运行结果 二、Work Queue&#xff08;工作队列&#xff09; 2.1 概念 1.2 代码实现 生产者 消费者 运行结果 三、Publish/Subscribe&#xff08;发布/订阅模式&#xff09; 3.1 概念 3.2 代码实现 生产者…

厚铜PCB钻孔工艺全解析:从参数设置到孔壁质量的关键控制点

在现代电子设备中&#xff0c;厚铜PCB&#xff08;印刷电路板&#xff09;扮演着至关重要的角色。它们不仅为电子元件提供了支撑&#xff0c;还实现了电路之间的连接。然而&#xff0c;在生产厚铜PCB时&#xff0c;钻孔是一个关键环节。本文将为您介绍厚铜PCB生产中钻孔的科普知…

缺口拼图,非线性坐标关联

继上一篇文章&#xff0c; 欢迎一起交流探讨 https://t.zsxq.com/GEIze