分治归并问题

“别让自我被拯救~” 


谈谈归并与分治

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

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

        假设我们要对举例数组元素进行升序排序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太大了 完完全全统计不了ret
                tmp[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);
    }
};


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

祝你好运,向阳而生~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

哪些行业需要在线制作电子证书系统?

哪些行业需要在线制作电子证书系统&#xff1f; 1、教育机构&#xff1a;学校和培训机构需要为学生和培训者颁发证书&#xff0c;您的系统可以帮助他们快速生成和管理这些证书。 2、企业及政府部门&#xff1a;用于员工培训、资质认证等&#xff0c;提高内部管理效率。 3、专…

【C++】虚拟继承 组合

目录 一、虚拟继承 &#x1f31f;【非虚拟内存分布】 &#x1f31f;【虚拟继承内存分布】 &#x1f31f;【虚拟继承读取】 &#x1f31f;【练习检验】 &#x1f31f;【继承的总结和反思】 二、组合 &#x1f31f;【继承和组合】 &#x1f31f;【前言回顾】 上一篇文章我们…

GL-15过流继电器 10A、5A 板前接线带附件 JOSEF约瑟

系列型号&#xff1a; GL-11过流继电器; GL-12过流继电器; GL-13过流继电器; GL-14过流继电器; GL-15过流继电器; GL-16过流继电器; GL-17过流继电器; 用途 GL-10系列过流继电器(以下简称继电器)具有反时限特性&#xff0c;应用于电机、变压器等主设备以及输配电系统的继电保…

PLC_博图系列☞P:扫描操作数的信号上升沿

PLC_博图系列☞P&#xff1a;扫描操作数的信号上升沿 文章目录 PLC_博图系列☞P&#xff1a;扫描操作数的信号上升沿背景介绍P&#xff1a;扫描操作数的信号上升沿说明参数示例 关键字&#xff1a; PLC、 西门子、 博图、 Siemens 、 p 背景介绍 这是一篇关于PLC编程的文章…

QT_day3:2024/3/22

作业1&#xff1a;设计界面 使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin…

穿越地心:3D可视化技术带你领略地球内部奇观

在广袤无垠的宇宙中&#xff0c;地球是一颗充满生机与奥秘的蓝色星球。我们每天都生活在这颗星球上&#xff0c;感受着它的温暖与恩赐&#xff0c;却往往忽略了它深邃的内部世界。 想象一下&#xff0c;你能够穿越时空&#xff0c;深入地球的核心&#xff0c;亲眼目睹那些亿万年…

面向低成本巡线机器人的PID控制器优化——文末源码

目录 介绍 测试 电子元器件 系统特征 控制器设计 位置误差的计算 比例控制 积分控制 微分控制 改进的PID控制器 测试轨迹 源码链接 本文对经典PID控制器的改进和开环控制机制的发展进行了讨论&#xff0c;以提高差动轮式机器人的稳定性和鲁棒性。为了部署该算法&am…

桌面显示器PD芯片:引领桌面显示技术的新篇章

随着科技的飞速发展&#xff0c;桌面显示器作为人们日常工作与生活中不可或缺的重要设备&#xff0c;其性能与品质也在不断提升。其中&#xff0c;PD芯片作为桌面显示器中的核心组件&#xff0c;发挥着至关重要的作用。本文将对桌面显示器PD芯片进行详细介绍&#xff0c;探讨其…

【排序算法】插入排序与选择排序详解

文章目录 &#x1f4dd;选择排序是什么&#xff1f;&#x1f320;选择排序思路&#x1f309; 直接选择排序&#x1f320;选择排序优化&#x1f320;优化方法&#x1f309;排序优化后问题 &#x1f320;选择排序效率特性 &#x1f309;插入排序&#x1f320;插入排序实现 &#…

总结虚函数表机制——c++多态底层原理

前言&#xff1a; 前几天学了多态。 然后过去几天一直在测试多态的底层与机制。今天将多态的机制以及它的本质分享给受多态性质困扰的友友们。 本节内容只涉及多态的原理&#xff0c; 也就是那张虚表的规则&#xff0c;有点偏向底层。 本节不谈语法&#xff01;不谈语法&#x…

每日一题|djwcb【算法赛】|字符串快速幂

每日一题|djwcb【算法赛】 djwcb 心有猛虎&#xff0c;细嗅蔷薇。你好朋友&#xff0c;这里是锅巴的C\C学习笔记&#xff0c;常言道&#xff0c;不积跬步无以至千里&#xff0c;希望有朝一日我们积累的滴水可以击穿顽石。 djwcb 注意&#xff1a; 快速幂字符串&#xff0c;看…

js获取cookie

js获取cookie 前言实现讲解特别注意&#xff1a; 前言 主要是通过document.cookie来进行实现的 实现讲解 首先通过document.cookie 来获取到所有的cookie 然后通过分号进行分割成list 然后循环list,将list中的字符串通过首个等号进行分割然后和指定的cookie名进行比对然后返…

Android 导航方式切换

1.导航栏样式目前有&#xff0c;三键导航&#xff0c;也有全局手势导航&#xff0c;具体的设置是在setting里 setting里对应的 代码逻辑在packages\apps\Settings\src\com\android\settings\gestures\SystemNavigationPreferenceController.java static boolean isOverlayPacka…

芯片设计工程师必备基本功——《Verilog+HDL应用程序设计实例精讲》

进入芯片行业需要学习哪些基本功呢&#xff1f;其实芯片设计工程师的技能是通过多年的经验学习的。在您开始作为芯片设计工程师工作之前&#xff0c;很难给出一个需要的全面的单一列表&#xff0c;也不可能学习所有内容。话虽如此&#xff0c;但您开始芯片设计师职业生涯时必须…

图论基础|417. 太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙

目录 417. 太平洋大西洋水流问题 827.最大人工岛 127. 单词接龙 417. 太平洋大西洋水流问题 题目链接(opens new window) 有一个 m n 的矩形岛屿&#xff0c;与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界&#xff0c;而 “大西洋” 处于大陆的右边界…

EDR下的线程安全

文章目录 前记进程断链回调执行纤程内存属性修改early birdMapping后记reference 前记 触发EDR远程线程扫描关键api&#xff1a;createprocess、createremotethread、void&#xff08;指针&#xff09;、createthread 为了更加的opsec&#xff0c;尽量采取别的方式执行恶意代…

C语言---------strlen的使用和模拟实现

字符串是以‘\0’作为结束标志&#xff0c;strlen函数的返回值是‘\0’前面的字符串的个数&#xff08;不包括‘\0’&#xff09; 注意 1&#xff0c;参数指向的字符串必须以‘\0’结束 2&#xff0c;函数的返回值必须以size_t,是无符号的 使用代码 ​ #include<stdio.…

数据库备份和恢复

前期准备 至少准备两台虚拟机完成Mysql的相关操作&#xff01; 在生产环境中&#xff0c;数据的安全性至关重要&#xff0c;任何数据的丢失都可能产生严重的后果。 造成数据丢失的原因&#xff1a; 程序错误人为操作错误运算错误磁盘故障&#xff1a; 解决措施&#xff1a;rai…

eric7安装报错

如果你遇到这样的问题&#xff1a; Sorry, please install QScintilla2 and its PyQt6 wrapper. Error: DLL load failed while importing Qsci: 找不到指定的程序。 如果你遇到这样的回答pip install QScintilla 如果你执行了这样的命令后&#xff0c;依旧报上面的错&#xf…

Word邮件合并

Word邮件合并功能可以解决在Word中批量填写内容的需求&#xff0c;当需要大量格式相同&#xff0c;只修改少数相关内容时&#xff0c;例如利用Word制作工资条&#xff0c;通知函&#xff0c;奖状等等&#xff0c;同时操作也非常简单灵活。下面通过例子来说明邮件合并的使用方法…