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

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;
		else
			return end;
	}
	else
	{
		if (a[midi] > a[end])
			return midi;
		else if (a[end] < a[begin])
			return end;
		else
			return 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变量在这里表示待排序和合并的数组部分的起止索引

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PHP反序列化---字符串逃逸(增加/减少)

一、PHP反序列化逃逸--增加&#xff1a; 首先分析源码&#xff1a; <?php highlight_file(__FILE__); error_reporting(0); class A{public $v1 ls;public $v2 123;public function __construct($arga,$argc){$this->v1 $arga;$this->v2 $argc;} } $a $_GET[v…

计算机组成原理-2-计算机的发展应用

2. 计算机的发展应用 文章目录 2. 计算机的发展应用2.1 计算机的发展史2.1.1 计算机的产生和发展2.1.2 微型计算机的出现和发展2.1.3 软件技术的发展和兴起 2.2 计算机的应用和展望 本笔记参考哈工大刘宏伟老师的MOOC《计算机组成原理&#xff08;上&#xff09;_哈尔滨工业大学…

sqllab第二十四关通关笔记

知识点&#xff1a; 二次注入 先埋一个炸弹&#xff0c;然后通过其他路径引爆它 查看界面发现是一个登录框&#xff0c;尝试进行登录框的注入发现这里不存在注入点 那么就注册一个新的账户吧 通过点击注册&#xff0c;进入注册面板&#xff0c;注册一个新的账户 用户名为 re…

react native 实现自定义底部导航与路由文件配置

首先先把需要的一些库引入 yarn install react-navigation/native yarn install react-native-screens react-native-safe-area-context yarn install react-navigation/native-stack yarn add react-navigation/bottom-tabs 创建路由文件及四个底部导航页面 router文件下的bot…

车载电子电器架构 - 网络拓扑

车载电子电器架构 - 网络拓扑 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师 (Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,喝完再挣扎,出门靠…

SpringBoot ApplicationListener实现发布订阅模式

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

数据结构中单向链表(无头)的学习

一.数据结构 1.定义 一组用来保存一种或者多种特定关系的数据的集合&#xff08;组织和存储数据&#xff09; 程序的设计&#xff1a;将现实中大量而复杂的问题以特定的数据类型和特定的存储结构存储在内存中&#xff0c; 并在此基础上实现某个特定的功能的操…

Games101-光栅化(三角形 )

视锥 fov:可视角 aspect ratio&#xff1a;宽高比 MVP 屏幕&#xff1a;抽象的认为是一个二维数组&#xff0c;数组中的每一个元素是一个像素 屏幕是一个典型的光栅成像设备 光栅化&#xff1a;把东西画在屏幕上的过程 像素&#xff1a;理解为不变的最小单位&#xff0c;RGB的…

SSA优化最近邻分类预测(matlab代码)

SSA-最近邻分类预测matlab代码 麻雀搜索算法(Sparrow Search Algorithm, SSA)是一种新型的群智能优化算法&#xff0c;在2020年提出&#xff0c;主要是受麻雀的觅食行为和反捕食行为的启发。 数据为Excel分类数据集数据。 数据集划分为训练集、验证集、测试集,比例为8&#…

Android14之HIDL报错:Invalid sparse file format at header magic(一百九十六)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

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

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

苍穹外卖-day06:HttpClient、微信小程序开发、微信登录(业务流程)、导入商品浏览功能代码(业务逻辑)

苍穹外卖-day06 课程内容 HttpClient微信小程序开发微信登录导入商品浏览功能代码 功能实现&#xff1a;微信登录、商品浏览 微信登录效果图&#xff1a; 商品浏览效果图&#xff1a; 1. HttpClient 1.1 介绍 HttpClient 是Apache Jakarta Common 下的子项目&#xff0c;…

Redis中的String编码转换底层原理及6.0新特性

String编码转换底层原理 String对象为什么把大于39字节或者44字节的字符串编码为raw&#xff0c;小于的时候编码为embstr? 在Redis3.2以前的版本中,SDS作为字符串类型中存储字符串内容的结构&#xff0c;源码如下&#xff1a; 3.2版本SDS结构 struct sdshdr {// 记录buf数…

linux 安装常用软件

文件传输工具 sudo yum install –y lrzsz vim编辑器 sudo yum install -y vimDNS 查询 sudo yum install bind-utils用法可以参考文章 《掌握 DNS 查询技巧&#xff0c;dig 命令基本用法》 net-tools包 yum install net-tools -y简单用法&#xff1a; # 查看端口占用情况…

[python3] 设置多进程名称并且在ps命令中可见

Centos7 系统 setproctitle 是一个 Python 模块&#xff0c;用于设置进程标题&#xff08;process title&#xff09;。进程标题是在系统中用来标识进程的名字&#xff0c;通常会显示在系统级的进程管理工具&#xff08;如 ps 命令&#xff09;中。通过设置进程标题&#xff0c…

代码随想录|Day23|回溯03|39.组合总和、40.组合总和II、131.分割回文串

39.组合总和 本题和 216.组合总和III 类似&#xff0c;但有几个区别&#xff1a; 没有元素个数限制&#xff1a;树的深度并不固定&#xff0c;因此递归终止条件有所变化每个元素可以使用多次&#xff1a;下层递归的起始位置和上层相同&#xff08;startIndex不需要改动&#xf…

接口测试常见接口类型?

常见接口类型 1.根据协议区分 1、webService接口:是走soap协议通过http传输请求报文和返回报文都是xml格式的&#xff0c;我们在测试的时候都用通过工具才能进行调用&#xff0c;测试。可以使用的工具有Soapul、jmeter、loadrunner等; 2、http接口:是走http协议&#xff0c;…

Python爬虫在Django项目中的数据处理与展示实例

当谈到Python爬虫技术与Django项目结合时&#xff0c;我们面临着一个引人入胜又具有挑战性的任务——如何利用爬虫技术从网络上抓取数据&#xff0c;并将这些数据进行有效地处理和展示。在本文中&#xff0c;我将为您介绍Python爬虫技术在Django项目中的数据抓取与处理流程。 在…

Java-JVM 虚拟机原理调优实战

一、基础 栈帧&#xff08;Stack Frame&#xff09;栈空间的 基本元素&#xff0c;用于 方法的调用和方法的执行的数据结构 堆内存用来存放由new创建的对象和数组。在堆中分配的内存&#xff0c;由Java虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或对象后&#xff0c…

Linux 管道

目录 一、认识管道 二、匿名管道 pipe函数 用法&#xff1a; pipefd&#xff1a; 匿名管道通信&#xff1a; 三、命名管道 概念&#xff1a; 创建&#xff1a; 特性&#xff1a; 用途&#xff1a; 四、命名管道和匿名管道的区别 命名&#xff1a; 持久性&#xff1a;…