排序算法——归并排序(递归与非递归)

news2025/5/18 16:26:20

归并排序

以升序为例

文章目录

  • 归并排序
    • 基本思想
    • 核心步骤
    • 递归写法
      • 实现代码
    • 非递归
      • 处理边界情况
      • 实现代码
    • 时间复杂度

基本思想

  • 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
  • 如果对两个有序序列的归并操作还不太熟悉,建议先看看合并两个有序链表

核心步骤

在这里插入图片描述

  • 由上图我们可以看到,归并排序首先要对待排序列不断二分,直到分成不可分割的子序列(即只有一个元素的序列,相当于有序)
  • 然后,再对有序的子序列不断进行归并操作,最后得到完全有序的序列。
  • 归并排序有递归和非递归两种写法,接下来我们来讨论如何实现的具体细节:

递归写法

  • 首先我们要注意,在进行归并操作时,为了防止原序列的元素被覆盖而导致排序错误,我们需要向内存申请一块空间用来临时存放合并的序列,同时,由于归并的此时不止一次,为防止多次申请内存而导致效率不高,我们直接向内存申请一块和原序列大小相等的空间
void MergeSort(int *nums, int numsSize)
{
	int* temp = (int*)malloc(sizeof(int) * numsSize);
	…………;
	free(temp);                           
}
  • 同时,归并排序在进行归并操作时需要知道每个子序列的区间,由于递归参数的限制,我们需要再定义一个子函数MergeSort(),并对这个子函数递归
void _MergeSort(int *nums, int *temp, int left, int right)
{
    …………;
}

void MergeSort(int *nums, int numsSize)
{
	int* temp = (int*)malloc(sizeof(int) * numsSize);
	_MergeSort(nums, temp, 0, numsSize - 1);
	free(temp);                           
}
  • 易知,当子序列长度为1时,就可以不再进行二分
void _MergeSort(int *nums, int *temp, int left, int right)
{
	if (left >= right)
		return;
	…………;
}
  • 对待排序列的左半部分和右半部分不断递归分割
void _MergeSort(int *nums, int *temp, int left, int right)
{
	if (left >= right)
		return;

	int mid = (right - left) / 2 + left;
	_MergeSort(nums, temp, left, mid);
	_MergeSort(nums, temp, mid + 1, right);

    ……………;
}
  • 接下来,就是对两个有序序列的合并操作
  • 注:可以走到合并这一步说明待合并的两个序列[left,mid]和[mid + 1,right]是有序的,存在两种情况:
    • 情况一:例如序列{9,2},进入函数_MergeSort()后,其子序列是单个数字,满足left >= right的条件,直接退出递归,开始合并。
    • 情况二:例如序列{9,2,5,4},进入函数_MergeSort()后,子序列{9,2}递归,合并后退出递归,然后子序列{5,4}递归,合并,退出递归,最后就变成了两个有序序列{2,9}和{4,5}的合并
    • 建议对递归不是很清楚的小伙伴可以尝试画画递归展开图,这对了解递归逻辑大有裨益。
void _MergeSort(int *nums, int *temp, int left, int right)
{
	if (left >= right)
		return;

    //递归分割
	int mid = (right - left) / 2 + left;
	_MergeSort(nums, temp, left, mid);
	_MergeSort(nums, temp, mid + 1, right);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;

    //归并
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (nums[begin1] > nums[begin2])
			temp[index++] = nums[begin2++];
		else
			temp[index++] = nums[begin1++];
	}
	while (begin1 <= end1)
		temp[index++] = nums[begin1++];
	while (begin2 <= end2)
		temp[index++] = nums[begin2++];

    //将temp暂时存储的数据覆盖待排序列nums原有位置的数据,实现待排序列区间有序
	for (int i = left; i <= right; i++)
		nums[i] = temp[i];
}

实现代码

void _MergeSort(int *nums, int *temp, int left, int right)
{
	if (left >= right)
		return;

    //递归分割
	int mid = (right - left) / 2 + left;
	_MergeSort(nums, temp, left, mid);
	_MergeSort(nums, temp, mid + 1, right);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;

    //归并
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (nums[begin1] > nums[begin2])
			temp[index++] = nums[begin2++];
		else
			temp[index++] = nums[begin1++];
	}
	while (begin1 <= end1)
		temp[index++] = nums[begin1++];
	while (begin2 <= end2)
		temp[index++] = nums[begin2++];

    //将temp暂时存储的数据覆盖待排序列nums原有位置的数据(拷贝回去),实现待排序列区间有序
	for (int i = left; i <= right; i++)
		nums[i] = temp[i];
}

void MergeSort(int *nums, int numsSize)
{
	int* temp = (int*)malloc(sizeof(int) * numsSize);
	_MergeSort(nums, temp, 0, numsSize - 1);
	free(temp);                           
}

非递归

  • 我们可以直接用循环解决问题,如图所示:

在这里插入图片描述

  • 由上面的递归分析可以知道,两个单个数字可以直接合并成一个有序序列。因此我们定义gap,表示每次合并的两个序列长度为gap,gap从1递增,直到不能满足条件gap < numsSize,然后就进行和递归相同的合并操作就可以了
void MergeSort(int* nums, int numsSize)
{
	int* temp = (int*)malloc(sizeof(int) * numsSize);

	int gap = 1;

	while (gap < numsSize)
	{
        /*
        	因为每次是对两个有序序列进行合并
        	因此每次合并过后i应该跳过两个序列长度
        */
		for (int i = 0; i < numsSize; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int index = begin1;
			
            //归并
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (nums[begin1] < nums[begin2])
					temp[index++] = nums[begin1++];
				else
					temp[index++] = nums[begin2++];
			}
			while(begin1 <= end1)
				temp[index++] = nums[begin1++];
			while(begin2 <= end2)
				temp[index++] = nums[begin2++];
			
            //将temp暂时存储的数据覆盖待排序列nums原有位置的数据(拷贝回去),实现待排序列区间有序
			for (int j = i; j <= end2; j++)
				nums[j] = temp[j];
		}

		gap *= 2;
	}

	free(temp);
}

处理边界情况

  • 看起来好像很简单,但上面的代码仍存在些许bug,仍需要我们谨慎处理
  • 我们来看一个情况,如果给的待排数组是{5,4,3,2,9,7,1,6,8}

在这里插入图片描述

  • 如果给的待排数组是{5,4,3,2,9,7

在这里插入图片描述

int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int index = begin1;

/*
	如果右半区间不存在,只有左半区间
	说明待合并的只有一个区间
	显然没有合并的必要,直接退出合并循环即可
*/
if (begin2 >= numsSize)
    break;

//如果右半区间算多了,那么对end2进行修正
if (end2 >= numsSize)
    end2 = numsSize - 1;

实现代码

void MergeSort(int* nums, int numsSize)
{
	int* temp = (int*)malloc(sizeof(int) * numsSize);

	int gap = 1;

	while (gap < numsSize)
	{
        /*
        	因为每次是对两个有序序列进行合并
        	因此每次合并过后i应该跳过两个序列长度
        */
		for (int i = 0; i < numsSize; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;
            int index = begin1;

            /*
                如果右半区间不存在,只有左半区间
                说明待合并的只有一个区间
                显然没有合并的必要,直接退出合并循环即可
            */
            if (begin2 >= numsSize)
                break;

            //如果右半区间算多了,那么对end2进行修正
            if (end2 >= numsSize)
                end2 = numsSize - 1;
            
            
			 //归并
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (nums[begin1] < nums[begin2])
					temp[index++] = nums[begin1++];
				else
					temp[index++] = nums[begin2++];
			}
			while(begin1 <= end1)
				temp[index++] = nums[begin1++];
			while(begin2 <= end2)
				temp[index++] = nums[begin2++];
			
            //将temp暂时存储的数据覆盖待排序列nums原有位置的数据(拷贝回去),实现待排序列区间有序
			for (int j = i; j <= end2; j++)
				nums[j] = temp[j];
		}

		gap *= 2;
	}

	free(temp);
}

时间复杂度

  • 易得,合并两个有序序列的时间复杂度为O(N)
  • 由于是对待排序列的不断二分,知道分割为不可分割的子序列,因此这一过程的时间复杂度为O(log2N)
  • 因此归并排序的时间复杂度为O(NlogN)

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

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

相关文章

python:并发编程(十九)

前言 本文将和大家一起探讨python并发编程的实际项目&#xff1a;win图形界面应用&#xff08;篇一&#xff0c;共八篇&#xff09;&#xff0c;系列文章将会从零开始构建项目&#xff0c;并逐渐完善项目&#xff0c;最终将项目打造成适用于高并发场景的应用。 本文为python并…

跨模态检索论文阅读:(PTP)Position-guided Text Prompt for Vision-Language Pre-training

(PTP)Position-guided Text Prompt for Vision-Language Pre-training 视觉语言预训练的位置引导文本提示 摘要 视觉语言预训练(VLP)已经显示出将图像和文本对统一起来的能力&#xff0c;促进了各种跨模态的学习任务。 然而&#xff0c;我们注意到&#xff0c;VLP模型往往缺乏…

Redis 2023面试5题(五)

一、Redis主节点岩机导致数据全部丢失怎么恢复数据 1. 备份恢复&#xff1a; 如果你已经设置了定期备份&#xff0c;可以使用备份文件进行恢复。首先&#xff0c;停止Redis服务器&#xff0c;将备份文件复制到Redis数据目录中&#xff0c;然后启动Redis服务器。这将恢复备份时…

Maven学习1

概述 主要学习记录Maven仓库相关知识&#xff0c;如何借助上传项目jar包到GitHub、Nexus Sonatype&#xff0c;&#xff0c;以及搭建自己的Nexus Sonatype私服&#xff0c;然后在Maven项目的pom文件引入使用&#xff0c;参考Maven官网文档:https://central.sonatype.org/publi…

0基础学习地平线QAT量化感知训练

文章目录 1. 背景2. 基础理论知识3. 文件准备与程序运行4. 代码详解4.1 导入必要依赖4.2 主函数4.3 构建fx模式所需要的float_model4.4 不同阶段模型的获取4.5 定义常规模型训练与验证的函数4.6 float与qat训练代码解读——float_model/qat_model4.7 模型校准部分的代码解读——…

day58_LayUI

Layui 一、介绍 layui&#xff08;谐音&#xff1a;类 UI) 是一套开源的Web UI解决方案&#xff0c;采用自身经典的模块化规范&#xff0c;并遵循原生HTML/CSS/JS的开发方式&#xff0c;常适合网页界面的快速开发。layui 区别于那些基于MVVM 底层的前端框架&#xff0c;它更多…

2020新基建决赛-misc-ezPIC

2020新基建决赛-misc-ezPIC 一、概要 1、标题&#xff1a;ezPIC 2、关键字&#xff1a;盲水印、png宽高 3、比赛&#xff1a;2020新基建决赛 4、工具&#xff1a;python、010editor 二、开始 1、题目分析 在此感谢csdn上星辰之门和零食商人两位大佬给出的解题思路。 题目解…

黑马头条-day02

文章目录 前言一、文章列表加载1.1 需求分析1.2 表结构分析1.3 导入文章数据库1.4 实现思路1.5 接口定义1.6 功能实现 二、freemarker2.1 freemarker简介2.2 环境搭建&&快速入门2.2.1 创建测试工程 2.3 freemarker基础2.3.1 基础语法种类2.3.2 集合指令2.3.3 if指令2.3…

#10036. 「一本通 2.1 练习 2」Seek the Name, Seek the Fame

字符串的题真的要比dfs&#xff0c;bfs的简单好多 大致思路 首先&#xff0c;再度重复哈希函数 H ( C ′ ) H ( C , k n ) − H ( C , k ) ∗ b n H(C)H(C,kn)-H(C,k)*b^n H(C′)H(C,kn)−H(C,k)∗bn具体模板详见我的上几篇题解 哈希函数模板对此题&#xff0c;我们只需要对…

RPC 框架架构设计

RPC 框架架构设计 RPC 又称远程过程调用&#xff08;Remote Procedure Call&#xff09;&#xff0c;用于解决分布式系统中服务之间的调用问题。通俗地讲&#xff0c;就是开发者能够像调用本地方法一样调用远程的服务。下面我们通过一幅图来说说 RPC 框架的基本架构。 RPC 框架…

(二)WPF - 应用程序

一、运行程序的过程&#xff1a; &#xff08;1&#xff09; Application 对象被构造出来。&#xff08;2&#xff09;Run方法被调用。&#xff08;3&#xff09;Application.Startup 事件被触发&#xff08;4&#xff09;用户代码构造一个或多个 Window 对象。&#xff08;5&…

Hibernate知识总结

关于Hibernate映射 关于Hibernate的映射要说明的一点就是关于ID的访问权限&#xff0c;property以及field的区别。以前使用的时候根本没有注意过这个问题&#xff0c;这里简单的强调一下。 表的主键在内存中对应一个OID对象描述标识符&#xff0c;需要在xml的配置文件中要指定对…

数据结构——队列的实现

队列&#xff0c;又称为伫列&#xff08;queue&#xff09;&#xff0c;计算机科学中的一种抽象资料类型&#xff0c;是先进先出&#xff08;FIFO, First-In-First-Out&#xff09;的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端&#xff08;称为rear&…

Hadoop 集群如何升级?

前言 本文隶属于专栏《大数据技术体系》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见大数据技术体系 正文 升级 Hadoop 集群需要细致的规划&#xff0c;特…

Web安全——JavaScript基础

JavaScript基础 一、概述二、嵌入方法1、内嵌式2、外链式3、行内式 三、语句四、注释五、变量六、JavaScript 保留关键字七、JavaScript 作用域1、JavaScript 局部变量2、JavaScript 全局变量 八、数据类型1、判断类型2、数字类型&#xff08;Number&#xff09;3、字符串型&am…

windows第三大结构体--KPCR

前面我们介绍了windows的两大结构体&#xff0c;一个是进程结构体&#xff0c;一个是线程结构体。那么第三个就是KPCR。KPCR是什么呢&#xff0c;是用于描述CPU的结构体。每一个CPU都有一个这样的结构体来描述CPU干了什么事。 1.在当线程切换的时候&#xff0c;也就是线程从3环…

Windows和Linux动态注入

摘要&#xff1a;最近对动态注入有一些兴趣因此搜索了些资料&#xff0c;简单整理了下相关的技术实现。本文只能够带你理解何如注入以及大概如何实现&#xff0c;对注入的方法描述的并不详细。   关键字&#xff1a;dll注入&#xff0c;hook&#xff0c;提权   读者须知&am…

hadoop -- Yarn资源管理

Yarn YARN被设计用以解决以往架构的需求和缺陷的资源管理和调度软件。 Apache Hadoop YARN &#xff08;Yet Another Resource Negotiator&#xff0c;另一种资源协调者&#xff09;是一种新的 Hadoop 资源管理器&#xff0c;它是一个通用资源管理系统和调度平台&#xff0c;可…

基于uprobe的调试调优浅析

uprobe与krobe对应&#xff0c;动态附加到用户态调用函数的切入点称为uprobe&#xff0c;相比如kprobe 内核函数的稳定性&#xff0c;uprobe 的函数由开发者定义。uprobe是用户态的探针&#xff0c;它和kprobe是相对应的&#xff0c;kprobe是内核态的探针。uprobe需要制定用户态…

深度学习模型压缩方法综述

深度学习因其计算复杂度或参数冗余,在一些场景和设备上限制了相应的模型部署,需要借助模型压缩、系统优化加速等方法突破瓶颈,本文主要介绍模型压缩的各种方法,希望对大家有帮助。 1,模型压缩技术概述 我们知道,一定程度上,网络越深,参数越多,模型也会越复杂,但其最终…