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

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.rhkb.cn/news/340626.html

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

相关文章

苹果或面临退一赔三,新iPad悄悄砍了核心规格

618 快过去了一半&#xff0c;各家都卖的如火如荼&#xff0c;这其中当属苹果搞得最热火朝天。 某东手机竞速榜中&#xff0c;iPhone15 Pro Max 销量稳坐头把交椅&#xff0c;平板方面虽然没有统计表&#xff0c;但是相信销量也是不差。 加上今年刚刚发布的新系列的 iPad&…

求助!什么软件可以人声分离?手机上可以进行人声分离操作吗?

在数字时代&#xff0c;音频处理变得越来越重要&#xff0c;而人声分离技术则是其中的一项关键技术。很多人可能都有过这样的疑问&#xff1a;什么软件可以实现人声分离&#xff1f;手机上能否进行人声分离操作&#xff1f;今天&#xff0c;我们就来为大家解答这些问题&#xf…

提取伴奏与人声分离软件:5款手机必备音频软件

在数字音乐的浪潮中&#xff0c;音频处理软件已经成为手机用户不可或缺的工具。特别是在音乐制作、卡拉OK伴奏制作以及日常音频编辑中&#xff0c;人声与伴奏的分离显得尤为重要。本文将为您介绍五款免费且实用的手机音频软件&#xff0c;它们都具有人声与伴奏分离的功能&#…

spring boot 3.x版本 引入 swagger2启动时报错

一&#xff0c;问题 Spring Boot 3.x版本的项目里&#xff0c;准备引入Swagger2作为接口文档&#xff0c;但是项目启动报错&#xff1a; java.lang.TypeNotPresentException: Type javax.servlet.http.HttpServletRequest not present at java.base/sun.reflect.generics.…

安装windows x64的开源录屏软件GifCapture

下载压缩包 GIF软件 安装报错 下载.NET桌面版运行 .NET 即可在最近安装部分找到GifCapture打开使用

容器项目之前后端分离

容器化部署ruoyi项目 #需要的镜像nginx、java、mysql、redis、 #导入maven镜像、Java镜像和node镜像 docker load -i java-8u111-jdk.tar docker load -i maven-3.8.8-sapmachine-11.tar docker load -i node-18.20.3-alpine3.20.tar #拉取MySQL和nginx镜像 docker pull mysql…

【JavaScript】ECMAS6(ES6)新特性概览(二):解构赋值、扩展与收集、class类全面解析

🔥 个人主页:空白诗 🔥 热门专栏:【JavaScript】 文章目录 🌿 引言五、 Destructuring Assignment - 解构赋值,数据提取的艺术 🎨📌 数组解构📌 对象解构&

文件夹突变解析:类型变文件的数据恢复与预防

在数字化时代&#xff0c;文件夹作为我们存储和组织数据的基本单元&#xff0c;其重要性不言而喻。然而&#xff0c;有时我们可能会遇到一种令人困惑的情况——文件夹的类型突然变为文件&#xff0c;导致无法正常访问其中的内容。这种现象不仅会影响我们的工作效率&#xff0c;…

Solon2分布式事件总线的应用价值探讨

随着现代软件系统的复杂性日益增加&#xff0c;微服务架构逐渐成为开发大型应用的主流选择。在这种架构下&#xff0c;服务之间的通信和协同变得至关重要。Solon2作为一个高性能的Java微服务框架&#xff0c;其分布式事件总线&#xff08;Distributed Event Bus&#xff09;为微…

FastAPI给docs/配置自有域名的静态资源swagger-ui

如果只是要解决docs页面空白的问题&#xff0c;可先看我的这篇博客&#xff1a;FastAPI访问/docs接口文档显示空白、js/css无法加载_fastapi docs打不开-CSDN博客 以下内容适用于需要以自用域名访问swagger-ui的情况&#xff1a; 1. 准备好swagger-ui的链接&#xff0c;如&am…

读后感:《SQL数据分析实战》运营SQL实用手册

学习SQL&#xff0c;先有用起来&#xff0c;有了使用价值&#xff0c;之后才是去了解它的原理&#xff0c;让使用更加顺畅。 在大部分业务场景中&#xff0c;通过SQL可以快速的实现数据处理与统计。《SQL数据分析实战》区别于其他工具书&#xff0c;它并没有介绍SQL是什么&…

为何限定项目的 Node.js 版本

首先区分三个概念nvm&#xff0c;npm&#xff0c;nodejs。 Node.js: Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境。它允许开发者使用 JavaScript 在服务器端编写应用程序,而不仅限于在浏览器中运行 JavaScript。Node.js 提供了一系列内置的模块和 API,使得开发…

Redis之常用实战场景

1.Redis数据丢失场景 1.1 持久化丢失 采用RDB或者不持久化&#xff0c;就会有数据丢失&#xff0c;因为是手动或者配置以快照的形式来进行备份。 解决: 启用AOF&#xff0c;以命令追加的形式进行备份&#xff0c;但是默认也会有1s丢失&#xff0c;这是在性能与数据安全性中寻…

2021 hnust 湖科大 数据结构课设报告+代码

2021 hnust 湖科大 数据结构 课设报告代码 描述 hnust大一下学期数据结构课设的报告和源代码&#xff08;放在了附录里面&#xff09; 目录 项目名称完成日期页码复杂度分析(Ⅰ)2021-06-211—2复杂度分析(Ⅱ)2021-06-213—4Josephus问题(Ⅰ)2021-06-215—6Josephus问题(Ⅱ…

做抖音小店掌握这些方法,轻松和达人合作!

大家好&#xff0c;我是喷火龙。 抖店的新手商家想要出单&#xff0c;最快速稳定的出单方法无疑是与达人合作&#xff0c;通过达人的自身流量来帮助我们进行产品的转化。 接下来我就教大家怎么和达人合作。 首先我们可以通过蝉妈妈、达人广场、抖音私信等方式来找到我们想要…

Linux之线程互斥

线程简单封装 试着用线程控制力介绍的一些系统调用, 将线程的创建、执行和等待等都封装起来. 我们在程序中指定一个函数Print, 让多个线程不断地执行该函数. myThread.hpp #pragma once #include <functional> #include <pthread.h> #include <string>//假…

centos7下卸载MySQL,Oracle数据库

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

HiveMetastore

HiveMetastore 背后的存储 select * from DBS; select * from TBLS; select * from TABLE_PARAMS; 查找出没有 totalSize stats 的table SELECT DBS.NAME,t.TBL_NAME from DBS inner join (select DB_ID,TBL_NAME from TBLS where TBLS.TBL_ID not in(select TBL_ID from T…

JavaEE:Servlet创建和使用及生命周期介绍

目录 ▐ Servlet概述 ▐ Servlet的创建和使用 ▐ Servlet中方法介绍 ▐ Servlet的生命周期 ▐ Servlet概述 • Servlet是Server Applet的简称&#xff0c;意思是 用Java编写的服务器端的程序&#xff0c;Servlet被部署在服务器中&#xff0c;而服务器负责管理并调用Servle…

Leetcode:最长公共前缀

题目链接&#xff1a;14. 最长公共前缀 - 力扣&#xff08;LeetCode&#xff09; 普通版本&#xff08;横向扫描&#xff09; 主旨&#xff1a;用第一个字符串与后续的每个字符串进行比较&#xff0c;先获取S1和S2的最长公共前缀&#xff0c;然后将该次比较获得的最长公共前缀…