八大算法排序@归并排序(C语言版本)

目录

  • 归并排序
    • 概念
    • 算法思想
      • 第一步
      • 第二步
      • 第三步
    • 算法步骤
    • 代码实现
      • 代码1
      • 代码优化
    • 时间复杂度
    • 空间复杂度
    • 特性总结

归并排序

概念

  归并排序(Merge Sort)是一种基于分治策略的经典排序算法。它的基本思想是将待排序的数组划分成两个子数组,分别对这两个子数组进行递归排序,然后将已排序的子数组合并成一个有序的数组。归并排序的关键在于合并操作,这是该算法的核心。



算法思想

  归并、归并,其实可以认为就是递归+合并。递归就是将待排序的数组通过递归,细分到子数组有序为止。最差的情况,如细分到数组只剩一个元素,那么该数组既可以认为是升序的,也可以认为是降序的,总而言之,是有序的。
然后将一个个有序的数组,进行合并,最终合并成一个有序的数组。因此该排序算法的核心便是合并算法

我们借助数组arr = { 6 , 4 , 3 , 2 , 5 , 8 , 1 , 7 }。借用图形模拟演示下流程。

流程图

通过上图所示的流程图,或许看着比较通俗易懂,然后实际上用代码实现起来还是没有想象中的那么简单的。




第一步

首先,我们不可能如流程图演示一样,递归一次就开辟一些新的数组,而且频繁的开辟数组也会造成性能的浪费。因此,在一开始便会申请一块与待排序数组同样空间大小的临时数组tmp。

// 归并排序 - 递归实现
void MergeSort(int* a, int n)
{
	assert(a);	// 确保数组不为空

	int* tmp = (int*)malloc(sizeof(int) * n);

	free(tmp);	// 申请的空间,没用时要主动释放
}

解决了临时空间的问题,下一步我们将着手解决递归和合并的问题。




第二步

因为待排序的数据与后序递归细分到有序数组都是一样的问题,我们可以统一给它们划分成一个子问题,如以下的_MergeSort()函数:


// 归并排序 - 递归实现
void MergeSort(int* a, int n)
{
	assert(a);

	int* tmp = (int*)malloc(sizeof(int) * n);

	_MergeSort(a, 0, n - 1, tmp);	// 子问题,解决递归和合并的问题

	free(tmp);

}

因为递归划分数组时,是根据数组下标进行划分的,因此子函数设计时,传入数组下标的范围更佳,同时要将临时数组tmp也传过去。




第三步

  如下,对数组进行划分,分别用left 和 right 接收传入的数组下标的范围,然后通过下标算出数组的中间下标值,用 变量mid接收,根据变量mid,将数组划分为两个区间,区间范围为:[ left , mid ] 、 [ mid+1 , right ] 。
而对于[ left , mid ] 和 [ mid+1 , right ] 两个子数组若是有序,则可以进行合并;如果还没有序时,依旧是子问题,这便是递归的由来。
  子函数_MergeSort(),传入的参数依旧是待排序数组的下标范围,和临时辅助的数组tmp。如下代码所示:

// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void _MergeSort(int* a, int left, int right, int* tmp)
{
	int mid = (left + right) / 2;
	// 分割为两个区间[left,mid]   [mid+1,right]
	//[left,mid] [mid+1,right] 有序,则可以合并,他们还没有序时,子问题解决
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);
}

观察以上的代码,我们发现,
1、递归函数中,缺少了结束条件,这将导致一直递归个不停,从而导致栈溢出,致使程序崩溃。而如何确定结束条件呢?回顾流程图,当数组中只有一个元素时,便可以认为数组是有序的了,即当待排序数组的下标范围, left >= right 时便可以结束递归,返回,进行合并了。
2、缺少合并的步骤。

因此,要解决以上两个问题,如下:

// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = (left + right) / 2;
	// 分割为两个区间[left,mid]   [mid+1,right]
	//[left,mid] [mid+1,right] 有序,则可以合并,他们还没有序时,子问题解决
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	/*  当执行到这里时,数组[left,mid] 和 [mid+1 , right] 已经有序,因此下面将是退出递归、合并数组的步骤 */
	// 归并:递归往回退	([left,mid]、[mid+1,right]两个区间已经有序)
	int begin1 = left, end1 = mid;
	int begin2 = mid+1, end2 = right;
	int index = begin1;		// 此处注意,tmp起始位置在 left
	while (begin1 <= end1 && begin2 <= end2)
	{
		// 在两个数组中,依次找最小的数存入临时数组tmp
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}
	// 一组数组归并完,将另一组数组剩下的全部归并到后面,结束的那一组将不会进入while循环
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];

	while (begin2 <= end2)
		tmp[index++] = a[begin2++];

	// 把归并好的在tmp的数据,再拷贝回到原数组
	for (int i = left; i <= right; i++)
		a[i] = tmp[i];

}

以上,需要注意的是:
1、当待排序的数组还未有序时,统一归纳为子问题,继续递归下去。直到待排序数组有序时(数组只有一个元素)才开始递归返回,接着执行数组的合并。
2、需要将待合并的两个数组,挨个选取两个数组中最小(升序)/最大(降序)的数放入临时数组tmp中。同时需要注意,临时数组tmp的下标问题。
3、将两个待合并的数组,有序的合并到临时数组tmp,返回上一级递归前,需要将临时数组中合并好的、排好序数组,拷贝回原数组。


以上便是对于归并算法的大体流程,下面是对于该算法的步骤大体总结。



算法步骤

1、分割数组: 将待排序的数组划分为两个相等(或近似相等)大小的子数组。这一步采用分治策略,递归地对子数组进行分割,直到每个子数组包含一个元素。

2、递归排序: 对分割后的子数组进行递归排序。这是通过再次调用归并排序来实现的。

3、合并操作: 将已排序的子数组合并成一个有序数组。合并操作是归并排序的关键步骤,它涉及比较已排序的子数组的元素,并按顺序将它们合并到一个新的数组中。


结合以上的全部学习,让我们给出完整的代码,进行学习上的整合。



代码实现

代码1


// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = (left + right) / 2;
	// 分割为两个区间[left,mid]   [mid+1,right]
	//[left,mid] [mid+1,right] 有序,则可以合并,他们还没有序时,子问题解决
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	/*  分解  +   合并  */

	// 归并:递归往回退	([left,mid]、[mid+1,right]两个区间已经有序)
	int begin1 = left, end1 = mid;
	int begin2 = mid+1, end2 = right;
	int index = begin1;		// 此处注意,tmp起始位置在 left
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}
	// 一组数组归并完,将另一组数组剩下的全部归并到后面,结束的那一组将不会进入while循环
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];

	while (begin2 <= end2)
		tmp[index++] = a[begin2++];

	// 把归并好的在tmp的数据,再拷贝回到原数组
	for (int i = left; i <= right; i++)
		a[i] = tmp[i];


}

// 归并排序 - 递归实现
void MergeSort(int* a, int n)
{
	assert(a);

	int* tmp = (int*)malloc(sizeof(int) * n);

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);

}

以上便是对于归并算法的具体代码实现。其中,为了更好的函数封装性。我们可以将具体的合并过程,封装成一个合并函数,使代码可读性更强。如下:




代码优化

//  合并处理函数
void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int* tmp)
{
	int left = begin1, right = end2;
	int index = begin1;		// 此处注意,tmp起始位置在 left
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[index++] = a[begin1++];
		else
			tmp[index++] = a[begin2++];
	}
	// 一组数组归并完,将另一组数组剩下的全部归并到后面,结束的那一组将不会进入while循环
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];

	while (begin2 <= end2)
		tmp[index++] = a[begin2++];

	// 把归并好的在tmp的数据,再拷贝回到原数组
	for (int i = left; i <= right; i++)
		a[i] = tmp[i];
}

// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = (left + right) / 2;
	// 分割为两个区间[left,mid]   [mid+1,right]
	//[left,mid] [mid+1,right] 有序,则可以合并,他们还没有序时,子问题解决
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	/*  分解  +   合并  */
	// 归并:递归往回退	([left,mid]、[mid+1,right]两个区间已经有序)
	MergeArr(a, left, mid, mid + 1, right, tmp);

}

// 归并排序 - 递归实现
void MergeSort(int* a, int n)
{
	assert(a);

	int* tmp = (int*)malloc(sizeof(int) * n);

	_MergeSort(a, 0, n - 1, tmp);


	free(tmp);

}

以上便是封装性更佳的归并算法。



时间复杂度

O(N*logN)

归并排序有点类似于二叉树中的后序遍历。先将数组平分、平分,直到最后不能再分时,再合并返回。
因为递归的高度为logN,而合并的过过程,每一层可以归纳统计认为是N。
因此归并排序的时间复杂度为:O(N*logN)。



空间复杂度

O(N)
该算法需要用到额外开辟的数组。数组大小为待排序数组的大小。故空间复杂度为O(N)。
(N为待排序数组的个数)




特性总结

1、 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问
题。
2、时间复杂度:O(N*logN)
3、空间复杂度:O(N)
4、稳定性:稳定

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

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

相关文章

响应式布局 Bootstrap

响应式开发 原理&#xff1a;就是根据媒体查询针对不同宽度的设备进行布局和样式的设置&#xff0c;从而适配不同设备的目的。 响应式需要一个父级做为布局容器&#xff0c;来配合子级元素来实现变化效果。 原理&#xff1a;就是在不同屏幕下&#xff0c;通过媒体查询来改变这…

基于Python+Django,开发一款房屋租赁系统

学习文档 学习过程中&#xff0c;遇到问题可以咨询作者 功能介绍 平台采用B/S结构&#xff0c;后端采用主流的PythonDjango进行开发&#xff0c;前端采用主流的Vue.js进行开发。 整个平台包括前台和后台两个部分。 前台功能包括&#xff1a;首页、房屋详情页、用户中心模块。…

[java]JAVA中文版API手册 -jdk_api_1.8

有mac和win版本 链接&#xff1a;https://pan.baidu.com/s/14WGXJYBICeSxgg6OxBVGRQ 提取码&#xff1a;c03p

【总线接口】1.以Xilinx开发板为例,直观的认识硬件板卡和接口

初接触硬件&#xff0c;五花八门的总线、接口一定会让你有些疑惑&#xff0c;我尝试用一系列文章来解开你的疑惑 系列文章 【总线接口】1.以Xilinx开发板为例&#xff0c;直观的认识硬件接口 【总线接口】2.学习硬件这些年接触过的硬件接口、总线 大汇总 【总线接口】…

C# Unity将地形(Terrain)导出成obj文件

C# Unity将地形(Terrain)导出成obj文件 从其他地方搬运过来的&#xff0c;只能到出obj模型&#xff0c;不能导出贴图 using System.IO; using System.Text; using UnityEditor; using UnityEngine; using System;enum SaveFormat { Triangles, Quads } enum SaveResolution {…

《计算机科学中的建模技术》复习点

0 考试题型 题型&#xff1a;选择、填空、大题&#xff08;综合题&#xff09; 分值&#xff1a;选择填空30分&#xff0c;综合70分 填空&#xff1a;基本概念题 第 1 章&#xff1a;计算机科学基本问题与数学建模概要 1.1 科学计算的基本概念 科学计算是指利用计算机来完成…

Java二分查找冒泡排序插入排序

二分查找 又叫折半查找&#xff0c;要求待查找的序列有序。每次取中间位置的值与待查关键字比较&#xff0c;如果中间位置的值比待查关键字大&#xff0c;则在前半部分循环这个查找的过程&#xff0c;如果中间位置的值比待查关键字小&#xff0c;则在后半部分循环这个查找的过程…

【占用网络】VoxFormer 基于视觉的3D语义场景方案 CVPR 2023

前言 本文分享“占用网络”方案中&#xff0c;来自CVPR2023的VoxFormer&#xff0c;它基于视觉实现3D语义场景补全。 使用Deformable Attention从图像数据中&#xff0c;预测三维空间中的体素占用情况和类别信息。 VoxFromer是一个两阶段的框架&#xff1a; 第一个阶段&…

TypeScript 从入门到进阶之基础篇(四) symbol类型篇

系列文章目录 TypeScript 从入门到进阶系列 TypeScript 从入门到进阶之基础篇(一) ts基础类型篇TypeScript 从入门到进阶之基础篇(二) ts进阶类型篇TypeScript 从入门到进阶之基础篇(三) 元组类型篇TypeScript 从入门到进阶之基础篇(四) symbol类型篇 持续更新中… 文章目录 …

mysql之视图执行计划

一.视图 1.1视图简介 1.2 创建视图 1.3视图的修改 1.4视图的删除 1.5查看视图 二.连接查询案例 三.思维导图 一.视图 1.1视图简介 虚拟表&#xff0c;和普通表一样使用 MySQL中的视图&#xff08;View&#xff09;是一个虚拟表&#xff0c;其内容由查询定义。与实际表不…

解决 POST http://x.x.x.x:8000/aaa/ net::ERR_CONNECTION_TIMED_OUT

记录一下我遇到的问题和解决办法 我的项目前后端分离&#xff0c;在前端用的vue访问后端时连接不上后端&#xff0c;一切操作都触发不了后端&#xff0c;数据也传不到后端去&#xff1b; 原因&#xff1a;url有问题&#xff0c;url地址写的不是本机&#xff0c;所以导致连接超…

OpenCV的安装和vscode的配置

在图像处理领域&#xff0c;OpenCV的使用是必不可少的&#xff0c;这里介绍一下OpenCV的安装及其在vscode中的配置 1.OpenCV的安装 &#xff08;1&#xff09;安装依赖 sudo apt-get install build-essentialsudo apt-get install cmake git libgtk2.0-dev pkg-config libavc…

vue2中vuex详细使用

1.安装 说明&#xff1a;也就是版本号&#xff0c;一般vue2安装vuex3。 npm i vuex3.6.2 2.搭建架子 执行流程如下&#xff1a; 初始化状态&#xff1a;在state对象中定义了一个名为message的属性&#xff0c;并将其初始值设置为"启动"。 定义变更函数&#xff08…

leetcode 每日一题 2023年12月30日 一周中的第几天

题目 给你一个日期&#xff0c;请你设计一个算法来判断它是对应一周中的哪一天。 输入为三个整数&#xff1a;day、month 和 year&#xff0c;分别表示日、月、年。 您返回的结果必须是这几个值中的一个 {"Sunday", "Monday", "Tuesday", &qu…

pytorch集智-2单车预测器

完整代码在个人主页简介链接pytorch路径下可找到 1 单车预测器1.0 1.1 人工神经元 对于sigmoid函数来说&#xff0c;w控制函数曲线的方向&#xff0c;b控制曲线水平方向位移&#xff0c;w控制曲线在y方向的幅度 1.2 多个人工神经元 模型如下 数学上可证&#xff0c;有限神经…

[大厂实践] 无停机迁移大规模关键流量(下)

在系统升级、迁移的过程中&#xff0c;如何验证系统逻辑、性能正确无误&#xff0c;是一个很大的挑战。这一系列介绍了Netflix通过重放流量测试解决这一挑战的实践。原文: Migrating Critical Traffic At Scale with No Downtime — Part 2 想象一下&#xff0c;你被心爱的Netf…

【操作系统xv6】学习记录5--实验1 Lab: Xv6 and Unix utilities

ref:https://pdos.csail.mit.edu/6.828/2020/xv6.html 实验&#xff1a;Lab: Xv6 and Unix utilities 环境搭建 实验环境搭建&#xff1a;https://blog.csdn.net/qq_45512097/article/details/126741793 搭建了1天&#xff0c;大家自求多福吧&#xff0c;哎。~搞环境真是折磨…

浅谈 JVM 类加载过程

&#x1f697;&#x1f697;&#x1f697;今天给大家分享的是HTTPS加密的工作过程。 清风的CSDN博客 &#x1f6e9;️&#x1f6e9;️&#x1f6e9;️希望我的文章能对你有所帮助&#xff0c;有不足的地方还请各位看官多多指教&#xff0c;大家一起学习交流&#xff01; ✈️✈…

SQL Server从0到1——写shell

xp_cmdshell 查看能否使用xpcmd_shell&#xff1b; select count(*) from master.dbo.sysobjects where xtype x and name xp_cmdshell 直接使用xpcmd_shell执行命令&#xff1a; EXEC master.dbo.xp_cmdshell whoami 发现居然无法使用 查看是否存在xp_cmdshell: EXEC…