十大排序 —— 归并排序

十大排序 —— 归并排序

  • 归并排序
  • 治(排序)
  • 归并排序的性能
  • 一些小总结

我们今天继续来学习排序算法 —— 归并排序:

归并排序

归并排序(Merge Sort)是一种高效的、稳定的排序算法,它采用分治法(Divide and Conquer)的思想来实现。归并排序的基本工作原理如下:

  1. (Divide):将当前序列分成两个尽可能相等的子序列。如果当前序列只有一个元素或者为空,则不需要进一步划分,因为它本身就是有序的。
  1. (Conquer):递归地对两个子序列分别进行归并排序。这意味着每个子序列都要重复执行划分和递归排序的过程,直到子序列不能再分割为止(即子序列中只有一个元素)。
  1. (Combine):将两个已经排序好的子序列合并成一个有序序列。合并的过程是通过比较两个子序列中的元素,将较小的元素先放入一个新的序列中,直到一个子序列为空,然后再将另一个子序列中剩余的元素依次加入新序列中。这个过程保证了新序列的整体有序性。

归并排序的关键操作是合并步骤,这也是算法名称的由来。整个算法通过这种方式逐步构建出整个序列的排序结果,从最小的子序列开始,一步步合并出更大范围的有序序列,直到整个序列变得有序。

我们一步一步来看:

归并排序首先要,将区间分成尽量相等的区间:
在这里插入图片描述

这里我们注意一下,我们一来对区间进行详尽的划分,中间并没有进行任何的其他操作。

我们可以用递归来一直模拟二分:

void merge_sort(std::vector<int>& array,
		int left,int right,std::vector<int>& temp) //temp是辅助数组
	{
		if (left >= right)
			return;

		//分
		int mid = left + (right - left) / 2;
		merge_sort(array, left, mid, temp);
		merge_sort(array, mid + 1, right, temp);
	}

划分到不能再划分时,我们进行第二步:

治(排序)

我们划分到了最后,会有两个部分的数据:
在这里插入图片描述

这个时候,我们要对两个部分数据进行排序,我们一边拿一进行比较:
在这里插入图片描述

我们用一个临时数组,用来存放比较之后的数据:
在这里插入图片描述在这里插入图片描述

此时begin2越界,只需要将左边的数据放入temp:
在这里插入图片描述

然后我们要放回原来的数组:
在这里插入图片描述整体是这样的:

在这里插入图片描述
在这里插入图片描述

// 归并排序函数
void merge_sort(std::vector<int>& array, int left, int right, std::vector<int>& temp) {
    // 基准情况:如果左边索引大于等于右边索引,说明区间内只有一个元素或无元素,不需要排序
    if (left >= right) {
        return;
    }

    // 分:找到中间索引,将当前区间分为两个子区间,递归地对它们进行排序
    int mid = left + (right - left) / 2; // 防止大数相加导致溢出
    merge_sort(array, left, mid, temp);   // 对左半区间排序
    merge_sort(array, mid + 1, right, temp); // 对右半区间排序

    // 在合并之前清空temp,确保在一个干净的环境中进行合并操作
    temp.clear();

    // 合:将两个有序子区间合并成一个有序区间
    int begin1 = left;  // 左子区间的起始索引
    int end1 = mid;     // 左子区间的结束索引
    int begin2 = mid + 1; // 右子区间的起始索引
    int end2 = right;   // 右子区间的结束索引

    // 合并两个子区间
    while (begin1 <= end1 && begin2 <= end2) {
        // 如果左子区间当前元素大于右子区间当前元素,将右子区间元素加入temp
        if (array[begin1] > array[begin2]) {
            temp.push_back(array[begin2++]);
        }
        // 反之,将左子区间元素加入temp
        else {
            temp.push_back(array[begin1++]);
        }
    }

    // 处理剩余的元素,如果左半边还有剩余,全部加入temp
    while (begin1 <= end1) {
        temp.push_back(array[begin1++]);
    }

    // 如果右半边还有剩余,全部加入temp
    while (begin2 <= end2) {
        temp.push_back(array[begin2++]);
    }

  
}

这里的合就是拷贝回原数组的过程:
在这里插入图片描述

    // 拷贝回原数组

    // 注意:这里使用i - left是因为temp是从0开始的,而原数组的子区间是从left开始的

    for (int i = left; i <= right; i++) {

        array[i] = temp[i - left];

    }
// 归并排序函数
void merge_sort(std::vector<int>& array, int left, int right, std::vector<int>& temp) {
    // 基准情况:如果左边索引大于等于右边索引,说明区间内只有一个元素或无元素,不需要排序
    if (left >= right) {
        return;
    }

    // 分:找到中间索引,将当前区间分为两个子区间,递归地对它们进行排序
    int mid = left + (right - left) / 2; // 防止大数相加导致溢出
    merge_sort(array, left, mid, temp);   // 对左半区间排序
    merge_sort(array, mid + 1, right, temp); // 对右半区间排序

    // 在合并之前清空temp,确保在一个干净的环境中进行合并操作
    temp.clear();

    // 合:将两个有序子区间合并成一个有序区间
    int begin1 = left;  // 左子区间的起始索引
    int end1 = mid;     // 左子区间的结束索引
    int begin2 = mid + 1; // 右子区间的起始索引
    int end2 = right;   // 右子区间的结束索引

    // 合并两个子区间
    while (begin1 <= end1 && begin2 <= end2) {
        // 如果左子区间当前元素大于右子区间当前元素,将右子区间元素加入temp
        if (array[begin1] > array[begin2]) {
            temp.push_back(array[begin2++]);
        }
        // 反之,将左子区间元素加入temp
        else {
            temp.push_back(array[begin1++]);
        }
    }

    // 处理剩余的元素,如果左半边还有剩余,全部加入temp
    while (begin1 <= end1) {
        temp.push_back(array[begin1++]);
    }

    // 如果右半边还有剩余,全部加入temp
    while (begin2 <= end2) {
        temp.push_back(array[begin2++]);
    }

    // 拷贝回原数组
    // 注意:这里使用i - left是因为temp是从0开始的,而原数组的子区间是从left开始的
    for (int i = left; i <= right; i++) {
        array[i] = temp[i - left];
    }
}

这段代码实现了一个标准的归并排序算法流程,包括了递归划分区间、合并已排序区间,以及最终将排序结果拷贝回原数组的操作。通过这样的分治策略,归并排序能够高效地对整个数组进行排序。

在这里插入图片描述

归并排序的性能

归并排序(Merge Sort)在性能上的主要特点如下:

  1. 时间复杂度

    • 归并排序的时间复杂度在平均情况、最好情况以及最坏情况下均为O(n log n),其中n是数组中的元素数量。这是因为归并排序总是将数组分成两半处理,每一层递归深度为log n层,每层需要线性时间n来合并,故总时间为n * log n。
    • 这种时间复杂度保证了归并排序对于大规模数据集来说是非常高效的,且性能稳定,不依赖于原始数据的排列状态。
  2. 空间复杂度

  • 归并排序的空间复杂度为O(n),主要原因是需要一个与原数组相同大小的临时数组来合并两个子数组。这是归并排序的一个缺点,尤其是在处理极大规模数据时,额外的空间需求可能成为一个限制因素。
  • 不过,也有原地归并排序(In-place Merge Sort)的变体尝试减少空间复杂度,但它们通常比标准归并排序更复杂且可能牺牲一些性能。
  1. 稳定性
  • 归并排序是一种稳定的排序算法,即相等的元素在排序前后相对位置不变。这是因为合并过程中,当遇到两个相等的元素时,总是先取左边子数组的元素,保持了稳定性。
  1. 适用场景
  • 归并排序适合于数据量较大的排序场景,特别是对稳定性有要求的情况。它也是外部排序算法的基础,例如在处理磁盘文件排序时非常有用,因为可以将数据分块读入内存进行排序后再合并。
    • 对于内存受限环境,归并排序可能不是最佳选择,此时空间效率更高的算法(如原地排序算法)可能更为合适。
  1. 比较与其他排序算法
  • 相较于快速排序,归并排序虽然两者的时间复杂度相同,但归并排序是稳定的且时间复杂度不会受输入数据的影响。而快速排序在最坏情况下可能退化到O(n^2),尽管通过随机化选取枢轴可以很大程度上避免这种情况。
  • 与插入排序、冒泡排序等简单排序算法相比,归并排序的时间复杂度明显更低,尤其在处理大量数据时优势显著,但它们的空间复杂度更低,更适合小数据集或几乎已排序的数据。

综上所述,归并排序在时间效率上表现出色,尤其适合大规模数据集的排序,但在空间使用上较为奢侈,这是使用时需要考虑的主要权衡点。

一些小总结

我们抽离一下,看看这个代码的结构:

void merge_sort(std::vector<int>& array,
		int left,int right,std::vector<int>& temp)
	{
		if (left >= right)
			return;

		//分
		int mid = left + (right - left) / 2;
		merge_sort(array, left, mid, temp);
		merge_sort(array, mid + 1, right, temp);

		temp.clear(); //保证是在一个干净的环境里进行工作

		//合
		int begin1 = left;
		int end1 = mid;

		int begin2 = mid + 1;
		int end2 = right;

		while (begin1 <= end1 && begin2 <= end2)
		{
			if (array[begin1] > array[begin2])
			{
				temp.push_back(array[begin2++]);
			}
			else
			{
				temp.push_back(array[begin1++]);
			}
		}

		//剩下的多出来的也放进去
		while (begin1 <= end1)
		{
			temp.push_back(array[begin1++]);
		}

		while (begin2 <= end2)
		{
			temp.push_back(array[begin2++]);
		}

		//铐回原来的数组
		for (int i = left; i <= right; i++)
		{
			array[i] = temp[i - left];
		}
	}

我们可以得出这样的结构:

void merge_sort(std::vector<int>& array,
		int left,int right,std::vector<int>& temp)
	{
		//终止条件
		if (left >= right)
			return;

		//分
		int mid = left + (right - left) / 2;
		merge_sort(array, left, mid, temp);
		merge_sort(array, mid + 1, right, temp);

   //操作

递归在前,操作在后,这是一个标准的后序遍历,所以归并排序的思想基础是基于二叉树的后序遍历

我们来看看快速排序:


```cpp
/**
 * 快速排序函数
 * @param array 待排序的整型数组指针
 * @param begin 数组的起始索引
 * @param end 数组的结束索引
 */
void quick_sort_part(int *array, int begin, int end) {
    int left = begin; // 初始化左指针
    int right = end;  // 初始化右指针

    // 当左指针小于右指针时继续排序
    if (left >= right) {
        return;
    }

    int stander = left; // 选择数组起始位置的元素作为基准值

    // 外层循环:直到左右指针相遇
    while (left < right) {
        // 移动右指针,直到找到一个小于等于基准值的元素或右指针到达左指针
        while (right > left && array[stander] <= array[right]) {
            right--;
        }

        // 移动左指针,直到找到一个大于等于基准值的元素或左指针到达右指针
        while (left < right && array[stander] >= array[left]) {
            left++;
        }

        // 如果左右指针未相遇,则交换这两个元素的位置
        if (left < right) {
            swap(array[left], array[right]);
        }
    }

    // 将基准值放到最终位置(左右指针相遇的位置)
    swap(array[stander], array[left]);

    // 对基准值左边的子数组进行快速排序
    quick_sort_part(array, begin, left - 1);

    // 对基准值右边的子数组进行快速排序
    quick_sort_part(array, left + 1, end);
}

提出框架:


```cpp
/**
 * 快速排序函数
 * @param array 待排序的整型数组指针
 * @param begin 数组的起始索引
 * @param end 数组的结束索引
 */
void quick_sort_part(int *array, int begin, int end) {
 	  //初始化操作

    // 当左指针小于右指针时继续排序,停止条件
    if (left >= right) {
        return;
    }

    //中间操作
     
    // 对基准值左边的子数组进行快速排序
    quick_sort_part(array, begin, left - 1);
    // 对基准值右边的子数组进行快速排序
    quick_sort_part(array, left + 1, end);
}

发现了吗,快速排序基于二叉树前序遍历的思想。

如果刷题刷的多的话,会发现大部分的问题是基于二叉树,或者n叉树的框架来的,所以有意识的积累,对解题有帮助。

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

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

相关文章

Spring原理-IOC和AOP

概述 在此记录spring的学习内容。spring官网&#xff1a;https://spring.io/ 概念故事 从前&#xff0c;在Java的大森林中&#xff0c;有一片神奇的土地&#xff0c;名叫"Spring"。这片土地上生长着各种美丽而强大的植物&#xff0c;它们分别象征着Spring框架中的…

LabVIEW调用第三方硬件DLL常见问题及开发流程

在LabVIEW中调用第三方硬件DLL时&#xff0c;除了技术问题&#xff0c;还涉及开发流程、资料获取及与厂家的沟通协调。常见问题包括函数接口不兼容、数据类型转换错误、内存管理问题、线程安全性等。解决这些问题需确保函数声明准确、数据类型匹配、正确的内存管理及线程保护。…

金钱世界:资本主义的未来

概述 《金钱世界&#xff1a;资本主义的未来》是一部探讨资本主义未来、全球经济停滞、大型全球企业与国家关系以及贫富差距问题的纪录片。纪录片分集内容&#xff1a;该纪录片共分为3集&#xff0c;每集都聚焦于不同的主题&#xff1a; 第一集《世界会继续发展吗&#xff1f…

QT实现动态翻译切换

1、实现QT动态中英文切换效果 效果如下: 2、原理 因为软件本身就是中文版,所以只需准备一个英文版的翻译即可,,那就是将所有需要翻译的地方用tr包裹,然后首先执行lupdate更新一下,接着用qt的翻译软件 Qt Linguist打开ts文件进行翻译,然后保存,最后使用 lrelease发布一…

小白跟做江科大32单片机之旋转编码器计次

原理部分按照下面这个链接理解即可y小白跟做江科大32单片机之对射式红外传感器计次-CSDN博客https://blog.csdn.net/weixin_58051657/article/details/139350487https://blog.csdn.net/weixin_58051657/article/details/139350487 实验过程 1.按照江科大老师给的电路图进行连接…

音视频开发—V4L2介绍,FFmpeg 打开摄像头输出yuv文件

实验平台&#xff1a;Ubuntu20.04 摄像头&#xff1a;1080P 监控摄像头&#xff0c;采用V4L2驱动框架 文章目录 1.V4L2相关介绍1.1. 基本概念1.2. 主要功能1.3. V4L2驱动框架1.4. 主要组件1.5. 使用V4L2的应用1.6. 常用V4L2工具 2.ffmpeg命令实现打开摄像头输出yuv文件3.使用C…

文献阅读:GCNG:用于从空间转录组数据推断基因相互作用的图卷积网络

文献介绍 「文献题目」 GCNG: graph convolutional networks for inferring gene interaction from spatial transcriptomics data 「研究团队」 Ziv Bar-Joseph&#xff08;美国卡内基梅隆大学&#xff09; 「发表时间」 2020-12-10 「发表期刊」 Genome Biology 「影响因子…

一文搞懂分布式事务Seta-AT模式实现原理

分布式事务概念 分布式事务&#xff08;Distributed Transaction&#xff09; 是指在分布式系统中&#xff0c;涉及多个数据库、服务、消息队列等资源&#xff0c;并且需要保证这些资源上的操作要么全部成功提交&#xff0c;要么全部失败回滚的一种机制。在分布式系统中&#…

极简网络用户手册(1)

极简网络系统处理流程 模块位置&#xff1a;参数平台--专题分析--极简网络分析 步骤&#xff1a; 步骤一&#xff1a;创建精细化场景策略 步骤二&#xff1a;创建任务&#xff0c;主要选择策略&#xff08;包括√配置和距离配置&#xff09;和需要处理的小区清单&#xff08;源…

vulhub中Nexus Repository Manager 3 未授权目录穿越漏洞(CVE-2024-4956)

Nexus Repository Manager 3 是一款软件仓库&#xff0c;可以用来存储和分发Maven、NuGET等软件源仓库。 其3.68.0及之前版本中&#xff0c;存在一处目录穿越漏洞。攻击者可以利用该漏洞读取服务器上任意文件。 环境启动后&#xff0c;访问http://your-ip:8081即可看到Nexus的…

德克萨斯大学奥斯汀分校自然语言处理硕士课程汉化版(第四周) - 语言建模

语言建模 1. 统计语言模型2. N-gram语言建模 2.1. N-gram语言模型中的平滑处理 3. 语言模型评估4. 神经语言模型5. 循环神经网络 5.1. Vanilla RNN5.2. LSTM 1. 统计语言模型 统计语言模型旨在量化自然语言文本中序列的概率分布&#xff0c;即计算一个词序列&#xff08;如一…

【记忆化搜索】2318. 不同骰子序列的数目

本文涉及知识点 记忆化搜索 LeetCode 2318. 不同骰子序列的数目 给你一个整数 n 。你需要掷一个 6 面的骰子 n 次。请你在满足以下要求的前提下&#xff0c;求出 不同 骰子序列的数目&#xff1a; 序列中任意 相邻 数字的 最大公约数 为 1 。 序列中 相等 的值之间&#xff…

重庆耶非凡科技有限公司的选品师项目加盟靠谱吗?

在当今电子商务的浪潮中&#xff0c;选品师的角色愈发重要。而重庆耶非凡科技有限公司以其独特的选品师项目&#xff0c;在行业内引起了广泛关注。对于想要加盟该项目的人来说&#xff0c;项目的靠谱性无疑是首要考虑的问题。 首先&#xff0c;我们来看看耶非凡科技有限公司的背…

图解 IPv6 多播范围

1、 IPv6 多播范围 2、从单播地址生成请求节点多播地址 3、已分配的多播地址

Day04 左侧菜单导航实现

一.点击左侧菜单导航到对应的View页面 1.首先在MyToDo项目中,创建出左侧菜单所有的View(视图)及对应的ViewModel(视图逻辑处理类) ViewViewModel首页IndexViewIndexViewModel待办事项ToDoViewToDoViewModel忘备录MemoViewMemoViewModel设置SettingsViewSettingsViewModel

制作一个简单HTML旅游网站(HTML+CSS+JS)云南旅游网页设计与实现5个页面

一、&#x1f468;‍&#x1f393;网站题目 旅游&#xff0c;当地特色&#xff0c;历史文化&#xff0c;特色小吃等网站的设计与制作。 二、✍️网站描述 云南旅游主题的网页 一共七个个页面 - 旅游网页使用html css js制作 有banana图 - 页面可以相互跳转 包含表单 三级页面…

XFeat:速度精度远超superpoint的轻量级图像匹配算法

代码地址&#xff1a;https://github.com/verlab/accelerated_features?tabreadme-ov-file 论文地址&#xff1a;2404.19174 (arxiv.org) XFeat (Accelerated Features)重新审视了卷积神经网络中用于检测、提取和匹配局部特征的基本设计选择。该模型满足了对适用于资源有限设备…

sqlite基本操作

简介 文章目录 简介1.数据库的安装2.数据库命令&#xff1a;API&#xff0c;创建表单代码 csprintf&#xff08;&#xff09;getchar和scanf&#xff08;&#xff09; 1.数据库的安装 sudo dpkg -i *.deb这个报错表明出现依赖问题 用这个命令后再试试sudo apt --fix-broken in…

Nocobase快速上手 - 开发第一个插件

在前面的几篇博文中&#xff0c;记录了在Nocobase中配置collection和界面&#xff0c;这篇文章开始插件的开发。插件可以扩展Nocobase的业务能力&#xff0c;解锁更强大的功能。 环境搭建 创建插件需要配置nocobase的开发环境&#xff0c;笔者采用的是clone 官方代码repo的方…

【Centos7】解决 CentOS 7 中出现 “xx: command not found“ 错误的全面指南

【Centos7】初探xx:command not found解决方案 大家好 我是寸铁&#x1f44a; 【Centos7】解决 CentOS 7 中出现 “xx: command not found” 错误的全面指南✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 前言 经常有小伙伴问我&#xff0c;xx:command not found怎么办&#xff1…