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

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

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

相关文章

QY-02-W2 无线雨量监测站 防洪防汛 大坝水库雨水情实时监测

无线雨量监测站配置&#xff1a; 不锈钢支架、雨量传感器、太阳能板、数据采集发送器。 产品概述 无线雨量监测站是一款无线传输数据的雨量监测设备&#xff0c;由不锈钢支架、ABS雨量筒、不锈钢防护箱、主机采集器、无线数据发送装置、太阳能电池板、蓄电池等构成&#xff0c…

体验OceanBase OBD V2.5.0 组件内扩容和组件变更

背景 OBD 是OceanBase的命令行部署工具&#xff0c;在 obd V2.5.0 版本之前&#xff0c;其主要功能主要是部署各类组件&#xff0c;例如 oceanbase-ce,obproxy-ce,obagent 等。然而&#xff0c;它并不支持组件的变更操作以及组件内部的扩缩容调整。具体来说&#xff1a; 1、若…

利用自定义 URI Scheme 在 Android 应用中实现安全加密解密功能

在现代移动应用开发中&#xff0c;安全性和用户体验是至关重要的考虑因素。在 Android 平台上&#xff0c;开发人员可以利用自定义 URI Scheme 和 JavaScript 加密解密技术来实现更安全的数据传输和处理。本文将介绍如何在 Android 应用中注册自定义 URI Scheme&#xff0c;并结…

鸿蒙App开发学习 - TypeScript编程语言全面开发教程(下)

现在我们接着上次的内容来学习TypeScript编程语言全面开发教程&#xff08;下半部分&#xff09; 4. 泛型 TypeScript 中的泛型&#xff08;Generics&#xff09;是一种编程模式&#xff0c;用于在编写代码时增强灵活性和可重用性。泛型使得在定义函数、类、接口等数据类型时…

Machine Learning ---- Feature Scaling

目录 一、What is feature scaling:&#xff1a; 二、Why do we need to perform feature scaling? 三、How to perform feature scaling: 1、Normalization: 2、Mean normalization: 3、Standardization (data needs to follow a normal distribution): 一、What is featur…

关于 闰年 的小知识,为什么这样判断闰年

闰年的规定&#xff1a; 知道了由来&#xff0c;我们就可以写程序来判断&#xff1a; #include <stdio.h> int main() {int year, leap;scanf("%d",&year);if((year%4 0 && year%100 ! 0) || year%400 0)leap 1;else leap 0;if(leap) printf(…

26-Java访问者模式 ( Visitor Pattern )

Java访问者模式 摘要实现范例 访问者模式&#xff08;Visitor Pattern&#xff09;使用了一个访问者类&#xff0c;它改变了元素类的执行算法&#xff0c;通过这种方式&#xff0c;元素的执行算法可以随着访问者改变而改变访问者模式中&#xff0c;元素对象已接受访问者对象&a…

为什么说CRM是制造业数字化模型的源头?

2023年&#xff0c;中国制造业投资持续企稳向好&#xff0c;全年增长6.5%。制造业发展韧性强劲&#xff0c;但如何激发有效需求&#xff0c;在防范产能过剩的同时实现产业升级&#xff0c;这是摆在当下诸多制造企业面前的现实问题。 山东章鼓企划信息管理处部长、山东鲸头鹳智…

win修改图标自定义QQ桌面图标

当安装了TIM后&#xff0c;想把图标改成QQ 图标见顶部&#xff0c;或通过网盘下载 提取码&#xff1a;9Ayc 操作步骤&#xff1a; 1.桌面右键图标&#xff0c;点击属性 2.选择快捷方式-更改图标 3.浏览选择下载的ico图标即可

基于单片机的老人防丢系统设计

目 录 摘 要 I Abstract II 引 言 3 1 系统总体架构 6 1.1方案设计与选择 6 1.2 系统架构设计 6 1.3 系统器件选择 7 2 系统硬件设计 9 2.1 单片机外围电路设计 9 2.2 LCD1602液晶显示电路设计 12 2.3 短信模块电路设计 14 2.4 GPS模块电路设计 14 2.5 电源与按键控制电路设计…

C++——字符串、读写文件、结构体、枚举

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的夜间车辆检测系统(深度学习代码+UI界面+训练数据集)

摘要&#xff1a;开发夜间车辆检测系统对于自动驾驶技术具有关键作用。本篇博客详细介绍了如何运用深度学习构建一个夜间车辆检测系统&#xff0c;并提供了完整的实现代码。该系统基于强大的YOLOv8算法&#xff0c;并对比了YOLOv7、YOLOv6、YOLOv5&#xff0c;展示了不同模型间…

力扣坑题:回文排列

bool canPermutePalindrome(char* s){ int arr[128]{0}; for(int x0;x<strlen(s);x) { arr[(s[x])]; } int flag0; for(int i0;i<128;i) {if(arr[i]%2!0){flag;} } if(flag>1) {return false; } return true; }没想到吧&#xff0c;不止26个英文字母

maven一点通

1.maven简介 Maven是一个基于Java的工程构建工具&#xff0c;用于管理和构建项目的依赖关系。它提供了一种标准的项目结构和一组约定&#xff0c;使得项目的开发、构建、部署和文档化更加容易和可靠。 Maven的主要功能包括&#xff1a; 依赖管理&#xff1a;Maven可以自动下载…

海康威视相机SDK二次开发(JAVA语言)

目录 前言客户端创建虚拟相机示例代码保存图片程序运行结果修改需求 二次开发引入外部包对SaveImage.java文件进行修改保存图片saveDataToFile方法选择相机chooseCamera方法主方法 FileUtil类处理过期照片启动类与配置文件application.yml通过实体类读取yml启动类 SaveImage.ja…

【JS逆向学习】猿人学第六题 js混淆 回溯

逆向目标 网址&#xff1a;https://match.yuanrenxue.cn/match/6接口&#xff1a;https://match.yuanrenxue.cn/api/match/6参数&#xff1a;payload(m、q) 逆向过程 老规矩&#xff0c;先来分析网络请求&#xff0c;加密的地方一目了然&#xff0c;没什么可多说的&#xff…

惬意上手Redis

Redis介绍 Redis&#xff08;全称为REmote Dictionary Server&#xff09;是一个开源的、内存中的数据存储结构&#xff0c;主要用作应用程序缓存或快速相应数据库。 REmote Dictionary Server: 有道翻译Redis是“远程字典服务”&#xff0c;远程不过是远程访问&#xff0c;而…

Linux:搭建ntp服务器

我准备两个centos7服务器 一个为主服务器连接着外网&#xff0c;并且搭建了ntp服务给其他主机同步 另外一个没有连接外网&#xff0c;通过第一台设备去同步时间 首先两个服务器都要安装ntp软件 yum -y install ntp 再把他俩的时间都改成别的 左侧的是主服务器&#xff0c;主…

07|链(下):想学“育花”还是“插花”用RouterChain确定客户意图

任务设定 鲜花养护&#xff08;保持花的健康、如何浇水、施肥等&#xff09;鲜花装饰&#xff08;如何搭配花、如何装饰场地等&#xff09; 如果接到的是第一类问题&#xff0c;你要给ChatBot A指示&#xff1b;如果接到第二类的问题&#xff0c;你要给ChatBot B指示。 整体…

数据可信流通,从运维信任到技术信任

信任 共同观点&#xff1a; 信任是涉及交易或交换关系的基础 身份可确认利益可依赖能力有预期行为有后果 数据流通中的不可信风险 内循环&#xff1a;数据持有方在自己的运维安全域内对自己的额数据使用和安全拥有全责外循环&#xff1a;数据要素在离开持有方安全域后&#…