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

news2024/11/16 17:47:35

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变量在这里表示待排序和合并的数组部分的起止索引

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

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

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

相关文章

掘根宝典之C++RTTI和类型转换运算符

什么是RTTI RTTI是运行阶段类型识别的简称。 哪些是RTTI? C有3个支持RTTI的元素。 1.dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针&#xff0c;否则该运算符返回0——空指针。 2.typeid运算符返回一个指出对象类型的信息 3.type_info结构存储…

【鸿蒙HarmonyOS开发笔记】如何使用图片插帧将低像素图片清晰放大

开发UI时&#xff0c;当我们的原图分辨率较低并且需要放大显示时&#xff0c;图片会模糊并出现锯齿。如下图所示 这时可以使用interpolation()方法对图片进行插值&#xff0c;使图片显示得更清晰。该方法的参数为ImageInterpolation枚举类型&#xff0c;可选的值有: ImageInte…

通过点击按钮实现查看全屏和退出全屏的效果

动态效果如图&#xff1a; 可以通过点击按钮&#xff0c;或者esc键实现全屏和退出全屏的效果 实现代码&#xff1a; <template><div class"hello"><el-button click"fullScreen()" v-if"!isFullscreen">查看全屏</el-butt…

centos创建并运行一个redis容器 并支持数据持久化

步骤 : 创建redis容器命令 docker run --name mr -p 6379:6379 -d redis redis-server --appendonly yes 进入容器 : docker exec -it mr bash 链接redis : redis-cli 查看数据 : keys * 存入一个数据 : set num 666 获取数据 : get num 退出客户端 : exit 再退…

猫头虎分享已解决Bug || TypeError: Cannot interpret ‘float‘ value as integer.

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

luceda ipkiss教程 62:等长波导布线(二)

教程 27介绍了两段波导等长布线的例子&#xff0c;下面同样是通过控制偏移量实现三段波导的等长布线&#xff1a; 所有代码如下&#xff1a; from si_fab import all as pdk from ipkiss3 import all as i3class demo(i3.Circuit):mmi i3.ChildCellProperty(doc"mmi in…

数据导入--Insert into

Insert Into是我们在MySQL中常用的导入方式&#xff0c;StarRocks同样也支持使用Insert into的方式进行数据导入&#xff0c;并且每次insert into操作都是一次完整的导入事务。 在StarRocks中&#xff0c;Insert的语法和MySQL等数据库的语法类似&#xff0c;具体可以参考官网文…

苹果谷歌,要联手反攻了

一则消息&#xff0c;让苹果、谷歌的夜盘股价一度分别暴拉1.5、3.5%&#xff0c;谷歌盘前甚至飙升超过5.5%&#xff0c;引发市场一阵轰动。 据知情人士透露&#xff0c;苹果公司正在谈判将谷歌的Gemini人工智能引擎植入iPhone&#xff0c;希望获得Gemini的授权&#xff0c;为今…

【办公类-22-11】周计划系列(5-3)“周计划-03 周计划内容循环修改“ (2024年调整版本)

背景需求&#xff1a; 前文从原来的“新模版”文件夹里提取了周计划主要内容和教案内容。 【办公类-22-10】周计划系列&#xff08;5-2&#xff09;“周计划-02源文件docx读取5天“ &#xff08;2024年调整版本&#xff09;-CSDN博客文章浏览阅读1.1k次&#xff0c;点赞29次&…

全基因集GSEA富集分析

原文链接&#xff1a;一文完成全基因集GSEA富集分析 本期内容 写在前面 我们前面分享过一文掌握单基因GSEA富集分析的教程&#xff0c;主要使用单基因的角度进行GSEA富集分析。 我们社群的同学咨询&#xff0c;全基因集的GSEA如何分析呢&#xff1f;&#xff1f;其实&#x…

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

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

C语言例:整型常量025,求解十进制和十六进制

1. 八进制数的每一位乘以对应的权值&#xff08;8的幂&#xff09;&#xff0c;然后将结果相加&#xff0c;得到十进制数。 025 21 2.八进制先转二进制&#xff08;一变三&#xff09;&#xff0c;再二进制转十六进制&#xff08;四合一&#xff09; 025 0001 0101 0…

25双体系Java学习之StringBuffer和StringBuilder

StringBuffer和StringBuilder ★小贴士 String str new String("welcome to "); str "here"; 字符串的拼接过程实际上是通过建立一个StringBuffer&#xff0c;然后调用StringBuffer的append方法&#xff0c;最后再将StringBuffer转为字符串&#xff0c…

第四百一十回

文章目录 1. 概念介绍2. 方法与细节2.1 获取方法2.2 使用细节 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何获取当前系统语言"相关的内容&#xff0c;本章回中将介绍如何获取时间戳.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章…

ElasticSearch架构设计

一、基础概念 Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene™ 基础上的搜索引擎.当然 Elasticsearch 并不仅仅是 Lucene 那么简单&#xff0c;它不仅包括了全文搜索功能&#xff0c;还可以进行以下工作: 一个分布式的实时文档…

【Linux】深入了解Linux磁盘配额:限制用户磁盘空间的利器

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;Linux ⛳️ 功不唐捐&#xff0c;玉汝于成 前言 在多用户环境下管理磁盘空间是服务器管理中的一项重要任务。Linux提供了强大的磁盘配额功能&#xff0c;可以帮助管理员限制用户或组对文件系统…

【趣味项目】命令行图片格式转换器

【趣味项目】一键生成LICENSE 项目地址&#xff1a;GitHub 项目介绍 一款命令行内可以批量修改图片格式的工具 使用方式 npm install xxhls/image-transformer -gimg-t --name.*.tiff --targetpng --path./images --recursiontrue技术选型 typeScript: 支持类型体操chal…

你开发的系统国际化了吗?

亲爱的朋友们&#xff0c;周一好&#xff0c;新的一周&#xff0c;精神满满。 在开发Spring Boot应用时&#xff0c;接口的参数校验是一个重要的环节&#xff0c;它确保了数据的完整性和准确性。而国际化处理则使得应用能够支持多种语言&#xff0c;提升了用户体验。 一、参数…

vue中判断是否使用自定义插槽

在封装自定义组件时&#xff0c;需要判断使用者是否使用了插槽<slot"aaa">&#xff0c;如果没有则使用一个组件中默认的值&#xff0c;反之就用传入的内容<template name"aaa"></template>,实现如下&#xff1a; <div class"lin…

《1w实盘and大盘基金预测 day6》

昨日预测完美&#xff0c;点位基本符合&#xff0c;我预测3052&#xff0c;实际最低3055。 走势也符合高平开&#xff0c;冲高回落&#xff0c;再反震荡上涨 大家可以观察我准不准哟&#xff5e;后面有我的一些写笔记、分享的网站。 关注公众号&#xff0c;了解各种理财预测内…