分治归并问题

“别让自我被拯救~” 


谈谈归并与分治

        当我们首次接触排序算法时,一定对所谓 "归并"方式排序的算法感到头疼~ 因为,我们难以形象出其不断 "分离"时,各个区域的状态。然而,即便 "归并"排序算法的学习是艰难,但掩盖不住其相较于其他算法而言,拥有更优时间复杂度,而迫使我们不得不盯着头疼的代码苦思冥想。并且,"归并"思想不仅仅体现在排序中,更能成为我们解决实际问题时的一个方略方法。

        归并核心之一就在于 "分治",那么啥子叫 "分治"? 简单来说,就是 "分而治之,大事化小" 。

        假设我们要对举例数组元素进行升序排序arr[8] = { 6,1,2,7,3,4,8,0}。要排升序需不需要知道每个数与每个数之间的大小关系?需要! 所以我们得从 n=1 开始与 n-1个数进行大小关系的比较~  唔,这一套下来,直接给时间复杂度 干上了 O(N^2),与冒泡的效率难分伯仲了。

        或许我们可以缩小比较范围,与n个数比较换成与 n / 2 个数比较呢? 或者 n / 4个数? 甚至 n==1。

归并 vs 快排

        快排与归并都是通过在既定区间,对数组内元素值进行排序,但两者有根本上的区别~


排序数组

        没什么好解析题目的,咱们直接以归并的思想拿这道题练练手~

递归版本:

class Solution {
public:void _MergeSort(vector<int>& nums,int l,int r,vector<int>& tmp){// 当归并的区间不存在~if(l >= r) return;// 划分区域int mid = (l + r) >> 1;// 进行左右区间划分分治 --> 我们就认为MergeSort可以给我们完成划分分治工作_MergeSort(nums,l,mid,tmp);_MergeSort(nums,mid+1,r,tmp);// 走到这里说明 分治已经做完~ 此时开始归并了// 此时区域被划分为 {l,mid} {mid+1,l} // 我们需要保证归并数组的 有序性 所以我们得将这两个区间归并排序// 我们额外的数组起作用了~int begin1 = l,end1 = mid;int begin2 = mid+1,end2 = r;int index_tmp = l; // 注意tmp模拟的是nums 所以这里的index不能为0 根据你的区域划分l设置初始值while(begin1 <= end1 && begin2 <= end2){if(nums[begin1] <= nums[begin2]) tmp[index_tmp++] = nums[begin1++];else tmp[index_tmp++] = nums[begin2++];}while(begin1 <= end1) tmp[index_tmp++] = nums[begin1++];while(begin2 <= end2) tmp[index_tmp++] = nums[begin2++];//我们现在只是将排序好的数组放在了 tmp 现在我们需要将这些元素 放置回去// 防止多少呢? tmp归并了多少: index_tmp// 从哪里开始放呢 l// for(int i=l;i<index_tmp;++i) nums[i] = tmp[i];memcpy(&nums[0] + l,&tmp[0] + l,sizeof(int) * (r-l+1));}void MergeSort(vector<int>& nums,int l,int r){vector<int> tmp(nums.size());_MergeSort(nums,l,r,tmp);}vector<int> sortArray(vector<int>& nums) {MergeSort(nums,0,nums.size()-1);return nums;}
};

非递归版本:

        有些时候考虑堆栈递归的深度,可能会要求使用非递归的方式实现归并排序~我们可以使用希尔排序的思想,依据gap的间距划分待排序区间~

        算法思想: 不过与 shell排序明显不同的地方是,其gap间距尽可能从大值选取,逐渐减小在归并中,初始取gap值为1,gap值逐渐增大。 当然这就对应了 递归版的,将左右区间缩减到1个元素

细节问题:         

class Solution {
public:void _MergeSort(vector<int>& nums, int start1, int end1,int start2, int end2, vector<int>& tmp){int index_tmp = start1;int begin = start1;while (start1 <= end1 && start2 <= end2){if (nums[start1] <= nums[start2]) tmp[index_tmp++] = nums[start1++];else tmp[index_tmp++] = nums[start2++];}while (start1 <= end1) tmp[index_tmp++] = nums[start1++];while (start2 <= end2) tmp[index_tmp++] = nums[start2++];for (int i = begin;i < index_tmp;++i) nums[i] = tmp[i];}void MergeSort(vector<int>& nums, int l, int r){vector<int> tmp(nums.size());int gap = 1;while (gap < nums.size()){for (int i = 0;i < nums.size();i += 2 * gap){// 为什么需要-1? 这里是为了满足 [1,1]归并int start1 = i, end1 = i + gap - 1;int start2 = i + gap, end2 = i + 2 * gap - 1;// 越界if (end1 > r || start2 > r) break;// 不足if (end2 > r) end2 = r;// 进行归并_MergeSort(nums, start1, end1, start2, end2, tmp);}gap *= 2;}}vector<int> sortArray(vector<int>& nums) {MergeSort(nums, 0, nums.size() - 1);return nums;}
};

排序链表

        区别于数组排序,我们可以通过拿到数组的首元素地址从而访问所有的元素值,如果想要交换两个元素的位置直接使用库函数 std::swap()。但,现在我们需要排序的数链表,其节点内部包含着val,我们不能像数组那样对齐进行随机访问,所以,意向使用快排或者shell完成对题目规定内O(N*LOGN),是行不通的~\

细节问题:

        所以,我们把目光聚焦在归并排序~ 区别于数组排序,我们可以通过俩左右下标计算出mid划分区域,在归并排序中,我们借助快慢指针完成对前后区域的划分~

class Solution {
public:ListNode* MergeSort(ListNode* head){if(head == nullptr || head->next == nullptr) return head;ListNode* fast = head->next,*slow = head;while(fast && fast->next){slow = slow->next;fast = fast->next->next;}// 这里进行切断ListNode* mid = slow->next;slow->next = nullptr;// 分治ListNode* start1 = MergeSort(head);ListNode* start2 = MergeSort(mid);// 归并ListNode* newhead = new ListNode(-1);ListNode* tail = newhead;while(start1 && start2){if(start1->val <= start2->val){tail->next = start1;start1 = start1->next;}else{tail->next = start2;start2 = start2->next;}tail = tail->next;}// 归并剩下的tail->next = start1 == nullptr ? start2 : start1;tail = newhead->next;delete newhead;return tail;}ListNode* sortList(ListNode* head) {return MergeSort(head);}
};

合并K个升序链表

        一个很简单的思路,就是给数组内的链表节点继续排升序。我们可以利用 优先级队列,把vector中的所有元素插入进该数组,构建小堆。这样我们每次取堆顶数据时,都可以取到最小值,从而实现所谓的合并~

class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {// 这里我们需要重载比较函数 因为默认函数greater不支持auto func = [](ListNode* node1,ListNode* node2)->bool{return node1->val > node2->val;};// 构建小堆priority_queue<ListNode*,vector<ListNode*>,decltype(func)> heap;for(auto l:lists) if(l) heap.push(l);ListNode* newhead = new ListNode(-1);ListNode* tail = newhead;while(!heap.empty()){ListNode* front = heap.top();heap.pop();tail->next = front;tail = tail->next;if(front->next) heap.push(front->next);}tail = newhead->next;delete newhead;return tail;}
};

        考虑优先级队列中的个数是既定的k(数组里的首元素)个,一个节点的插入、删除花费的时间复杂度为 O(logK),总的效率控制在O(kn * logK)。

        再次来会看题目:

        嘶~ 这难道不就是叫我们进行区间合并嘛? 我们似乎在哪里做过~ 是的! 归并排序中,当我们进行区域分治后,就是区域排序! 然而,现在更为简单,区域是给你划分好了,就连区域内的元素也是给你按 升序排序的!我们目前要做的,无非就是 "合并"!

class Solution {
public:ListNode* mergeList(ListNode* left,ListNode* right){// left||right都可能出现nullptr 即是 不能组成归并的 直接返回  不为空的if(left == nullptr) return right;if(right == nullptr) return left;ListNode* newhead = new ListNode(-1);ListNode* tail = newhead;while(left && right){if(left->val <= right->val){tail->next = left;left = left->next;}else{tail->next = right;right = right->next;}tail = tail->next;}if(left) tail->next = left;if(right) tail->next = right;tail = newhead->next;delete newhead;return tail;}ListNode* _mergeKLists(vector<ListNode*>& lists,int l,int r){if(l > r) return nullptr;// 如果只存在一组 直接返回这个链表if(l == r) return lists[l];int mid = (l+r) >> 1;// 划分为极小的ListNode* left = _mergeKLists(lists,l,mid);ListNode* right = _mergeKLists(lists,mid+1,r);// 进行两两归并return mergeList(left,right);}ListNode* mergeKLists(vector<ListNode*>& lists) {return _mergeKLists(lists,0,lists.size()-1);}
};

交易逆序对的总数

        所谓逆序对,就是指: "前后数存在大小关系,大的在前,小的在后"。我们要做的就是统计,符合条件的逆序对的个数~

        正如图示,我们可以采用暴力解法,从i=0开始,套两层的for循环,可以暴搜完成逆序对总数的查找和统计。

    int ret = 0;for(int i=0;i<n;++i)for(int j=i+1;j<n;++j){if(nums[i] > nums[j]) ret++;}

        哈哈哈,不过多数时候(或许压根任何时候)刷题软件的后端使用到的令人发指的测试数据往往会让你的代码经不起半点抗击打而原形毕露——  "超过时间限制"。

        或许,我们可以在暴搜的基础上做出一定的优化,暴搜花费的时间在哪里??"你这难道不是说屁话嘛??不就是那咔咔两行敲上去的双层for?"。对 ! 这是原因,但这只是表象而非根因~ 在暴搜的过程中,并没有保存之前元素艰难爬行于那千疮百孔般代码所留下的记录,它们的贡献被历史遗忘.... 当然,本题并不会按照这样的思路解题,但却是为探索新方法提供了别样的视角~

        那么回归本题,咱们又该从哪些地方下手?

为什么选择归并?

🎃 这与我们归并的过程几乎一样~

        我们引入一个mid指针,就会将整个数组分为两个部分。逆序对如何产生呢?它可能来自:左半边数组、也可能来自右半边数组、还可能一个来自左半数组,另一个来自右半数组...所以,一旦我们想要求整个数组的逆序对,就将这三种情况相加即可~

        先求左半数组逆序对、再求右半数组、最后是左、右逆序对,这就同归并排序一样,先排序左边数组、再继续右边数组的排序,最后进行归并。我们可以在这个过程中,顺便将左、右两边的逆序对求出来,最后把这些结果相加即可~

为什么可行?

        我们在归并之前就已经统计完成了逆序对,并且归并带来的有序性,能让我们快速统计出左、右部分数组的逆序对,而不需要像暴搜那样枚举所有的情况~

 

######### 按照升序排 #############
class Solution {
public:vector<int> tmp;int mergesort(vector<int>& nums,int left,int right){// 不存在的逆序对if(left >= right) return 0;int mid = (left + right) >> 1, ret = 0;// 加左、右两边ret += mergesort(nums,left,mid);ret += mergesort(nums,mid+1,right);// 合并数组的情况下:统计ret 左、右部分// 如果是升序 --> 找右半小值int cur1 = left,cur2 = mid + 1,i = left;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){//找大值// 这里赋值给tmp是满足归并tmp[i++] = nums[cur1++];}else{// 进行更新 统计右半区ret += mid - cur1 + 1;tmp[i++] = nums[cur2++]; // 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];return ret;}int reversePairs(vector<int>& record) {tmp.resize(record.size());return mergesort(record,0,record.size()-1);}
};######### 按照降序排 #############
class Solution {
public:vector<int> tmp;int mergesort(vector<int>& nums,int left,int right){// 不存在的逆序对if(left >= right) return 0;int mid = (left + right) >> 1, ret = 0;// 加左、右两边ret += mergesort(nums,left,mid);ret += mergesort(nums,mid+1,right);// 合并数组的情况下:统计ret 左、右部分// 如果是降序 ---> 找左边值int cur1 = left,cur2 = mid + 1,i = left;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){// 不行cur2太大了 完完全全统计不了rettmp[i++] = nums[cur2++];}else{// 统计右半区ret += right - cur2 + 1;tmp[i++] = nums[cur1++];}}// 处理剩余数组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];return ret;}int reversePairs(vector<int>& record) {tmp.resize(record.size());return mergesort(record,0,record.size()-1);}
};


本篇到此结束,感谢你的阅读

祝你好运,向阳而生~

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

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

相关文章

新能源汽车充电桩消防安全视频智能可视化监管建设方案

一、方案背景 据应急管理部门统计公布的数据显示&#xff0c;仅2023年第一季度&#xff0c;新能源汽车自燃率就上涨了32%&#xff0c;平均每天就有8辆新能源汽车发生火灾&#xff08;含自燃&#xff09;。在已查明起火原因中&#xff0c;58%源于电池问题&#xff0c;19%源于碰…

输出当前时间

用途&#xff1a;在项目中一些属性中设置当前时间 实例代码 import java.time.LocalDateTime; import java.time.format.DateTimeFormatter;public class time {public static void main(String[] args){LocalDateTime china LocalDateTime.now(); DateTimeFormatter forma…

ASPICE学习笔记 ———— 过程模型(Process reference model)

文章目录 介绍过程模型Primary life cycle processes categoryAcquisition Process GroupSupply Process GroupSystem Engineering Processes GroupSoftware Engineering Processes Group Supporting life cycle processes categoryOrganizational life cycle processes catego…

【活动预告】本周四(3月28日)AI算法大模型备案线上活动

Al算法备案中心特邀十年合规专家「乐歌」&#xff0c;于本周四进行线上算法备案活动 支持AI创业者&#xff0c;免费咨询算法备案 3.28日20&#xff1a;00腾讯会议欢迎参与&#xff01; 扫码添加活动助理报名参加&#xff01;

四川宏博蓬达法律咨询有限公司:您身边的法律守护者

在快节奏的现代生活中&#xff0c;法律咨询服务已成为人们不可或缺的一部分。四川宏博蓬达法律咨询有限公司正是这样一个值得您信赖的法律服务伙伴。我们专注于为客户提供专业、高效、安全的法律服务&#xff0c;致力于成为您生活中的法律守护者。 一、专业团队&#xff0c;服务…

反沙箱思路总结

文章目录 反调试反沙箱时间对抗环境检测 反虚拟机黑DLL父进程检测傀儡进程后记 反调试 IsDebuggerPresent #include<windows.h> #include<stdio.h> BOOL check() {return IsDebuggerPresent(); } BOOL isPrime(long long number){if (number < 1)return FALSE…

制作一个RISC-V的操作系统七-UART初始化(UART NS16550A 规定 目标 发送数据 代码 extern)

文章目录 UARTNS16550A规定目标发送数据代码extern UART 对应到嵌入式开发中&#xff0c;qemu模拟的就是那块开发板&#xff08;硬件&#xff09; 电脑使用qemu时可以理解为qemu模拟了那块板子&#xff0c;同时那块板子与已经与你的电脑相连接了&#xff08;我们对应的指定的内…

水牛社五大赚钱栏目概览:轻松了解项目核心与赚钱原理

很多新用户首次访问水牛社官网时&#xff0c;可能会感到有些迷茫。由于软件介绍相对较长&#xff0c;部分朋友可能缺乏耐心细读。然而&#xff0c;若您真心希望在网络上找到赚钱的机会&#xff0c;深入了解我们的发展历程将大有裨益。简而言之&#xff0c;本文旨在快速带您领略…

基于tcp协议的网络通信(将服务端守护进程化)

目录 守护进程化 引入 介绍 如何实现 思路 接口 -- setsid 注意点 实现代码 daemon.hpp log.hpp 运行情况 前情提要 -- 前后台任务介绍(区别命令),sessionsid介绍,session退出后的情况(nuhup,终端进程控制组),任务进程组概念,任务与进程组的关系,-bash介绍-CSDN博客…

ros找不到生成的可执行文件[rosrun] Couldn‘t find executable named hello_world_cpp below

catkin_make之后source ./devel/setup.bash source之后运行节点的时候,ros找不到可执行文件&#xff08;其实tab键补不齐就没找到了&#xff09; 手动查找发现生成的可执行文件在build下不在devel/lib下&#xff0c;所以白source&#xff0c;压根找不到。 查找原因说是因为CMa…

java项目将静态资源中的文件转为浏览器可访问的http地址

新增一个类叫啥无所谓&#xff0c;主要是实现 WebMvcConfigurer 加上注解 Configuration项目启动时加入bean中 只操作addResourceHandlers这一个方法 其他都没用 文章下方附带一个简易的上传图片代码 package cn.exam.config;import org.springframework.context.annotati…

Personal Website

Personal Website Static Site Generators hexo hugo jekyll Documentation Site Generator gitbook vuepress vitepress docsify docute docusaurus Deployment 1. GitHub Pages 2. GitLab Pages 3. vercel 4. netlify Domain 域名注册 freessl 域名解析域名…

DMA控制器

前言 大家好&#xff0c;我是jiantaoyab&#xff0c;这是我作为学习笔记的25篇&#xff0c;本篇文章给大家介绍DMA。 无论 I/O 速度如何提升&#xff0c;比起 CPU&#xff0c;总还是太慢。如果我们对于 I/O 的操作&#xff0c;都是由 CPU 发出对应的指令&#xff0c;然后等待…

【数据结构】线性表的定义与基本操作

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;数据结构 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进…

用户态和内核态:操作系统权限与运行模式解析

在现代计算机操作系统中&#xff0c;用户态&#xff08;User Mode&#xff09;和内核态&#xff08;Kernel Mode&#xff09;是两种重要的运行模式&#xff0c;用于区分用户程序与操作系统核心之间的权限和特权级别。深入理解这两种模式对于理解操作系统的工作原理至关重要。 …

学习次模函数-第2章 定义

纵观本专著&#xff0c;我们认为及其幂集&#xff08;即&#xff0c; 所有子集的集合&#xff09;&#xff0c;其基数为。我们也考虑一个实值集函数&#xff0c;使得。 与凸函数的一般约定相反&#xff08;见附录A&#xff09;&#xff0c;我们不允许函数有无穷大的值。 次模分…

ssm004新生报到系统+jsp

新生报到系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对新生报到信息管理混乱&#xff0c;出错率…

虚拟线圈法的车辆统计_3.12

目标 车流量统计的方法实现车流量检测 基于虚拟线圈法的车辆统计是一种利用计算机视觉技术模拟传统物理线圈检测原理&#xff0c;对交通视频流中的车辆进行计数的方法。在传统交通监控系统中&#xff0c;物理线圈是通过感应车辆经过时产生的电磁场变化来记录车辆流量。这种方式…

模型怎么处理不同尺寸的输入图像

1.有全连接层的的CNN模型 卷积能够处理不同尺寸的输入图像&#xff0c;但全连接层不行&#xff0c;因此在送入全连接层之前需将卷积层提取的特征转换为一个固定长度的特征向量。 那么如何转换&#xff1f; 1.1 GAP(Global Average Pooling)全局平均池化 直接代码举例&#…

MySQL数据库备份及恢复

一、数据库备份的分类 1.1 从物理与逻辑的角度 从物理与逻辑的角度&#xff0c;备份可分为物理备份、逻辑备份 物理备份:对数据库操作系统的物理文件(如数据文件日志文件等)的备份 物理备份方法 冷备份(脱机备份)是在关闭数据库的时候进行的 热备份(联机备份):数…