一篇文章讲透排序算法之归并排序

0.前言

本篇文章将详细解释归并排序的原理,以及递归和非递归的代码原理。

一.概念

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

二.具体思想

根据上述所说,我们应当首先将序列不断分为子序列,也就是说将下面的数组分为左右两个部分,然后想办法让左数组有序,右数组有序,之后将这两个数组合并,即可让数组有序。而原数组的左右两部分又可以不断的分割为新的左右数组,那么我们就可以不断的将这个数组分割,直到左右数组内只有一个元素停止。如下图所示:

由于现在的左右数组都只有一个元素,那它们各自自然都是有序的,我们就可以将其合并,得到一个有序数组。并不断的进行这个操作,我们即可让原数组有序。以原数组的左子数组为例,我们可做出这些行为:

三.做法

现在我们已经理解了他的原理,那么我们应该怎么做呢?

方法1:递归

我们发现,它每次都是将序列分为左右两部分,然后每次都是分为两部分继续遍历,分到不可分割后,我们便不再进行分割,而是进行归并。

而我们二叉树的遍历也是一直分左右子树;而二叉树的后序遍历中则是先将左右都遍历完再进行打印的,我们会发现它们极其类似。

既然如此,我们就可以使用处理二叉树的后序遍历的递归思路来处理这个问题。

根据上述思路,我们可以写出如下代码:

void MergeSort(int* a, int begin,int end)
{   //递归的终止条件if(begin>=end){return;}//分割int mid = (begin + end) / 2;MergeSort(a, begin, mid);MergeSort(a, mid+1, end);//合并//......
}

那么,我们下面的工作就是进行合并了。

我们发现,在合并的过程中,在原数组上操作会出现问题,因此我们需要开辟一块空间来保存合并后的数组,因此我们刚刚的函数体的参数列表则不可满足我们的需求。因此我们还应传入一个地址指向一块我们开辟的空间。

void MergeSort(int* a, int begin, int end, int* temp)
{//递归的终止条件if (begin >= end){return;}//分割int mid = (begin + end) / 2;MergeSort(a, begin, mid, temp);MergeSort(a, mid+1,end, temp);//合并
}

我们写出的函数是想要拿来就可以直接用的,而不是还需要做一些准备工作才能用。

我们并不想每次开空间时都要先开辟一块空间,这要怎么办呢?

我们可以通过函数的回调来解决这个问题。 

void _MergeSort(int* a, int begin, int end, int* temp)
{//递归的终止条件if (begin >= end){return;}//分割int mid = (begin + end) / 2;_MergeSort(a, begin, mid, temp);_MergeSort(a, mid+1,end, temp);//合并//......
}
void MergeSort(int* a, int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (!temp){perror("malloc fail!");}_MergeSort(a,0,n,temp);
}

现在我们就可以做到直接使用了,也不需要传递那么多乱七八糟的参数,只需要将数组的地址和长度传入即可。

那么现在我们来完成我们的归并过程。

我们拿上例中倒数第二趟的归并为例进行分析:

ps:此时两个小数组都已经有序

这里我们可以定义两个指针,分别指向两个小数组的第一个元素,然后我们比较指针指向的值,谁小就放到原数组的begin位置,之后再将其指针加1,之后再比较指针的值,直到一方遍历结束,再将另一方的元素拷贝进temp数组即可。过程如下:

过程1:

过程2:

我们下面重复这个操作即可完成对数组的排序。 

那么现在我们来完成代码部分:

void _MergeSort(int* a, int begin, int end, int* temp)
{//递归的终止条件if (begin >= end){return;}//分割int mid = (begin + end) / 2;_MergeSort(a, begin, mid, temp);_MergeSort(a, mid+1,end, temp);//合并int begin1 = begin, end1 = mid;//左区间int begin2 = mid + 1, end2 = end;//右区间int i = begin;//用来给temp赋值while (begin1<=end1&&begin2<=end2)//任何一个越界则结束{if (a[begin1] < a[begin2]){temp[i++] = a[begin1++];}if (a[begin2] < a[begin1]){temp[i++] = a[begin2++];}}//一个不越界,另外一个不越界// 下面的while循环必然会进入而且只会进入一个while (begin1 <= end1){temp[i++] = a[begin1++];}while (begin2 <= end2){temp[i++] = a[begin2++];}//每次归并完成都要将数据拷贝一份回去memcpy(a+begin,temp+begin,sizeof(int)*(end-begin+1))
}
void MergeSort(int* a, int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (!temp){perror("malloc fail!");}_MergeSort(a,0,n.temp);
}

方法2:非递归

我们现在再来分析一下这一问题。

我们发现,我们会先两个两个分成一组进行排序。

然后再四个四个分成一组进行排序。

那么我们是否可以通过控制区间来完成这个排序呢?

首先,我们应我们确立一个gap,每次排序完一趟之后都将gap*2,直到gap超过数组的长度。

void MergeSort(int* a, int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");exit(1);}int gap = 1;while (gap < n){//归并代码//......gap *= 2;}
}

 之后,我们便可以控制单趟的排序了。

我们每趟都是将原数组分为很多对小数组进行比较的,每次比较完毕之后需要跳过一对小数组,直到比较到数组尾为止。

void MergeSort(int* a, int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");exit(1);}int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){//比较逻辑//......}gap *= 2;}
}

 比较的逻辑和我们递归中的是一样的。

需要我们注意的仅仅只是每一轮的开始处都等于i。

void MergeSortNonR(int* a, int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");exit(1);}//开辟成功int gap = 1;while (gap < n){int j = 0;for (int i = 0; i < n; i += 2 * gap){   //注意,每一轮的i都相当于递归方法中的beginint begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2])temp[j++] = a[begin1++];elsetemp[j++] = a[begin2++];}while (begin1 <= end1)temp[j++] = a[begin1++];while (begin2 <= end2)temp[j++] = a[begin2++];}memcpy(a, temp, sizeof(int) * n);gap *= 2;}
}

现在我们基本完成了归并排序,最起码,长度为8的数组是能够完成的。

但是还有一个问题需要大家思考:

如果我们的数组长度是10的话,会出现什么样的情况呢?

如下所示:

这是为什么呢?

我们打印一下我们每次比较的区间来看一下

我们发现,出现了越界访问的情况。

通过上图分析,我们可以得到它们越界的情况(begin1不可能越界,因为被外循环限制

分别如下: 

  • end2越界
  • begin2,end2越界
  • end1,begin2,end2越界。

那么我们只要控制好这个边界,即可让这个程序很健壮。

那么我们如何控制这个边界情况呢?

 控制方法1:

当end1或begin2越界时,直接跳出循环,不进行归并,未归并的部分就会留存在原数组当中。

如果等到归并结束了之后再整体拷贝temp回原数组,就会覆盖掉没有归并的区域。

为了避免直接将temp中全部元素拷贝回原数组,保持原数组中未被归并部分的数据,

我们要每次归并都拷贝一次数据,而且只拷贝归并的部分。

如果将temp整体拷贝过去的话,上例中下标7后面的数据将会是乱码。

因此我们可以得到如下代码:

void MergeSortNonR(int* a, int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");exit(1);}int gap = 1;while (gap < n){int j = 0;for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;//更改部分if (end1 > n || begin2 > n){break;}if (end2 > n){end = n - 1;}while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2])temp[j++] = a[begin1++];elsetemp[j++] = a[begin2++];}while (begin1 <= end1)temp[j++] = a[begin1++];while (begin2 <= end2)temp[j++] = a[begin2++];memcpy(a + i, temp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}
}
控制方法2

当end1越界时,我们直接将end1设置为数组最后一个元素为止,让begin2和end2为一个不存在的区间。

当begin2越界时,让begin2和end2设置为一个不存在的区间。

当end2越界时,将end2设置为数组最后一个元素。 

void MergeSortNonR(int* a, int n)
{int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");exit(1);}//开辟成功int gap = 1;while (gap < n){int j = 0;for (int i = 0; i < n; i += 2 * gap){   //注意,每一轮的i都相当于递归方法中的beginint begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;if (end1 >= n){end1 = n - 1;//[2,1]的区间就寄掉了。begin2 = 2;end2 = 1;}else if (begin2 >= n){begin2 = n;end2 = n - 1;}else if (end2 >= n){end2 = n - 1;}while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2])temp[j++] = a[begin1++];elsetemp[j++] = a[begin2++];}while (begin1 <= end1)temp[j++] = a[begin1++];while (begin2 <= end2)temp[j++] = a[begin2++];}memcpy(a, temp, sizeof(int) * n);//一趟归并一次gap *= 2;}
}

这样有个优点在于,每次原数组中的元素被会归并到每个数据都可以在归并后拷贝到temp数组中,因此我们就可以不像控制方法1那样归并一次拷贝一次了,我们归并一趟之后再拷贝即可。

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

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

相关文章

虚拟机Ubuntu 22.04上搭建GitLab操作步骤

GitLab是仓库管理系统&#xff0c;使用Git作为代码管理工具。GitLab提供了多个版本&#xff0c;包括社区版(Community Edition)和企业版(Enterprise Edition)。实际应用场景中要求CPU最小4核、内存最小8GB&#xff0c;非虚拟环境。 以下是在虚拟机中安装社区版步骤&#xff1a;…

git获取的项目无法运行

一、Unsupported engine 问题&#xff1a;在使用命令npm install下载依赖项的时候就遇到了这个问题&#xff0c;有帖子说多试几次&#xff0c;其实这是提示node版本问题&#xff0c;版本的更新出现兼容性问题&#xff0c;多试几次也没用。 解决方案&#xff1a; 更新node.js的…

Paper速读-[Visual Prompt Multi-Modal Tracking]-Dlut.edu-CVPR2023

文章目录 简介关于具体的思路问题描述算法细节实验结果模型的潜力模型结果 论文链接&#xff1a;Visual Prompt Multi-Modal Tracking 开源代码&#xff1a;Official implementation of ViPT 简介 这篇文章说了个什么事情呢&#xff0c;来咱们先看简单的介绍图 简单来说&…

【玩转C语言】第一讲--->C语言概念

&#x1f525;博客主页&#x1f525;&#xff1a;【 坊钰_CSDN博客 】 欢迎各位点赞&#x1f44d;评论✍收藏⭐ 目录 引言&#xff1a; 1. C语言是什么 2. C语言的辉煌历史 3. 第一个C语言程序 4. main()函数 5. printf() 函数 6. 库函数 6.1 库函数概念 7. 关键字介绍 …

工具:一键采集 平台:TB+PDD+JD...

什么是数据集&#xff1f; 电商商品数据集通常是指收集自电子商务平台的商品信息的结构化数据集合。这些数据包括但不限于商品名称、价格、描述、用户评价、分类标签、卖家信息、销售量、库存量、图片链接等。数据集可以由电商平台公开提供&#xff0c;也可以通过网络爬虫等技术…

沟通技巧(Communication Skills 业务分析能力)

背景 业务分析的胜任力模型&#xff0c;有六大部分&#xff0c;今天我们看第二部分&#xff0c;业务知识 Analytical Thinking and Problem Solving &#xff1a;分析判断及问题解决能力Behavioural Characteristics&#xff1a; 行为特质&#xff08;责任、道德、适应性等等…

Kubernetes小记

Kubernetes 集群 架构 一个有效的 Kubernetes 部署称为集群&#xff0c;可以将 Kubernetes 集群分为两个部分&#xff1a;控制平面与计算设备&#xff08;或称为节点&#xff09;控制组件 控制平面 K8s 集群的神经中枢,负责处理重要的工作&#xff0c;以确保容器以足够的数量…

GCN 代码解析(一) for pytorch

Graph Convolutional Networks 代码详解 前言一、数据集介绍二、文件整体架构三、GCN代码详解3.1 utils 模块3.2 layers 模块3.3 models 模块3.4 模型的训练代码 总结 前言 在前文中&#xff0c;已经对图卷积神经网络&#xff08;Graph Convolutional Neural Networks, GCN&am…

小程序CI/CD之自动化打包预览并钉钉通知发布进程

小程序打包方式分为两种&#xff1a;手动打包、自动打包 那如何实现 自动打包 呐&#xff1f;我们今天就来聊一聊&#xff01; 首先&#xff0c;很重要&#xff0c;看 官方文档 这里提到今天我们要聊的“主角” miniprogram-ci miniprogram-ci 是从微信开发者工具中抽离的关于…

nacos开启鉴权

nacos版本1.4.3 1.nacos配置开启鉴权 application.properties 或 application.yaml 文件中 nacos.core.auth.enabled 设置为 true 2.修改JWT令牌 如果是Linux系统可以使用命令随机生成 echo -n ThisIsARandomlyGeneratedSecureKey32CharactersLong | base64 nacos.core.au…

【HarmonyOS-Stage应用模型-UIAbility生命周期】

概述 在应用开发过程中&#xff0c;组件的生命周期尤为重要&#xff0c;当用户打开、切换和返回到对应应用时&#xff0c;应用中的UIAbility实例会在其生命周期的不同状态之间转换。我们可以通过生命周期来对应用的状态进行监控并执行特定的操作。比如在创建时进行应用初始化、…

网络摄像头项目

1.OV2640 简介 OV2640 是 OV&#xff08;OmniVision&#xff09;公司生产的一颗 1/4 寸的 CMOS UXGA&#xff08;1632*1232&#xff09;图像 传感器。该传感器体积小、工作电压低&#xff0c;提供单片 UXGA 摄像头和影像处理器的所有功能。通 过 SCCB 总线控制&#xff0c;可以…

【机器学习】LoRA:大语言模型中低秩自适应分析

LoRA&#xff1a;大型语言模型中的低秩自适应调优策略 一、LoRA的原理与优势二、LoRA在编程和数学任务中的性能表现四、总结与展望 随着人工智能技术的飞速发展&#xff0c;大型语言模型已成为自然语言处理领域的明星技术。然而&#xff0c;这些模型通常拥有数以亿计的参数&…

做好产线工控安全的关键

在现代化工业生产中&#xff0c;产线工控安全是确保生产顺利进行、产品质量稳定、员工生命安全的重要一环。随着信息技术的飞速发展&#xff0c;工控系统面临着越来越多的安全威胁&#xff0c;因此&#xff0c;如何做好产线工控安全成为了摆在我们面前的重要课题。 首先&#x…

使用Python进行高并发压测:技术指南与实战案例

目录 一、引言 二、压测基础知识 压测的目的与类型 压测工具的选择 三、使用Python进行压测 Python压测框架的选择 压测脚本的编写 压测的执行与监控 四、实战案例分析 案例背景介绍 压测计划制定 压测实施过程 结果分析与优化建议 五、常见问题与解决方案 六、…

centos7下卸载MySQL,Oracle数据库

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 操作系统版本为CentOS 7 使⽤ MySQ…

JVM 运行流程

JVM 是 Java 运行的基础&#xff0c;也是实现一次编译到处执行的关键&#xff0c;那么 JVM 是如何执行的呢&#xff1f; JVM 执行流程 程序在执行之前先要把java代码转换成字节码&#xff08;class 文件&#xff09;&#xff0c; JVM 首先需要把字节码通过一定的 方式 类加…

文件怎么加密?文件加密软件哪个好用?

在数字化生活和工作中&#xff0c;文件安全已成为不容忽视的话题。 为了保护个人隐私和企业敏感数据不被非法访问或泄露&#xff0c;文件加密成为了不可或缺的手段。 本文将介绍文件加密的基本概念、加密方法以及推荐几款好用的文件加密软件&#xff0c;帮助您为重要文件穿上“…

捋一捋C++中的逻辑运算(一)——表达式逻辑运算

注意&#xff0c;今天要谈的逻辑运算是C语言编程中的“与或非”逻辑运算&#xff0c;不是数学集合中的“交并补”逻辑运算。而编程中的逻辑运算又包括表达式逻辑运算和位逻辑运算&#xff0c;本章介绍表达式逻辑运算&#xff0c;下一章介绍位逻辑运算。 目录 一、几个基本的概…

视频号上怎么卖货?需要直播,还有粉丝吗?一篇文章带你了解!

大家好&#xff0c;我是电商糖果 关于在视频号上卖货&#xff0c;这是大家最常提起的话题。 大家之所以对视频号卖货感兴趣&#xff0c;主要原因还是抖音卖货火起来了。 而视频号是和抖音处于同一个赛道&#xff0c;这两年也在往电商方向发力。 所以大家对视频号推出电商平…