【算法】归并排序概念及例题运用

在这里插入图片描述

📢博客主页:https://blog.csdn.net/2301_779549673
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 🏳️‍🌈归并排序概念
  • 🏳️‍🌈原理与实现
  • 🏳️‍🌈性能分析
    • ❤️(一)时间复杂度
    • 🧡(二)空间复杂度
    • 💛(三)稳定性
  • 🏳️‍🌈例题分析(4题)
    • ❤️链接: [912. 排序数组](https://leetcode.cn/problems/sort-an-array/description/)
    • 🧡链接: [LCR 170. 交易逆序对的总数](https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/description/)
    • 💛链接: [315. 计算右侧小于当前元素的个数](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/description/)
    • 💚链接: [493. 翻转对](https://leetcode.cn/problems/reverse-pairs/description/)
  • 👥总结


🏳️‍🌈归并排序概念

在这里插入图片描述
归并排序是一种高效稳定的基于分治思想的排序算法,适用于多种场景。

归并排序(Merge sort)是建立在归并操作上的一种有效、稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。它将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

当有 n 个记录时,需进行 logn 轮归并排序,每一轮归并,其比较次数不超过 n,元素移动次数都是 n。因此,归并排序的时间复杂度为 O (nlogn)。归并排序时需要和待排序记录个数相等的存储空间,所以空间复杂度为 O (n)。

归并排序适用于数据量大,并且对稳定性有要求的场景。例如,在处理大量数据的数据库系统中,归并排序可以保证数据的稳定性,确保相同值的数据在排序前后的相对位置不变。同时,在一些需要对数据进行多次排序的应用中,归并排序的稳定性也能保证结果的准确性。

在归并排序的过程中,基本的操作是合并两个已排序的数组。取两个输入数组 A 和 B,一个输出数组 C,以及三个计数器 i、j、k,它们初始位置置于对应数组的开始端。A [i] 和 B [j] 中较小者拷贝到 C 中的下一个位置,相关计数器向前推进一步。当两个输入数组有一个用完时候,则将另外一个数组中剩余部分拷贝到 C 中。

总的来说,归并排序以其高效性和稳定性,在各种数据处理场景中都有着广泛的应用。


🏳️‍🌈原理与实现

在这里插入图片描述
归并排序的原理基于分治思想,其主要步骤包括分割和合并。

  • 首先,将待排序的数组不断地分割成较小的子数组,直到每个子数组只包含一个元素。此时,每个子数组都是有序的。
  • 然后,开始进行合并操作。在合并过程中,将两个已排序的子数组合并成一个更大的有序数组。具体来说,比较两个子数组的首元素,将较小的元素放入新的数组中,然后移动相应子数组的指针。
  • 重复这个过程,直到其中一个子数组被遍历完。
  • 最后,将另一个子数组中剩余的元素直接复制到新数组中。通过不断地进行分割和合并操作,最终可以得到一个完全有序的数组。

在这里插入图片描述
此外,由于归并排序是先排中间,然后再排两边的,所以可以近似看成二叉树的后序遍历,也就是先遍历左子树和右子树,再是根节点。

🏳️‍🌈性能分析

❤️(一)时间复杂度

归并排序的时间复杂度始终为 O(nlogn),这意味着无论数据的初始状态如何,归并排序的运行时间都与数据规模 n 和对数函数 logn 的乘积成正比。

归并排序采用分治法,将问题不断分解为规模更小的子问题进行求解。假设我们需要对一个包含 n个数的序列使用归并排序,并且使用的是递归的实现方式,那么过程如下:

1.递归的第一层,将 n 个数划分为 2 个子区间,每个子区间的数字个数为 n/2
2.递归的第二层,将 n 个数划分为 4个子区间,每个子区间的数字个数为 n/4.
3.递归的第三层,将 n 个数划分为8个子区间,每个子区间的数字个数为 n/8
4.递归的第 logn 层,将 n 个数划分为 n 个子区间,每个子区间的数字个数为 1。

归并排序的总时间 T(n)= 2T(n/2)+o(n)。假设解决最后的子问题用时为常数 c,则对于 n个待排序记录来说整个问题的规模为 cn 。从递归树可以看出,第一层时间代价为 cn,第二层时间代价为 cn/2 + cn/2= cn…每一层代价都是 cn,总共有 logn+1 层。所以总的时间代价为 cn*(logn+1),时间复杂度是 O(nlogn)

在不同规模数据下,归并排序的表现相对稳定。无论是小规模数据还是大规模数据,其时间复杂度都保持在 O(nlogn)。对于小规模数据,虽然归并排序可能会显得有些“大材小用”,但其时间复杂度不会急剧上升。而对于大规模数据,归并排序的优势更加明显,相比一些时间复杂度较高的排序算法,如冒泡排序、选择排序等,归并排序能够在更短的时间内完成排序任务。

🧡(二)空间复杂度

归并排序需要额外的 O(n)空间来保存临时数组。在归并过程中,需要创建临时数组来存储合并后的结果。这意味着归并排序的空间开销与数据规模 成正比。

这种空间开销对算法性能有一定的影响。一方面,额外的空间需求可能会在处理大规模数据时占用较多的内存资源。特别是在内存有限的环境中,可能会导致内存不足的问题。另一方面,创建临时数组和进行数据复制也会带来一定的时间开销。然而,归并排序的稳定性和高效的时间复杂度在很多情况下可以弥补其空间复杂度较高的不足。在一些对稳定性要求较高的场景中,归并排序仍然是一个不错的选择。

💛(三)稳定性

归并排序是稳定排序算法,这意味着在排序过程中,相等元素的相对位置不会改变。

归并排序之所以是稳定的,原因在于其合并过程。在合并两个已排序的子数组时,如果两个元素相等,归并排序会先将位于前面子数组中的元素放入新的数组中,从而保证了相等元素的相对位置不变。

在实际应用中,稳定性具有重要意义。例如,在对学生成绩进行排序时,如果有多个学生得分相同,稳定排序可以保持他们原来的顺序,这对于后续的数据分析和处理非常重要。另外,在一些需要保持数据原有顺序关系的场景中,归并排序的稳定性可以确保排序结果的准确性和可靠性。

🏳️‍🌈例题分析(4题)

❤️链接: 912. 排序数组

在这里插入图片描述

  1. 明确我们的目的,是通过使用归并排序算法对给定的整数向量进行排序
  2. 对左右两边的数组进行排序
  3. 创建临时变量数组,通过双指针,使左右数组合并有序
class Solution {
    int tmp[50010];
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        mergesort(nums, 0, n-1);
        return nums;
    }
    void mergesort(vector<int>& nums, int l, int r){

        // 1.获取中间点坐标
        if(l >= r) return;
        int mid = l + ((r - l) >> 1);
        
        // 2. 利用归并排序自身对左右范围的数组进行,是左右两边都有序
        mergesort(nums, l, mid);
        mergesort(nums, mid + 1, r);

        // 3.创建临时变量数组,同时利用双指针,将左右有序数组合并
        int index = l;
        int cur1 = l, cur2 = mid + 1;
        while(cur1 <= mid && cur2 <= r){
            if(nums[cur1] < nums[cur2]) tmp[index++] = nums[cur1++];
            else tmp[index++] = nums[cur2++];
        }

        // 4.使左右两边的数组充分排序进入临时数组
        while(cur1 <= mid) tmp[index++] = nums[cur1++];
        while(cur2 <= r) tmp[index++] = nums[cur2++];

        // 5.将临时数组中的元素返回,使原数组有序
        for(int i = l; i <= r; ++i) nums[i] = tmp[i];
    }
};

🧡链接: LCR 170. 交易逆序对的总数

在这里插入图片描述
因为归并排序具有较好的稳定性,所以我们利用归并排序排升序的同时,计算每次右边数组大于左边数组元素的个数,并返回

class Solution {
int tmp[50010];
public:
    int reversePairs(vector<int>& record) {
        return mergesort(record, 0, record.size() - 1);
    }   

    int mergesort(vector<int>& record, int l, int r){
        // 1.获取中间点坐标
        if (l >= r) return 0;
        int mid = l + ((r - l) >> 1);
        int cur1 = l, cur2 = mid + 1, ret = 0;

        // 2. 利用归并排序自身对左右范围的数组进行,是左右两边都有序
        ret += mergesort(record, l, mid);
        ret += mergesort(record, mid+1, r);
        
        // 3.利用双指针合并数组
        // 4.利用升序数组的特性,计算当前两组元素中符合前一天的股价高于后一天的股价的组合个数
        int index = 0;
        while(cur1 <= mid && cur2 <= r){
            if(record[cur1] > record[cur2]) ret += mid - cur1 + 1, tmp[index++] = record[cur2++];
            else tmp[index++] = record[cur1++];
        }
        while(cur1 <= mid) tmp[index++] = record[cur1++];
        while(cur2 <= r) tmp[index++] = record[cur2++];

        // 5.将临时数组中的元素返回,使原数组有序
        for(int i=0; i<index; ++i) record[l+i] = tmp[i];
        return ret;
    }
};

💛链接: 315. 计算右侧小于当前元素的个数

在这里插入图片描述
整体做法和上题如出一辙,不过这里需要返回对应的元素个数组成的vector,所以我们需要提前建立可以用来标记元素下标和记录满足元素个数数量的返回数组

class Solution {
    // 返回值
    vector<int> ret;
    // 记录原始下标
    vector<int> index;
    // 临时变量数组
    int tmpnums[100010];
    // 临时下标数组
    int tmpindex[100010];

public:
    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        ret.resize(n);
        index.resize(n);
        // 获取原始下标
        for (int i = 0; i < n; ++i)
            index[i] = i;
        mergesort(ret, index, nums, 0, n - 1);
        return ret;
    }
    void mergesort(vector<int>& ret, vector<int>& index, vector<int>& nums, int left, int right) {
        if (left >= right)
            return;
        int mid = left + ((right - left) >> 1);

        mergesort(ret, index, nums, left, mid);
        mergesort(ret, index, nums, mid + 1, right);

        int cur1 = left, cur2 = mid + 1, i = 0;
        while (cur1 <= mid && cur2 <= right) {
            if (nums[cur2] >= nums[cur1]) {
                tmpindex[i] = index[cur2];
                tmpnums[i++] = nums[cur2++];
            } else {
                ret[index[cur1]] += right - cur2 + 1;
                tmpindex[i] = index[cur1];
                tmpnums[i++] = nums[cur1++];
            }
        }
        while (cur1 <= mid)
            tmpindex[i] = index[cur1], tmpnums[i++] = nums[cur1++];
        while (cur2 <= right)
            tmpindex[i] = index[cur2], tmpnums[i++] = nums[cur2++];

        for (int j = left; j <= right; ++j)
            index[j] = tmpindex[j - left], nums[j] = tmpnums[j - left];
    }
};

💚链接: 493. 翻转对

在这里插入图片描述
大体规则相同,当这里用降序更容易实现

class Solution {
    int tmp[50010];

public:
    int reversePairs(vector<int>& nums) {
        int n = nums.size();
        return mergesort(nums, 0, n - 1);
    }

    int mergesort(vector<int>& nums, int left, int right) {
        if (left >= right)
            return 0;
        int ret = 0;
        int mid = (left + right) >> 1;

        ret += mergesort(nums, left, mid);
        ret += mergesort(nums, mid + 1, right);

        int cur1 = left, cur2 = mid + 1, i = left;
        while (cur1 <= mid) {
            while (cur2 <= right && nums[cur2] >= nums[cur1] / 2.0)
                cur2++;
            if (cur2 > right)
                break;
            ret += right - cur2 + 1;
            cur1++;
        }

        
        cur1 = left, cur2 = mid + 1;
        while (cur1 <= mid && cur2 <= right) {
            tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];
        }
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];

        for (int j = left; j <= right; ++j)
            nums[j] = tmp[j];
        return ret;
    }
};

👥总结

本篇博文对 归并排序概念及例题运用 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

请添加图片描述

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

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

相关文章

linux链接、目标文件全解析

内容目录 内容目录 链接 1. 静态链接2. 目标文件3. 可重定位目标文件4. 符号和符号表5. 符号解析 5.1 链接器如何解析多重定义的符号5.2 与静态库链接5.3 链接器如何使用静态库来解析引用 6. 重定位 6.1 重定位条目 - 6.2 重定位符号引用 6.2.1 重定位PC相对引用6.2.2 重定位…

计算机系统的层次

目录 计算机系统的层次ISA&#xff08;指令集体系结构&#xff09; 计算机系统的层次 计算机硬件是基础指令集体系结构&#xff1a;将硬件的功能封装从指令供软件使用操作系统&#xff1a;提供人机交互界面、提供服务功能的内核例程语言处理系统&#xff1a; 语言处理程序&…

群晖通过 Docker 安装 GitLab

Docker 配置容器步骤都是大同小异的&#xff0c;可以参考&#xff1a; 群晖通过 Docker 安装 Gitea-CSDN博客 1. 在 Docker 文件夹中创建 GitLab&#xff0c;并创建子文件夹 2. 设置权限 3. 打开 Docker 应用&#xff0c;并在注册表搜索 gitlab-ce 4. 选择 gitlab-ce 映像运行…

什么是不同类型的微服务测试?

大家好&#xff0c;我是锋哥。今天分享关于【什么是不同类型的微服务测试&#xff1f;】面试题&#xff1f;希望对大家有帮助&#xff1b; 什么是不同类型的微服务测试&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 微服务架构中的测试可以分为多种类…

多尺度建模:从理论到实践的深入探讨

#1024程序员节 | 征文# 引言 在现代科学与工程中&#xff0c;很多现象和过程在不同的空间和时间尺度上展现出复杂性。因此&#xff0c;能够有效地进行多尺度建模&#xff0c;已经成为了许多领域&#xff08;如物理、生物、工程、环境科学等&#xff09;研究的一个重要方向。本…

vue后台管理系统从0到1(5)

文章目录 vue后台管理系统从0到1&#xff08;5&#xff09;完善侧边栏修改bug渲染header导航栏 vue后台管理系统从0到1&#xff08;5&#xff09; 接上一期&#xff0c;我们需要完善我们的侧边狼 完善侧边栏 我们在 element 组件中可以看见&#xff0c;这一个侧边栏是符合我们…

【操作系统】06.进程控制

一、进程创建 1.1 认识fork函数 在linux中fork函数是非常重要的函数&#xff0c;它从已存在进程中创建一个新进程。新进程为子进程&#xff0c;而原进程为父进程。 进程调用fork&#xff0c;当控制转移到内核中的fork代码后&#xff0c;内核将 分配新的内存块和内核数据结构…

Aspose.PDF功能演示:使用 JavaScript 从 PDF 中提取文本

在数据提取、业务文档自动化和文本挖掘方面&#xff0c;使用 JavaScript 从PDF中提取文本非常有用。它允许开发人员自动执行从 PDF 收集信息的过程&#xff0c;从而显著提高处理大量文档的生产力和效率。在这篇博文中&#xff0c;我们将学习如何使用 JavaScript 从 PDF 中提取文…

人工智能的未来应用与发展前景

随着人工智能&#xff08;AI&#xff09;技术的快速进步&#xff0c;我们正亲历着它在各行各业中带来的巨大变革。无论是医疗、企业管理&#xff0c;还是日常生活&#xff0c;AI 技术都在改变着我们的工作和生活方式。那么&#xff0c;人工智能的应用前景究竟如何&#xff1f;它…

【消息队列】RabbitMQ实现消费者组机制

目录 1. RabbitMQ 的 发布订阅模式 2. GRPC 服务间的实体同步 2.1 生产者服务 2.2 消费者服务 3. 可靠性 3.1 生产者丢失消息 3.2 消费者丢失消息 3.3 RabbitMQ 中间件丢失消息 1. RabbitMQ 的 发布订阅模式 https://www.rabbitmq.com/tutorials/tutorial-three-go P 生…

winUI3 c++ 入门 2、 样式

目录 一、winUI3 基本概念及样式 1、边距 2、如何使用样式 1)、布局控件内定义样式 2)、APP.xmal定义全局样式 3)、单独的样式文件 3.1)、新增字典资源 xmal 3.2)、在里面设置样式 3.3)、引用样式 3、更多样式修改 1)、修改默认属性 2)、修改所有的默认颜色…

垃圾收集器与内存分配机制(一)

目录 一、为什么我们要去了解垃圾收集和内存分配 二、对象已死&#xff1f; 1. 引用计数算法 2. 可达性分析算法 3. 再谈引用 4. 生存还是死亡 5. 回收方法区 三、垃圾收集算法 1. 简介 2. 分代收集理论 2.1. 弱分代/强分代假说 2.2. 前面两代假说的缺陷 3. 标记-清…

智能去毛刺:2D视觉引导机器人如何重塑制造业未来

机器人技术已经深入到各个工业领域中&#xff0c;为制造业带来了前所未有的变革。其中&#xff0c;2D视觉引导机器人技术以其精准、高效的特点&#xff0c;在去毛刺工艺中发挥着越来越重要的作用。本文将为您介绍2D视觉引导机器人技术的基本原理及其在去毛刺工艺中的应用&#…

blender 理解 积木组合 动画制作 学习笔记

一、学习blender视频教程链接 案例2&#xff1a;积木组合_动画制作_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Bt4y1E7qn?vd_sourced0ea58f1127eed138a4ba5421c577eb1&p10&spm_id_from333.788.videopod.episodes 二、说明 之前已经学习了如何制作积木组…

20 Shell Script输入与输出

标出输入、标准输出、错误输出 一、程序的基本三个IO流 一&#xff09;文件描述符 ​ 任何程序在Linux系统中都有3个基本的文件描述符 ​ 比如: ​ cd/proc/$$/fd ​ 进入当前shell程序对于内核在文件系统的映射目录中: [rootlocalhost ~]# cd /proc/$$/fd [rootlocalhos…

Ubuntu22.04环境搭建MQTT服务器

官网&#xff1a; https://mosquitto.org 1.引入库 sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa2.升级安装工具 sudo apt-get update 3.安装 sudo apt-get install mosquitto 4.安装客户端 sudo apt-get install mosquitto-clients5.添加修改配置文件 进…

微信小程序上传图片添加水印

微信小程序使用wx.chooseMedia拍摄或从手机相册中选择图片并添加水印&#xff0c; 代码如下&#xff1a; // WXML代码&#xff1a;<canvas canvas-id"watermarkCanvas" style"width: {{canvasWidth}}px; height: {{canvasHeight}}px;"></canvas&…

【Linux】冯诺依曼体系结构 OS的概念

&#x1fa90;&#x1fa90;&#x1fa90;欢迎来到程序员餐厅&#x1f4ab;&#x1f4ab;&#x1f4ab; 主厨&#xff1a;邪王真眼 主厨的主页&#xff1a;Chef‘s blog 所属专栏&#xff1a;青果大战linux 总有光环在陨落&#xff0c;总有新星在闪烁 前言废话&#xff1a…

将java项目jar包打包成exe服务

1.结构展示 2.注意事项 前提: 环境准备:jdk8 和 .net支持 { 1.控制面板》程序和功能》启用和关闭windows功能》.net的勾选》2.jdk8自行百度安装环境3.其他项目必须的软件环境安装等&#xff08;数据库...&#xff09; }第一次准备: 1.将打包好的jar包放到premiumServices.exe…

销冠教你如何转化观望客户

在销售实践中&#xff0c;常会遇到这样的场景&#xff1a;客户对我们的提案表现出极大的兴趣&#xff0c;但在执行阶段却显得迟疑&#xff0c;频繁表示“还需观望&#xff0c;再考虑”。这种态度不仅拖慢了项目进度&#xff0c;甚至可能导致项目完全停滞&#xff0c;从而错失宝…