【数据结构与算法】:非递归实现快速排序、归并排序

Alt

🔥个人主页: Quitecoder

🔥专栏:数据结构与算法
Alt

上篇文章我们详细讲解了递归版本的快速排序,本篇我们来探究非递归实现快速排序和归并排序

目录

  • 1.非递归实现快速排序
    • 1.1 提取单趟排序
    • 1.2 用栈实现的具体思路
    • 1.3 代码实现
  • 2.归并排序

1.非递归实现快速排序

快速排序的非递归实现主要依赖于栈(stack)来模拟递归过程中的函数调用栈。递归版本的快速排序通过递归调用自身来处理子数组,而非递归版本则通过手动管理一个栈来跟踪接下来需要排序的子数组的边界

那么怎样通过栈来实现排序的过程呢?

思路如下:

使用栈实现快速排序是对递归版本的模拟。在递归的快速排序中,函数调用栈隐式地保存了每次递归调用的状态。但是在非递归的实现中,你需要显式地使用一个辅助栈来保存子数组的边界

以下是具体步骤和栈的操作过程:

  1. 初始化辅助栈
    创建一个空栈。栈用于保存每个待排序子数组的起始索引(begin)和结束索引(end)。

  2. 开始排序
    将整个数组的起始和结束索引作为一对入栈。这对应于最初的排序问题。

  3. 迭代处理
    在栈非空时,重复下面的步骤:

    • 弹出一对索引(即栈顶元素)来指定当前要处理的子数组。
    • 选择子数组的一个元素作为枢轴(pivot)进行分区(可以是第一个元素,也可以通过其他方法选择,下面我们还是用三数取中)。
    • 进行分区操作,这会将子数组划分为比枢轴小的左侧部分和比枢轴大的右侧部分,同时确定枢轴元素的最终位置。
  4. 处理子数组
    分区操作完成后,如果枢轴元素左侧的子数组(如果存在)有超过一个元素,则将其起始和结束索引作为一对入栈。同样,如果右侧的子数组(如果存在)也有超过一个元素,也将其索引入栈

  5. 循环
    继续迭代该过程,直到栈为空,此时所有的子数组都已经被正确排序。

所以主要思路就两个:

  1. 分区
  2. 单趟排序

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作为枢轴。因为枢轴已经在第一个位置,我们可以直接开始单趟排序。

现在,开始单趟排序:

  1. 枢轴值为6
  2. 从右向左扫描,找到第一个小于6的数1
  3. 从左向右扫描,找到第一个大于6的数9
  4. 交换这两个元素。
  5. 继续进行上述步骤,直到左右指针相遇。

经过单趟排序后:

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. 递归分解数组:如果数组的长度大于1,首先将数组分解成两个部分。通常这是通过将数组从中间切分为大致相等的两个子数组

  2. 递归排序子数组:递归地对这两个子数组进行归并排序,直到每个子数组只包含一个元素或为空,这意味着它自然已经排序好

  3. 合并排序好的子数组:将两个排序好的子数组合并成一个排序好的数组。这通常通过设置两个指针分别指向两个子数组的开始,比较它们指向的元素,并将较小的元素放入一个新的数组中,然后移动指针。重复此过程,直到所有元素都被合并进新数组

在这里插入图片描述
所以我们得需要递归来实现这一过程,首先声明函数并建造新的数组:

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);
}
  • 排序好的左半部分和右半部分接着被合并。为此,使用了两个游标begin1begin2,它们分别指向两个子数组的起始位置,然后比较两个子数组当前元素,将较小的元素拷贝到tmp数组中。这个过程继续直到两个子数组都被完全合并
  • 在所有元素都被合并到tmp数组之后,使用memcpy将排序好的部分拷贝回原数组a。这个地方注意memcpy的第三个参数,它是sizeof(int)*(end - begin + 1)表示拷贝的总大小,单位是字节
  • begin和end变量在这里表示待排序和合并的数组部分的起止索引

本节内容到此结束!感谢大家阅读!

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

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

相关文章

Linux:git的基础操作

git的下载 版本控制系统一般分为两种&#xff0c;集中式版本控制系统&#xff0c;分布式版本控制系统 什么是集中式版本控制系统&#xff1a;版本库集中存放在中央服务器&#xff0c;工作时候使用自己的电脑&#xff0c;当工作时候在中央服务器上拉取最新版本的代码&#xff0c…

U盘变身“本地磁盘”?数据恢复与防范策略大揭秘

一、突发状况&#xff1a;U盘秒变“本地磁盘” 在日常工作生活中&#xff0c;U盘凭借其便携性和大容量&#xff0c;成为我们存储和传输数据的重要工具。然而&#xff0c;有时我们会遇到这样一个棘手的问题&#xff1a;原本应显示为可移动磁盘的U盘&#xff0c;在插入电脑后却突…

ModuleNotFoundError: No module named,(python跨文件导包)

方法一&#xff1a;在文件中开头加入&#xff1a; import os import sys REPO os.path.dirname(os.path.dirname(os.path.realpath(__file__))) sys.path.append(REPO) 方法二&#xff1a;直接添加环境变量&#xff08;比较好用&#xff09; # 添加环境变量export PYTHONPAT…

Nginx 的安装、启动和关闭

文章目录 一、背景说明二、Nginx 的安装2.1、依赖的安装2.2、Nginx 安装2.3、验证安装 三、启动 Nginx3.1、普通启动3.2、如何判断nginx已启动3.3、通过配置启动3.4、设置开机启动 四、关闭 Nginx4.1、优雅地关闭4.2、快速关闭4.3、只关闭主进程4.4、使用nginx关闭服务 五、重启…

R语言:microeco:一个用于微生物群落生态学数据挖掘的R包:第七:trans_network class

# 网络是研究微生物生态共现模式的常用方法。在这一部分中&#xff0c;我们描述了trans_network类的所有核心内容。 # 网络构建方法可分为基于关联的和非基于关联的两种。有几种方法可以用来计算相关性和显著性。 #我们首先介绍了基于关联的网络。trans_network中的cal_cor参数…

编曲学习:如何编写钢琴织体 Cubase12逻辑预置 需要弄明白的问题

钢琴织体是指演奏形式、方式,同一个和弦进行可以用很多种不同的演奏方法。常用织体有分解和弦,柱式和弦,琶音织体,混合织体。 在编写钢琴织体前,先定好歌曲的调。 Cubase小技巧:把钢琴轨道向上拖动打和弦轨道,就可以显示和弦!如果你有一些参考工程,不知道用了哪些和…

yum安装mysql及数据库补全功能

centos7上面没有mysql&#xff0c;它的数据库名字叫做mariadb [rootlocalhost ~]#yum install mariadb-server -y [rootlocalhost ~]#systemctl start mariadb.service [rootlocalhost ~]#systemctl stop firewalld [rootlocalhost ~]#setenforce 0 [rootlocalhost ~]#ss -na…

kerberos验证协议安装配置使用

一、kerberos是什么 Kerberos 是一个网络身份验证协议&#xff0c;用于在计算机网络中进行身份验证和授权。它提供了一种安全的方式&#xff0c;允许用户在不安全的网络上进行身份验证&#xff0c;并获取访问网络资源的权限。 二、安装配置kerberos服务端 1、安装kerberos #检…

微信小程序 nodejs+vue+uninapp学生在线选课作业管理系统

基于微信小程序的班级作业管理助手使用的是MySQL数据库&#xff0c;nodejs语言和IDEA以及微信开发者工具作为开发工具&#xff0c;这些技术和工具我在日常的作业中都经常的使用&#xff0c;并且因为对编程感兴趣&#xff0c;在闲暇时间也进行的进行编程的提高&#xff0c;所以在…

计算机二级(Python)真题讲解每日一题:《十字叉》

描述‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‮‬ ‪‬‪‬‪‬‪‬‪‬‮‬‪…

二、python基础

一、关键字&#xff08;保留字&#xff09; 指在python中赋予特定意义的一类单词&#xff0c;不能将关键字作为函数、变量、类、模块的名称 import keyword#利用内存模块keyword print(keyword.kwlist)#输出所有关键 print(len(keyword.kwlist))#利用内置函数len()输出关键字的…

Python笔记四之协程

本文首发于公众号&#xff1a;Hunter后端 原文链接&#xff1a;Python笔记四之协程 协程是一种运行在单线程下的并发编程模型&#xff0c;它的特点是能够在一个线程内实现多个任务的并发操作&#xff0c;通过在执行任务时主动让出执行权&#xff0c;让其他任务继续执行&#xf…

前端面试拼图-实践经验

摘要&#xff1a;最近&#xff0c;看了下慕课2周刷完n道面试题&#xff0c;记录并添加部分可参考的文档&#xff0c;如下... 1. H5页面如何进行首屏优化&#xff1f; 路由懒加载 适用于SPA&#xff08;不适用MPA&#xff09; 本质就是路由拆分&#xff0c;有限保证首页加载 服…

ThreadLocal-内存泄露问题

ThreadLocal概述 ThreadLocal是多线程中对于解决线程安全的一个操作类&#xff0c;它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享案例&#xff1a;使用JDBC操作数据库时&#xff0c;会将每一个线程的Conn…

从0到1理解MySQL的事务和ACID特性

从0到1理解MySQL的事务和ACID特性 在开始之前&#xff0c;我们先来了解一下MySQL的事务是什么&#xff0c;以及ACID是什么。 事务是什么&#xff1f; 事务可以简单地理解为一组数据库操作的集合&#xff0c;它们一起被视为一个不可分割的工作单元。事务要么全部成功执行&…

SpringBoot ApplicationListener实现发布订阅模式

文章目录 前言一、Spring对JDK的扩展二、快速实现发布订阅模式 前言 发布订阅模式(Publish-Subscribe Pattern)通常又称观察者模式&#xff0c;它被广泛应用于事件驱动架构中。即一个事件的发布&#xff0c;该行为会通过同步或者异步的方式告知给订阅该事件的订阅者。JDK中提供…

springboot 查看和修改内置 tomcat 版本

解析Spring Boot父级依赖 去到项目的根pom文件中&#xff0c;找到parent依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>${springboot.version}…

Error response from daemon Get server gave HTTP response to HTTPS client

使用docker compose拉起docker镜像时&#xff0c;若出现如下报错 Error response from daemon: Get "https://devops.test.cn:5000/v2/": http: server gave HTTP response to HTTPS client表示Docker守护进程无法从指定url获取响应&#xff0c; 可能原因有以下&…

苍穹外卖-day09:用户端历史订单模块(理解业务逻辑),商家端订单管理模块(理解业务逻辑),校验收货地址是否超出配送范围(相关API)

用户端历史订单模块 1. 查询历史订单&#xff08;分页查询&#xff09; 1.1 需求分析和设计 产品原型&#xff1a; 业务规则 分页查询历史订单可以根据订单状态查询展示订单数据时&#xff0c;需要展示的数据包括&#xff1a;下单时间、订单状态、订单金额、订单明细&#…

C解析YAML

在C语言中解析YAML文件&#xff0c;你可以使用如libyaml等专门的YAML解析库。以下是使用libyaml库进行YAML解析的一个基本示例。libyaml是一个用来解析和生成YAML数据的C库。 首先&#xff0c;你需要确保已经正确安装了libyaml库。对于许多Linux发行版&#xff0c;你可以使用包…