【数据结构与算法】:非递归实现快速排序、归并排序
🔥个人主页: Quitecoder
🔥专栏:数据结构与算法
上篇文章我们详细讲解了递归版本的快速排序,本篇我们来探究非递归实现快速排序和归并排序
目录
- 1.非递归实现快速排序
- 1.1 提取单趟排序
- 1.2 用栈实现的具体思路
- 1.3 代码实现
- 2.归并排序
1.非递归实现快速排序
快速排序的非递归实现主要依赖于栈(stack)来模拟递归过程中的函数调用栈。递归版本的快速排序通过递归调用自身来处理子数组,而非递归版本则通过手动管理一个栈来跟踪接下来需要排序的子数组的边界
那么怎样通过栈来实现排序的过程呢?
思路如下:
使用栈实现快速排序是对递归版本的模拟。在递归的快速排序中,函数调用栈隐式地保存了每次递归调用的状态。但是在非递归的实现中,你需要显式地使用一个辅助栈来保存子数组的边界
以下是具体步骤和栈的操作过程:
-
初始化辅助栈:
创建一个空栈。栈用于保存每个待排序子数组的起始索引(begin)和结束索引(end)。 -
开始排序:
将整个数组的起始和结束索引作为一对入栈。这对应于最初的排序问题。 -
迭代处理:
在栈非空时,重复下面的步骤:- 弹出一对索引(即栈顶元素)来指定当前要处理的子数组。
- 选择子数组的一个元素作为枢轴(pivot)进行分区(可以是第一个元素,也可以通过其他方法选择,下面我们还是用三数取中)。
- 进行分区操作,这会将子数组划分为比枢轴小的左侧部分和比枢轴大的右侧部分,同时确定枢轴元素的最终位置。
-
处理子数组:
分区操作完成后,如果枢轴元素左侧的子数组(如果存在)有超过一个元素,则将其起始和结束索引作为一对入栈。同样,如果右侧的子数组(如果存在)也有超过一个元素,也将其索引入栈 -
循环:
继续迭代该过程,直到栈为空,此时所有的子数组都已经被正确排序。
所以主要思路就两个:
- 分区
- 单趟排序
1.1 提取单趟排序
我们上篇文章讲到递归排序的多种方法,这里我们可以取其中的一种提取出单趟排序:
int Getmidi(int* a, int begin, int end)
{int midi = (begin + end) / 2;if (a[begin] < a[midi]){if (a[midi] < a[end])return midi;else if (a[begin] > a[end])return begin;elsereturn end;}else{if (a[midi] > a[end])return midi;else if (a[end] < a[begin])return end;elsereturn begin;}
}
void QuickSortHole(int* arr, int begin, int end) {if (begin >= end) {return;}int midi = Getmidi(arr, begin, end);Swap(&arr[midi], &arr[begin]);int key = arr[begin]; int left = begin;int right = end;while (left < right) {while (left < right && arr[right] >= key) {right--;}arr[left] = arr[right];while (left < right && arr[left] <= key) {left++;}arr[right] = arr[left];}arr[left] = key; QuickSortHole(arr, begin, left - 1);QuickSortHole(arr, left + 1, end);
}
接下来完成单趟排序函数:
int singlePassQuickSort(int* arr, int begin, int end)
{if (begin >= end) {return;}// 选择枢轴元素int midi = Getmidi(arr, begin, end);Swap(&arr[midi], &arr[begin]);int key = arr[begin]; // 挖第一个坑int left = begin; // 初始化左指针int right = end; // 初始化右指针// 进行分区操作while (left < right) {// 从右向左找小于key的元素,放到左边的坑中while (left < right && arr[right] >= key) {right--;}arr[left] = arr[right];// 从左向右找大于key的元素,放到右边的坑中while (left < right && arr[left] <= key) {left++;}arr[right] = arr[left];}// 将枢轴元素放入最后的坑中arr[left] = key;// 函数可以返回枢轴元素的位置,若需要进一步的迭代过程return left;
}
1.2 用栈实现的具体思路
以下面这串数组为例:
首先建立一个栈,将整个数组的起始和结束索引作为一对入栈
弹出一对索引(即栈顶元素)来指定当前要处理的子数组:这里即弹出0 9索引
找到枢轴6进行一次单趟排序:
针对这个数组:
6 3 4 9 5 8 7 2 1 10
我们使用“三数取中”法选择枢轴。起始位置的元素为6
,结束位置的元素为10
,中间位置的元素为5
。在这三个元素中,6
为中间大小的值,因此选择6
作为枢轴。因为枢轴已经在第一个位置,我们可以直接开始单趟排序。
现在,开始单趟排序:
- 枢轴值为
6
。 - 从右向左扫描,找到第一个小于
6
的数1
。 - 从左向右扫描,找到第一个大于
6
的数9
。 - 交换这两个元素。
- 继续进行上述步骤,直到左右指针相遇。
经过单趟排序后:
6 3 4 1 5 2 7 8 9 10
接下来需要将枢轴6
放置到合适的位置。我们知道,最终左指针和右指针会停在第一个大于或等于枢轴值6
的位置。在这个例子中,左右指针会停在7
上。现在我们将6
与左指针指向的位置的数交换:
5 3 4 1 2 6 7 8 9 10
现在枢轴值6
处于正确的位置,其左侧所有的元素都小于或等于6
,右侧所有的元素都大于或等于6
。
分区操作完成后,如果枢轴元素左侧的子数组(如果存在)有超过一个元素,则将其起始和结束索引作为一对入栈。同样,如果右侧的子数组(如果存在)也有超过一个元素,也将其索引入栈
我们接下来完成这个入栈过程:让两个子数组的索引入栈
接着取0 4索引进行单趟排序并不断分区,分割的索引继续压栈,继续迭代该过程,直到栈为空,此时所有的子数组都已经被正确排序
1.3 代码实现
这里我们调用之前的栈的代码,基本声明如下:
typedef int STDataType;typedef struct Stack
{STDataType* a;int top;int capacity;
}ST; void StackInit(ST* ps);
// 入栈
void StackPush(ST* ps, STDataType x);
// 出栈
void StackPop(ST* ps);
// 获取栈顶元素
STDataType StackTop(ST* ps);
// 获取栈中有效元素个数
int StackSize(ST* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(ST* ps);
// 销毁栈
void StackDestroy(ST* ps);
我们接下来完成排序代码,首先建栈,初始化,并完成第一个压栈过程:
ST s;
StackInit(&s);
StackPush(&s, end);
StackPush(&s, begin);
实现一次单趟排序:
int left = StackTop(&s);
StackPop(&s);int right = StackTop(&s);
StackPop(&s);int keyi = singlePassQuickSort(a, left, right);
注意这里我们先压入end,那么我们先出的就是begin,用left首先获取begin,再pop掉获取end
接着判断keyi左右是否还有子数组
if (left < keyi - 1)
{StackPush(&s, keyi - 1);StackPush(&s, left);
}
if (keyi + 1<right)
{StackPush(&s, right);StackPush(&s, keyi+1);
}
将此过程不断循环即为整个过程,总代码如下:
void Quicksortst(int* a, int begin, int end)
{ST s;StackInit(&s);StackPush(&s, end);StackPush(&s, begin);while (!StackEmpty(&s)){int left = StackTop(&s);StackPop(&s);int right = StackTop(&s);StackPop(&s);int keyi = singlePassQuickSort(a, left, right);if (left < keyi - 1){StackPush(&s, keyi - 1);StackPush(&s, left);}if (keyi + 1<right){StackPush(&s, right);StackPush(&s, keyi+1);}}StackDestroy(&s);
}
这里思想跟递归其实是差不多的,也是一次取一组进行排序,递归寻找每个区间
2.归并排序
假如我们已经有了两个已经排序好的数组,我们如何让他们并为一个有序的数组呢?
我们的做法就是用两个索引进行比较,然后插入一个新的数组完成排序,这就是归并排序的基础思路
那如果左右不是两个排序好的数组呢?
下面是归并排序的算法步骤:
-
递归分解数组:如果数组的长度大于1,首先将数组分解成两个部分。通常这是通过将数组从中间切分为大致相等的两个子数组
-
递归排序子数组:递归地对这两个子数组进行归并排序,直到每个子数组只包含一个元素或为空,这意味着它自然已经排序好
-
合并排序好的子数组:将两个排序好的子数组合并成一个排序好的数组。这通常通过设置两个指针分别指向两个子数组的开始,比较它们指向的元素,并将较小的元素放入一个新的数组中,然后移动指针。重复此过程,直到所有元素都被合并进新数组
所以我们得需要递归来实现这一过程,首先声明函数并建造新的数组:
void MergeSort(int* a, int n)
{int* tmp =(int *) malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}free(tmp);
}
由于我们不能每次开辟一遍数组,我们这里就需要一个子函数来完成递归过程:
void _MergrSort(int* a, int begin, int end, int* tmp);
首先,不断递归将数组分解:
int mid = (begin + end) / 2;if (begin >= end)
{return;
}
_MergrSort(a, begin, mid, tmp);
_MergrSort(a, mid+1, end, tmp);
接着获取分解的两个数组的各自的首端到尾端的索引:
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
令要插入到数组tmp的起点为begin处:
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
接下来遍历两个数组,无论谁先走完都跳出循环
while (begin1 <= end1 && begin2 <= end2)
{if (a[begin1] < a[begin2]){tmp[i] = a[begin1];i++;begin1++;}else{tmp[i] = a[begin2];i++;begin2++;}
}
这时会有一方没有遍历完,按照顺序插入到新数组中即可
while (begin1 <= end1)
{tmp[i] = a[begin1];begin1++;i++;
}
while (begin2<= end2)
{tmp[i] = a[begin2];begin2++;i++;
}
插入到新数组后,我们拷贝到原数组中即完成了一次排序
memcpy(a+begin,tmp+begin,sizeof(int )*(end-begin+1));
完整代码如下:
void _MergrSort(int* a, int begin, int end, int* tmp)
{int mid = (begin + end) / 2;if (begin >= end){return;}_MergrSort(a, begin, mid, tmp);_MergrSort(a, mid+1, end, tmp);int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = begin;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[i] = a[begin1];i++;begin1++;}else{tmp[i] = a[begin2];i++;begin2++;}}while (begin1 <= end1){tmp[i] = a[begin1];begin1++;i++;}while (begin2<= end2){tmp[i] = a[begin2];begin2++;i++;}memcpy(a+begin,tmp+begin,sizeof(int )*(end-begin+1));
}
void MergeSort(int* a, int n)
{int* tmp =(int *) malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}_MergrSort(a, 0, n - 1, tmp);free(tmp);
}
- 排序好的左半部分和右半部分接着被合并。为此,使用了两个游标
begin1
和begin2
,它们分别指向两个子数组的起始位置,然后比较两个子数组当前元素,将较小的元素拷贝到tmp数组中。这个过程继续直到两个子数组都被完全合并 - 在所有元素都被合并到tmp数组之后,使用memcpy将排序好的部分拷贝回原数组a。这个地方注意memcpy的第三个参数,它是
sizeof(int)*(end - begin + 1)
,表示拷贝的总大小,单位是字节 - begin和end变量在这里表示待排序和合并的数组部分的起止索引
本节内容到此结束!感谢大家阅读!
相关文章:
【数据结构与算法】:非递归实现快速排序、归并排序
🔥个人主页: Quitecoder 🔥专栏:数据结构与算法 上篇文章我们详细讲解了递归版本的快速排序,本篇我们来探究非递归实现快速排序和归并排序 目录 1.非递归实现快速排序1.1 提取单趟排序1.2 用栈实现的具体思路1.3 代码…...
2024-3-18-C++day6作业
1>思维导图 2>试编程 要求: 封装一个动物的基类,类中有私有成员:姓名,颜色,指针成员年纪 再封装一个狗这样类,共有继承于动物类,自己拓展的私有成员有:指针成员:腿的个数&a…...
【OceanBase诊断调优】—— 敏捷诊断工具obdiag一键分析OB集群日志设计与实践
最近总结一些诊断OCeanBase的一些经验,出一个【OceanBase诊断调优】专题,也欢迎大家贡献自己的诊断OceanBase的方法。 1. 前言 obdiag定位为OceanBase敏捷诊断工具。1.2版本的obdiag支持诊断信息的一键收集,光有收集信息的能力,…...
python 调用redis创建查询key
部署redis apiVersion: apps/v1 # 描述api版本,默认都用这个 kind: Deployment # 资源类型,可以配置为pod,deployment,service,statefulset等等 metadata: # deployment相关的元数据,用于描述deployment的…...
归并排序思路
归并排序是一种经典的分治算法,其基本思路可以简述为以下几步: 分解:将待排序的数组递归地分解成较小的子数组,直到每个子数组只包含一个元素为止。这里采用分治的思想,将问题不断地划分为规模更小的子问题。 合并&am…...
【蓝桥杯选拔赛真题65】python输出三个字符 第十五届青少年组蓝桥杯python选拔赛真题 算法思维真题解析
目录 python输出3个字符 一、题目要求 1、编程实现 2、输入输出...
K8S日志收集方案-EFK部署
EFK架构工作流程 部署说明 ECK (Elastic Cloud on Kubernetes):2.7 Kubernetes:1.23.0 文件准备 crds.yaml 下载地址:https://download.elastic.co/downloads/eck/2.7.0/crds.yaml operator.yaml 下载地址:https://download.e…...
js基础语法大全(时间戳,uuid,字符串转json)
目录 一、获取时间戳二、获取uuid三、字符串转json格式 一、获取时间戳 var times Math.round(new Date().getTime()/1000).toString(); //获取 10位 时间戳 console.log(times);二、获取uuid function guid() {return xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx.replace(/[xy]…...
uView LoadingIcon 加载动画
此组件为一个小动画,目前用在uView的loadMore加载更多等组件的正在加载状态场景。 #平台差异说明 App(vue)App(nvue)H5小程序√√√√ #基本使用 通过mode设定动画的类型,circle为圆圈的形状࿰…...
Elasticsearch使用Kibana进行基础操作
一、Restful接口 Elasticsearch通过RESTful接口提供与其进行交互的方式。在ES中,提供了功能丰富的RESTful API的操作,包括CRUD、创建索引、删除索引等操作。你可以用你最喜爱的 web 客户端访问 Elasticsearch 。事实上,你甚至可以使用 curl …...
“SRP模型+”多技术融合在生态环境脆弱性评价模型构建、时空格局演变分析与RSEI 指数的生态质量评价及拓展应用教程
原文链接:“SRP模型”多技术融合在生态环境脆弱性评价模型构建、时空格局演变分析与RSEI 指数的生态质量评价及拓展应用教程https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247597452&idx5&snf723d9e5858a269d00e15dbe2c7d3dc0&chksmfa823c6…...
【Windows 常用工具系列 15 -- VMWARE ubuntu 安装教程】
文章目录 安装教程镜像下载 工具安装 安装教程 安装教程参考链接:https://blog.csdn.net/Python_0011/article/details/131619864 https://linux.cn/article-15472-1.html 激活码 VMware 激活码连接:https://www.haozhuangji.com/xtjc/180037874.html…...
SpringSecurity(SpringBoot2.X版本实现)
资料来源于 SpringSecurity框架教程-Spring SecurityJWT实现项目级前端分离认证授权 侵权删 目录 介绍 快速开始 认证 认证流程 登录校验流程 SpringSecurity完整流程 认证流程详解 代码实现 准备工作 mysql mybatis-plus redis 统一返回类 核心代码 密码加密存…...
仿牛客项目Day8:社区核心功能2
显示评论 数据库 entity_type代表评论的目标类型,评论帖子和评论评论 entity_id代表评论的目标id,具体是哪个帖子/评论 targer_id代表评论指向哪个人 entity public class Comment {private int id;private int userId;private int entityType;priv…...
Vmware虚拟机配置虚拟网卡
背景 今天同事咨询了我一个关于虚拟机的问题,关于内网用Vmware安装的虚拟机,无法通过本机访问虚拟上的Jenkins的服务。 验证多次后发现有如下几方面问题。 Jenkins程序包和JDK版本不兼容(JDK1.8对应Jenkins不要超过2.3.57)虚…...
双向链表代码(带哨兵位循环/不带哨兵位不循环
以下代码全为本人所写,如有错误,很正常,还请指出, 目录 带哨兵位循环 test.c DLList.c DLList.h 不带哨兵位不循环 test.c DLList.c DLList.h 带哨兵位循环 test.c #define _CRT_SECURE_NO_WARNINGS#include"DLlist.h&…...
C语言自学笔记13----C语言指针与函数
C 语言指针与函数 在C语言编程中,也可以将地址作为参数传递给函数。 要在函数定义中接受这些地址,我们可以使用指针。这是因为指针用于存储地址。让我们举个实例: 示例:通过引用致电 #include <stdio.h> void swap(int n1, …...
每日五道java面试题之mybatis篇(一)
目录: 第一题. MyBatis是什么?第二题. ORM是什么?第三题. 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?第四题. 传统JDBC开发存在的问题第五题. JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的…...
一文解读ISO26262安全标准:概念阶段
一文解读ISO26262安全标准:概念阶段 1 相关项定义2 安全生命周期启动3 危害分析和风险评估 HaRa4 功能安全概念 由上一篇文章知道,安全生命周期包含概念阶段、产品开发阶段、生产发布后续阶段。本文详细解读概念阶段要进行的安全活动。 本部分规定了车辆…...
微信小程序调用百度智能云API(菜品识别)
一、注册后生成应用列表创建应用 二、找到当前所需使用的api菜品识别文档 三、点链接看实例代码 这里需要使用到如下几个参数(如下),其他的参数可以不管 client_id : 就是创建应用后的API Keyclient_secret: 就是创建…...
idea项目mapper.xml中的SQL语句黄色下划线去除
问题描述 当我们使用idea开发java项目时,经常会与数据库打交道,一般在使用mybatis的时候需要写一大堆的mapper.xml以及SQL语句,每当写完SQL语句的时候总是有黄色下划线,看着很不舒服。 解决方案: 修改idea的配置 Edi…...
es 聚合操作(二)
书接上文,示例数据在上一篇,这里就不展示了 一、Pipeline Aggregation 支持对聚合分析的结果,再次进行聚合分析。 Pipeline 的分析结果会输出到原结果中,根据位置的不同,分为两类: Sibling - 结果和现有…...
【vue.js】文档解读【day 5】| ref模板引用
如果阅读有疑问的话,欢迎评论或私信!! 本人会很热心的阐述自己的想法!谢谢!!! 文章目录 模板引用前言访问模板引用模板引用与v-if、v-show的结合v-for中的模板引用函数模板引用 模板引用 前言 …...
算法简单小技巧
主页:xiaocr_blog 1.最小公倍数和最大公约数 #include<iostream> using namespace std; int main(){int a,b;cin>>a>>b;int r a%b;while (r!0){a b;b r;r a%b;}cout<<b<<endl;return 0 ; } #include<iostream> using nam…...
前端入职配置新电脑!!!
前端岗位入职第一天到底应该做些什么呢?又该怎样高效的认识、融入团队?并快速进入工作状态呢?这篇文章就来分享一下,希望对即将走向或初入前端职场的你,能够有所帮助。内含大量链接,欢迎点赞收藏࿰…...
Java面试题总结15之简述你对RPC,RMI的理解
RPC:在本地调用远程的函数,远程过程调用,可以跨语言实现,httpClient RMI:远程方法调用,Java中用于实现RPC的一种机制,RPC的Java版本是J2EE的网络调用机制,跨JVM调用对象的方法&…...
内网穿透利器 n2n 搭建指南
1. n2n 简介 上文实验分析了 FRP 和 Zerotier 的利弊,本文再介绍另一种内网穿透方案,n2n。 n2n 是 C/S 架构的内网穿透服务,不同于 FRP 的 反向代理,它的原理是类似 Zerotier 的先打孔,打孔失败再尝试转发。关于打孔本…...
phpcms头像上传漏洞引发的故事
目录 关键代码 第一次防御 第一次绕过 第二次防御 第二次绕过 第三次防御 第三次绕过 如何构造一个出错的压缩包 第四次防御 第四次绕过 本篇文章是参考某位大佬与开发人员对于文件包含漏洞的较量记录下的故事,因为要学习文件包含漏洞,就将大佬…...
二叉树|二叉树理论基础、二叉树的递归遍历
代码随想录 (programmercarl.com) 树和二叉树 1.树的基本概念 1.1树的定义 1.2树的逻辑表示方法 1.3树的基本术语 1.4树的性质 1.5树的基本运算 1.6树的存储结构 2.二叉树的概念和性质 2.1二叉树的定义 2.2二叉树的性质 2.3二叉树与树、森林之间的转换 3.二叉树的…...
JavaScript 语法-对象
对象 JavaScript 中的对象是一组键值对的集合,其中每个键都是字符串,每个值可以是任意类型。 对象是由一些属性和方法组成的集合,属性可以用来存储数据,方法可以用来操作数据。 属性和方法使用“.”来访问 // 创建一个对象 let …...
wordpress占用内存/网络营销推广方案怎么写
1、设计初衷: 身体是革命的本钱,面对生存,各种不规律的生活作息和重压下无法放松的紧张神经,导致我们的身体和心理都不堪重负。同时,在经济条件越来越好的情况下,越来越多的人开始关注起了健康这个亘古不变…...
网站建设超市/广点通官网
1.性能 Py3.0运行 pystone benchmark的速度比Py2.5慢30%。Guido认为Py3.0有极大的优化空间,在字符串和整形操作上可 以取得很好的优化结果。 Py3.1性能比Py2.5慢15%,还有很大的提升空间。 2.编码 Py3.X源码文件默认使用utf-8编码,这就使得以下…...
wordpress 代码在哪/竞价排名适合百度这样的网络平台吗
修改 checkbox(复选框) 默认的颜色 https://blog.csdn.net/weixin_44037153/article/details/107405594 复选框的全选全不选(单级) https://www.cnblogs.com/sgs123/p/10436516.html 复选框的全选全不选(多层级) 效果图 代码 <template><div class"deliver…...
动画形式的h5在哪个网站做/培训学校网站
转载请注明出处。谢谢http://blog.csdn.net/harryweasley/article/details/42121485 当想到要计算差值。我们肯定想的是“2014.12.14”-“2014.12.20”4,这个方案,可是java并没有直接给我们这个方案,所以我想的是,将字符串转化为D…...
品牌型网站建设特点/淘宝运营培训课程免费
TCP 作为传输层的协议,是一个软件工程师素养的体现,也是面试中经常被问到的知识点。在此,我将 TCP 核心的一些问题梳理了一下,希望能帮到各位。001. 能不能说一说 TCP 和 UDP 的区别?首先概括一下基本的区别:TCP是一个…...
网站设计一般什么字体/怎么做网络推广
①包装类 1、其中需要注意int对应的是Integer,char对应的Character,其他6个都是基本类型首字母大写即可。 2、字符串转成基本类型:包装类.parseInt(str); 3、基本数值转成字符串有3种方式: (1)、基本类型直…...