数据结构之 【排序】(归并排序)

目录

1.递归实现归并排序的思想及图解

2.递归实现归并排序的代码逻辑

2.1嵌套子函数

2.2递归过程

2.3递归结束条件

2.4归并及拷贝过程 

3.非递归实现归并排序的思想及图解

4.非递归实现归并排序的代码逻辑

4.1边归并边拷贝

4.2某一gap下归并完成才进行拷贝

5.归并排序的时间复杂度与空间复杂度


升序为例

1.递归实现归并排序的思想及图解

两两数组有序,借助一个临时数组就可将有序两数组合并成一个新的有序数组,这就叫归并

对于给定的待排序数组,我们运用递归策略,不断地将其对半拆分,直到被拆分的子数组只有一个元素时(此时认为子数组有序),我们开始进行归并操作,......最终使数组有序

2.递归实现归并排序的代码逻辑

2.1嵌套子函数

为了避免递归频繁创造临时数组而造成空间消耗,此时需要嵌套子函数

void _MergeSort(int* a, int left, int right, int* tmp);
void MergeSort(int* a, int n);void MergeSort(int* a, int n){int* tmp = (int*)malloc(sizeof(int) * n);if (!tmp){perror("malloc fail");return;}//对[0, n - 1]这段区间进行排序_MergeSort(a, 0, n - 1, tmp);free(tmp);tmp = NULL;
}

归并排序的递归操作类似于二叉树的后序遍历(左子树、右子树、根)

因为只有当原数组的两部分数组都有序后,才能通过归并使整个数组有序

void _MergeSort(int* a, int left, int right, int* tmp)
{//结束条件//递归过程//归并区间//归并到临时数组的对应位置//拷贝}

2.2递归过程

递归过程类似于后序遍历

int midi = left + (right - left) / 2;
//[left, midi][midi + 1, right]
_MergeSort(a, left, midi, tmp);
_MergeSort(a, midi + 1, right, tmp);
//开始归并

此时中间值midi的取法是向下取整,且具有防溢出的特性,使得 left <= midi < right

所以,终止条件 left >= right 合乎情理

标准的左右区间的划分[left, midi][midi + 1, right]

使子区间不重叠,递归范围严格缩小,合并时覆盖了整个区间

当数组只有两个数,下标从0到1并进行递归划分时,midi = 0,

此时左区间[left, midi] 递归可以停止,右区间同理

int mid = left + (right - left + 1) / 2; // 向上取整
//[left, midi - 1][midi, right]mergeSort(arr, left, mid - 1); // 左子区间mergeSort(arr, mid, right);    // 右子区间
//开始归并

向上取整有风险

此时区间需要做调整

当数组只有两个数,下标从0到1并进行递归划分时,midi = 1,

此时左区间如果是[left, midi] 将造成无限递归,所以更改为[left, midi - 1][midi, right]

2.3递归结束条件

根据思想,我们可以知道区间长度等于1时就停止递归,那么

//结束条件
if (left >= right)return;

且向下取整得到 midi 的做法使得 left 不会大于 right

2.4归并及拷贝过程 

升序为例

递归返回后区间所对应的数组就有序了,此时就可以进行归并操作

1.先确定两有序数组的边界   begin.  end.是对应数组的首元素下标、尾元素下标

2.从前往后顺次比较两有序数组,数组元素小的先放到临时数组中,一个数组结束后,再归并另一数组

3.图解

//开始归并
int begin1 = left, end1 = midi;
int begin2 = midi + 1, end2 = right;
//归并到临时数组的对应位置
int i = left;
while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2])tmp[i++] = a[begin1++];else    tmp[i++] = a[begin2++];
}while (begin1 <= end1)tmp[i++] = a[begin1++];while (begin2 <= end2)tmp[i++] = a[begin2++];//拷贝
memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));

归并区间不一定从0开始,而是从 left 开始的

两个begin下标都未到尾才继续比较存值,否则就跳出循环

此时常规思路就是是由if else语句判断到底哪一个begin下标未到尾然后再存值,

但上述做法直接利用while循环进行判断,这种做法更简洁

最后再将临时数组的归并内容拷贝会原数组,原数组对应位置就有序啦

通过递归使得数组最终都有序了

3.非递归实现归并排序的思想及图解

递归实现归并排序总是先递归到小规模数组,再从底层向上开始归并,即先是一个元素与一个元素进行归并,再是两个元素与两个元素进行归并.....这显然也可以是一种双重循环

外层循环是每组元素的个数,内存循环就是数组归并的过程

4.非递归实现归并排序的代码逻辑

4.1边归并边拷贝

(1)归并显然要借助临时数组

void MergeSortNonR1(int* a, int n)
{//临时数组int* tmp = (int*)malloc(sizeof(int) * n);if (!tmp){perror("malloc fail");return;}//归并过程free(tmp);tmp = NULL;
}

(2)特定gap(确定的每组个数)下的归并过程

int gap;
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;//修正边界//开始归并int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2])tmp[j++] = a[begin1++];else    tmp[j++] = a[begin2++];}while (begin1 <= end1)tmp[j++] = a[begin1++];while (begin2 <= end2)tmp[j++] = a[begin2++];//边归并边拷贝memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}

(3) 区间表示

上述图解的gap与数组个数成倍数关系,万一有奇数个元素,是否会越界呢?

(4)修正边界

printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);

打印确定gap下的区间帮助我们观察越界情况

从打印结果来看,end1、begin2、end2都存在越界的可能

而for循环中的i控制了小于n的情况,所以begin1不存在越界访问的说法

边归并边拷贝过程中当end1越界之后,begin2、end2肯定也越界了,此时第一部分没有存入临时数组再拷贝回原数组的必要,而当begin2越界而end1没越界时,第一部分也不需要归并拷贝

	//修正边界if (end1 >= n || begin2 >= n)break;else if (end2 >= n)end2 = n - 1;

只有end2越界时,第二部分的部分需要与第一部分进行归并拷贝,此时将end2修正为 n - 1

再次打印边界,对比发现不合理的区间都已跳过

最后控制gap逐渐增大即可

void MergeSortNonR1(int* a, int n){int* tmp = (int*)malloc(sizeof(int) * n);if (!tmp){perror("malloc fail");return;}//每组中的个数int gap = 1;while (gap < n){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;else if (end2 >= n)end2 = n - 1;//查看边界越界情况需要//printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);//开始归并int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2])tmp[j++] = a[begin1++];else    tmp[j++] = a[begin2++];}while (begin1 <= end1)tmp[j++] = a[begin1++];while (begin2 <= end2)tmp[j++] = a[begin2++];//边归并边拷贝memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}//printf("\n");//查看边界越界情况需要gap *= 2;}free(tmp);tmp = NULL;
}

4.2某一gap下归并完成才进行拷贝

void MergeSortNonR2(int* a, int n){int* tmp = (int*)malloc(sizeof(int) * n);if (!tmp){perror("malloc fail");return;}//每组中的个数int gap = 1;while (gap < n){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;//修正边界//查看越界情况才需要printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);//开始归并int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] <= a[begin2])tmp[j++] = a[begin1++];else	tmp[j++] = a[begin2++];while (begin1 <= end1)tmp[j++] = a[begin1++];while (begin2 <= end2)tmp[j++] = a[begin2++];}//查看越界情况才需要printf("\n");//全部归并完成后一次性拷贝memcpy(a, tmp, sizeof(int) * n);gap *= 2;}free(tmp);tmp = NULL;
}

打印边界

同样只有begin1不会越界,但是因为我们最终是将临时数组全部拷贝到原数组中去,所以

我们要确保不进行归并的部分也存入了临时数组,防止临时数组全部拷贝到原数组中去的过程中对原数据造成覆盖

if (end1 >= n)
{end1 = n - 1;begin2 = n;end2 = n - 1;
}
else if (begin2 >= n)
{begin2 = n;end2 = n - 1;
}
else if (end2 >= n)end2 = n - 1;

end1越界要将end1修改为 n - 1,使得第一部分能够存入临时数组,并且将第二部分的区间改为不存在,即 begin2 > end2 ,这样才能避免第二部分的越界存值

end1未越界,begin2越界,此时将第二部分的区间改为不存在,即 begin2 > end2 ,避免第二部分的越界存值即可

只有end2越界,此时将其修改为 n - 1 即可,这样可以保证一二部分进行归并存值操作

最终区间合理

5.归并排序的时间复杂度与空间复杂度

递归实现归并排序需要递归 logN 层,每层进行遍历归并,即每一层遍历N次

所以时间复杂度为O(N*logN)

int gap = 1;
while (gap < n){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;//........}gap *= 2;
}

非递归实现归并排序时,外层while循环要循环 logN 次,内层for循环中需要遍历数组中的每个元素,所以时间复杂度为O(N*logN)

递归实现归并排序需要递归 logN 层,所以递归栈的空间复杂度为 O(logN)

又预先在最外层创建了临时数组tmp,临时数组的空间复杂度为 O(N)

由于 O(N) 的增长速度远快于 O(logN),因此 O(N) 是主导项

所以   空间复杂度为 O(logN) + O(N) 约等于 O(N)  

非递归实现归并排序创建了一个临时数组tmp,空间复杂度为O(N)

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

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

相关文章

企业如何选择适合的高防服务器?

高防服务器租用哪家好&#xff1f;这个问题困扰着许多站长&#xff0c;建立的网站经常受到各种网络攻击&#xff0c;虽然高防服务器有着较高的防御性能&#xff0c;十分适合经常被攻击的行业网站&#xff0c;但是如何租到满意的高防服务器呢&#xff01;徐州高防服务器是部署在…

告别重复劳动:Ansible 自动化运维超详细学习路线图

在运维的世界里&#xff0c;我们总是在与重复性任务作斗争&#xff1a;部署同一套环境 N 次、在几十台服务器上修改同一个配置文件、一遍又一遍地执行相同的发布流程……这些工作不仅枯燥&#xff0c;还极易出错。 如果你也为此感到烦恼&#xff0c;那么 Ansible 就是为你量身打…

UDS 0x29 身份验证服务 Authentication service

背景 0x29服务的目的是为客户端提供一种证明其身份的方法&#xff0c;在ECU端&#xff0c;有些服务或者数据因信息安全、排放或功能安全原因而受到严格限制。 只有身份验证通过之后&#xff0c;才能够允许其访问数据和/或诊断服务。 例如&#xff0c;用于将数据下载/上传到ECU以…

【python高阶】-1- python工程和线程并发

一、项目工程守则1.pdm新建一个项目命令行终端&#xff1a;pip install pdmpdm init版本号&#xff1a;x.y.zx:兼容版本y:新增功能z:补丁版本pdm add pytest requests (添加依赖)pdm是协助管理我们的项目 2. black就是规范我们的代码风格的&#xff1a;pdm add blackblackblack…

YOLOv8 剪枝模型加载踩坑记:解决 YAML 覆盖剪枝结构的问题

1. 问题背景模型剪枝是实现模型轻量化、加速推理的关键步骤。然而&#xff0c;在 Ultralytics YOLOv8 的生态中&#xff0c;在成功剪枝后&#xff0c;进行微调&#xff08;Fine-tuning&#xff09;时会遇到一个令人困惑的现象&#xff1a;明明加载的是剪枝后的模型&#xff08;…

js的学习1

1.数组 数组方法 push()数组尾部添加unshift()数组头部添加pop()数组尾部删除shift()数组头部删除splice(起始位置&#xff0c;删除几个元素&#xff0c;要替换的元素)删除指定的元素&#xff0c;改变了原数组&#xff0c;返回值是被删除的元素indexOf()第一次查到的索引&#…

LeetCode 2563.统计公平数对的数目

给你一个下标从 0 开始、长度为 n 的整数数组 nums &#xff0c;和两个整数 lower 和 upper &#xff0c;返回 公平数对的数目 。 如果 (i, j) 数对满足以下情况&#xff0c;则认为它是一个 公平数对 &#xff1a; 0 < i < j < n&#xff0c;且 lower < nums[i] n…

ZABBIX配置自动发现与自动注册,网易邮箱告警和钉钉告警

一、自动发现zabbix server 主动的去发现所有的客户端&#xff0c;然后将客户端的信息登记在服务端上。缺点是如果定义的网段中的主机数量多&#xff0c;zabbix server 登记耗时较久&#xff0c;且压力会较大。1、部署准备准备三台虚拟机192.168.80.151&#xff1b;192.168.80.…

QT(五)常用类

1. QString字符串类(掌握) QString是Qt的字符串类&#xff0c;与C的string相比&#xff0c;不再使用ASCII编码&#xff0c;QString使用的是Unicode编码。 QString中每个字符都是一个16位的QChar&#xff0c;而不是8位的char。 QString完全支持中文&#xff0c;但是由于不同的技…

EXCEL怎么提取表名

错误的方法&#xff1a;使用以下方法提取表名的时候&#xff0c;会存在1个问题&#xff0c;公式只在当前工作表生效&#xff0c;换工作表会出现表名覆盖的情况。RIGHT(CELL("filename"),LEN(CELL("filename"))-FIND("]",CELL("filename&quo…

springboot校园外卖配送系统

目 录 第一章 绪 论 1.1背景及意义 1.2国内外研究概况 1.3 研究的内容 第二章 关键技术的研究 2.1开发技术 2.2 Springboot框架介绍 2.3 Vue.js 主要功能 2.4 MVVM模式介绍 2.4 B/S体系工作原理 2.5 MySQL数据库 第三章 系统分析 3.1 系统设计目标 3.2 系统可行性…

【智慧物联网平台】安装部署教程——仙盟创梦IDE

一、部署前准备1. 环境要求基础环境&#xff1a;JDK 1.8、MySQL 5.7/8.0、Maven 3.6、Redis&#xff08;用于缓存&#xff09;、Node.js&#xff08;用于前端构建&#xff0c;可选&#xff09;。依赖服务&#xff1a;若需对接门禁、道闸等硬件设备&#xff0c;需确保设备网络可…

【安全漏洞】防范未然:如何有效关闭不必要的HTTP请求方法,保护你的Web应用

在构建和维护Web应用的过程中&#xff0c;安全问题总是我们最关心的话题之一。今天&#xff0c;我们要探讨的是一个经常被忽视的Web漏洞——未关闭或限制不必要的HTTP请求方法。 虽然我们在日常开发中主要使用 GET 和 POST 这两种请求方法&#xff0c;但像 PUT、DELETE、HEAD、…

嵌入式Linux裸机开发笔记8(IMX6ULL)主频和时钟配置实验(1)

引言在前几章实验中我们都没有涉及到 I.MX6U 的时钟和主频配置操作&#xff0c;全部使用的默认配置&#xff0c; 默认配置下 I.MX6U 工作频率为 396MHz。但是 I.MX6U 系列标准的工作频率为 528MHz&#xff0c;有些 型号甚至可以工作到 696MHz。本章学习 I.MX6U 的时钟系统&…

设计模式(四)创建型:生成器模式详解

设计模式&#xff08;四&#xff09;创建型&#xff1a;生成器模式详解生成器模式&#xff08;Builder Pattern&#xff09;是 GoF 23 种设计模式中的核心创建型模式之一&#xff0c;其核心价值在于将一个复杂对象的构建过程与其表示分离&#xff0c;使得同样的构建过程可以创建…

《Angular+Spring Boot:ERP前端采购销售库存协同架构解析》

基于Angular与Spring Boot构建的全栈ERP前端&#xff0c;绝非技术的简单叠加&#xff0c;而是通过深度融合两者特性&#xff0c;打造出兼具稳定性与灵活性的业务载体。Angular的组件化架构将复杂界面拆解为可复用的独立单元&#xff0c;依赖注入机制则让服务调用与数据流转条理…

Java 排序

文章目录排序插入排序分析希尔排序分析选择排序分析堆排序分析冒泡排序分析快速排序霍尔法分析挖坑法找基准前后指针法题目快排的优化三数取中法非递归实现快排归并排序分析非递归实现归并排序海量数据的排序非比较的排序计数排序分析基数排序桶排序排序 稳定的排序&#xff1…

日本IT就职面试|仪容礼仪篇分享建议

日系企業で好印象を与える「身だしなみ」と「面接マナー」ガイドこんにちは。 日系企業への就職・転職活動をされている方にとって、「第一印象」は合否を左右する大切なポイントですよね。実は、面接の評価は入室の瞬間から始まっていると言っても過言ではありません。 今回は…

英语听力口语词汇-8.美食类

1.crispy,crisp adj.酥脆的&#xff0c;易碎的 2.sweet adj.甜的 比如说chocolate is so sweet and delicious 3.chewy adj.难嚼的&#xff0c;难咽的 4.oatmeal n.燕麦粉 5.pickle n.泡菜 7.stir-fry v.炒菜 8.bacon n.咸肉&#xff0c;熏肉 9.yummy adj.美味可口的 1…

力扣7:整数反转

力扣7:整数反转题目思路代码题目 给你一个 32 位的有符号整数 x &#xff0c;返回将 x 中的数字部分反转后的结果。 如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^31 − 1] &#xff0c;就返回 0。 思路 这道题我们可以分成两部分来做&#xff0c;一是完成反转二…