玩转快速排序(C语言版)

news2024/12/27 13:42:26

W...Y的主页 😊

代码仓库分享  💕


🍔前言:

本篇文章,我们来讲解一下神秘的快速排序。对于快速排序我相信大家都已经有所耳闻,但是快速排序是有很多的版本的。我们这次的目的就是快排的所有内容搞懂,废话不多说,让我们开始今天的内容。

🍟目录

快排的介绍

hoare版本

单趟排序 

 多趟排序

挖坑法

前后指针版本(双指针)

快排的优化

三数取中

小区间优化

快速排序之非递归


快排的介绍

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

基本思想:

  1. 选择一个基准元素(pivot)。可以选择数据集中的任意一个元素作为基准,一般情况下选择第一个元素或者最后一个元素作为基准。

  2. 将数据集中的其他元素按照与基准元素的大小关系进行分区。将小于基准的元素放在基准的左边,将大于基准的元素放在基准的右边。这个过程称为分区操作。

  3. 对分区后的左右两个子集,分别递归地应用相同的方法,继续进行分区操作,直到每个子集只包含一个元素或为空。

  4. 最后,将所有子集按照顺序拼接起来,就得到了一个有序的数据集。

快速排序的基本框架:

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

接下来我们来开始进入快速排序!!!

hoare版本

这是最原始的版本,也是第一代创始人hoare发明的版本。我们先来进行快排的单趟交换过程:

单趟排序 

这张图片就是hoare版本的全部过程,我们先来分析第一次的交换过程。

单趟过程:首先选取数组最左边的元素标记为key,然后创建两个指针left与right分布到数组的两边。right的指针先走,寻找比key小的元素,找不到就往前走,找到就停止。然后left后走,寻找比key值大的元素,找到后两个值交换即可。

然后我们来看一下单趟的代码:

while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}

代码易错点: 1. 假设数组为上图数组,right寻找比key小的值,right指针就会一直往前寻找,就会导致数组越界。所以我们必须在while循环条件中加入(left<right)。

2.我们在while循环中可以看到第二个判断条件,是否加等于号。必须要加等于号,否则当left与right指向的元素相等时,交换后的数组继续进行循环就会出现死循环。下图为特殊情况图:

 多趟排序

当上面的单趟走完后,我们会发现,keyi左边的全是小于a[keyi]的,右边全是大于a[keyi]的。

当交换完所有的数后,left指针与right指针就会相遇,我们将相遇的地方与key元素交换即可完成一次排序。现在的数组相对于刚才已经变得有序了一点。我们可以分割重复这个思想,就可以完成整个排序。

分割思路:

类似于二叉树思想,我们可以将其进行风格key左边的都小于key,key右边的都大于key。将数组从key中间一分为二。

 将数组划分为[begin,keyi-1],  keyi,   [keyi+1,end],然后与上面单趟排序思想一致,继续进行递归排序。递归结束条件:当begin == end  或者是 数组错误(begin>end)时,则为结束条件。

那为什么相遇位置一定比key小呢?怎么确定的?

这就要多亏与right指针先走。

相遇情况:

1.R动L不动,去跟L相遇。 相遇位置一定是R的位置,L和R在上一轮交换过,交换以后L的位置的值一定比key小。

2.L动R不动,去跟R相遇。相遇位置一定是L的位置,L的位置比key小。

完整代码如下:

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int PartSort(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			--right;
		}
		while (left < right && a[left] <= a[keyi])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int keyi = PartSort3(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

挖坑法

对于hoare版本,许多人会觉得不好理解,觉得非常抽像。现在我们来讲一种更加直观的方法,虽然算法思路一样,但是更容易理解。

我们先来看一下思路流程图:

算法思路:

将最左边设置为key并且设置为坑位。什么是坑位,就是将其看成空位。所以我们就要将key对应的元素值进行标记存储防止被覆盖。然后创建两个指针left与right,与key值进行比较。right先走寻找比key小的值,找到后将其填入坑中。然后right指向的数组成为新的坑位。right停止left寻找比key大的元素,找到后将其移动到新坑位,这样依次类推即可。

大致思路与hoare版本相同,唯一不同点就是加入了”坑位“,我们可以看作数组的填空。

在这里我们就不过多赘述了,直接展示源代码:

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int PartSort2(int* a, int left, int right)
{
	int hoop = a[left];
	while (left < right)
	{
		while (left < right && a[right] >= hoop)
		{
			right--;
		}
		a[left] = a[right];
		while (left < right && a[left] <= hoop)
		{
			left++;
		}
		a[right] = a[left];
	}
	a[left] = hoop;
	return left;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int keyi = PartSort3(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

 这里我们还是使用了递归的思想进行操作。

前后指针版本(双指针)

前后指针的思路与前两个有很大的区别,也是目前最流行的写法。我们还是从图中了解一下其中原理。

算法思路:

创建两个指针变量,一个prev指向最开始节点,cur指针指向prev的后方,也就是cur = prev + 1。cur去寻找比key大的值,如果cur指向比key小,prev先加1然后进行交换(在前面如果没遇到比key大的值,就相当于自己给自己赋值)。当遇到比key大的值时,prev不动,cur++。直到cur>=数组长度end即可停止。当循环结束,我们将prev指向的位置与key交换,这样单趟思路就搞定了。

多趟思路就是进行拆分递归,下来就是代码展示:

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[keyi]);
	return prev;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int keyi = PartSort3(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

注意:在遍历时,判断++prev != cur可以去掉,这样做是防止自己给自己赋值,优化程序。 

快排的优化

三数取中

快排的速度非常快,一般情况下为O(nlogn),但是在特殊情况下速度就会特别慢,达到了O(n^2).那是什么情况下会出现这种场景呢?
key是取决于速度快慢的关键,当key值取到的值为数组元素的中间值时就非常理想。 

但是如果这个数组是比较有序的,每次取到的key值都是最大值或最小值,那么指针在寻找需要的元素时基本都会直接遍历整个数组。 所以我们就需要进行”三数取中“,我们知道数组的头尾,就可以算出数组的中间值,我们将这三个位置的数组进行比较,取最中间的值作为key即可。

这样做就可以优化快排最坏的情况,让时间复杂的基本维持到O(nlogn)。

代码展示:

int GetMidi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
			return right;
	}
	else // a[left] > a[mid]
	{
		if (a[left] < a[right])
		{
			return left;
		}
		else if (a[right] > a[mid])
		{
			return right;
		}
		else
			return mid;
	}
}

小区间优化

对于大量的数,我们可以使用快速排序,但是一些小区间的数使用快排反而没有插入排序……效率高, 所以我们进行小区间优化。如果数组大小小于10个,我们就使用插入排序即可,如果大于10个数就可以使用快排进行。

void QuickSort1(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	if ((end - begin + 1) > 10)
	{
		int keyi = PartSort3(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	else
	{
		InsertSort(a + begin, end - begin + 1);
	}
}

插入排序的源代码在这寻找:插入排序之希尔排序——【数据结构】-CSDN博客

使用插入排序区间不一定是从头开始,所以在传参时要加begin。

快速排序之非递归

因为函数递归实在栈上开辟空间的,而栈上的空间只有4G左右,如果递归层数太深就会导致栈溢出。而非递归就是在堆上开辟空间,堆的空间非常大,我们有足够的空间去开辟,所以我们就使用非递归来完成。

算法思路:

我们利用栈先进后出的优势,存储其前后区间即可。假设数组区间为0~9,我们先存储右边再存储左边(因为取值时就可以先取左边再取出右边),然后进行函数调用排序将区间划分为[left , keyi - 1] keyi [ keyi +  1 , right ] ,再将区间的值进行右左入栈,然后再取两个值作为区间进行函数调用以此类推,直到栈为空为止。

我们将函数的递归转换成用栈来取区间。 

注意:一定要先存储右边,再存储左边 

代码展示:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;

void STInit(ST* ps);
void STDestroy(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);
int STSize(ST* ps);
bool STEmpty(ST* ps);
STDataType STTop(ST* ps);
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
void STInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->capacity = 0;
	ps->top = 0;
}
void STDestroy(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = newCapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}
void STPop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);

	--ps->top;
}
int STSize(ST* ps)
{
	assert(ps);

	return ps->top;
}
bool STEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}
STDataType STTop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);

	return ps->a[ps->top - 1];
}
void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	STInit(&st);
	STPush(&st, end);
	STPush(&st, begin);
	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st);
		int right = STTop(&st);
		STPop(&st);

		int keyi = PartSort3(a, left, right);
		if (keyi + 1 < right)
		{
			STPush(&st, right);
			STPush(&st, keyi + 1);
		}
		if (left < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, left);
		}
	}

	STDestroy(&st);
}

以上是本次快速排序全部内容,对大家有帮助的麻烦三连支持一下!!!

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

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

相关文章

vs2015 设置字体

Source Code Pro一款堪称完美的编程字体_source code字体-CSDN博客

zkPoT:基于机器学习模型训练的ZKP

1. 引言 Sanjam Garg等人2023年论文 Experimenting with Zero-Knowledge Proofs of Training 中&#xff0c;所设计的zkPoT&#xff08;zero-knowledge proof of training&#xff09;协议&#xff1a; 为streaming-friendly的。所需RAM与训练电路size不呈比例。结合了MPC-in…

Kaggle - LLM Science Exam上:赛事概述、数据收集、BERT Baseline

文章目录 一、赛事概述1.1 OpenBookQA Dataset1.2 比赛背景1.3 评估方法和代码要求1.4 比赛数据集1.5 优秀notebook 二、BERT Baseline2.1 数据预处理2.2 定义data_collator2.3 加载模型&#xff0c;配置trainer并训练2.4 预测结果并提交2.5 相关优化 前言&#xff1a;国庆期间…

基于Java的手机在线商城设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

竞赛 机器视觉人体跌倒检测系统 - opencv python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 机器视觉人体跌倒检测系统 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&…

【软考】4.3 模式分解/事务并发/封锁协议

《模式分解》 保持函数依赖分解 无损分解 分解后可还原关系模式充分必要条件&#xff1a;R1∩R2 —>&#xff08;R1 - R2&#xff09; || R1∩R2—>&#xff08;R2 - R1&#xff09;“R1 - R2” 即在R1筛选除了R2的元素 《并发控制》 事务的四种特性&#xff1a;原子…

UE4.27.2 自定义 PrimitiveComponent 出现的问题

目录 CreatePrimitiveUniformBufferImmediateFLocalVertexFactory 默认构造函数GetTypeHashENQUEUE_RENDER_COMMANDnull resource entry in uniform buffer parameters FLocalVertexFactory 在看大象无形&#xff0c;其中关于静态物体网络绘制的代码出错的 bug 我也搞了一会………

RabbitMQ-发布订阅模式和路由模式

接上文 RabbitMQ-工作队列 1 发布订阅模式 将之前的配置类内容都替换掉 Bean("fanoutExchange")public Exchange exchange(){//注意这里是fanoutExchangereturn ExchangeBuilder.fanoutExchange("amq.fanout").build();}Bean("yydsQueue1")publ…

全栈开发笔记2:项目部署上线的三种方式

文章目录 最原始的方式宝塔Docker 部署其他 本文为编程导航实战项目学习笔记。 项目部署的三种方式&#xff1a; 最原始方式✅ yum 手动安装 jdk mysql tomcat nginx打包前端项目&#xff0c;放到某个目录&#xff0c;修改 nginx 配置修改线上的 mysql 配置&#xff0c;打包 j…

MySQL 多表关联查询优化实践和原理解析

目录 一、前言二、表数据准备三、表关联查询原理和两种算法3.1、研究关联查询算法必备知识点3.2、嵌套循环连接 Nested-Loop Join(NLJ) 算法3.3、基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法3.4、被驱动表的关联字段没索引为什么要选择使用 BNL 算法而不使用 Nested…

C++_pen_静态与常量

成员 常成员、常对象&#xff08;C推荐使用 const 而不用#define,mutable&#xff09; const 数据成员只在某个对象生存周期内是常量&#xff0c;而对于整个类而言却是可变的&#xff08;static除外&#xff09; 1.常数据成员&#xff08;构造函数初始化表赋值&#xff09; c…

华为云云耀云服务器L实例评测|安装搭建学生成绩管理系统

1.前言概述 华为云耀云服务器L实例是新一代开箱即用、面向中小企业和开发者打造的全新轻量应用云服务器。多种产品规格&#xff0c;满足您对成本、性能及技术创新的诉求。云耀云服务器L实例提供丰富严选的应用镜像&#xff0c;实现应用一键部署&#xff0c;助力客户便捷高效的在…

【重拾C语言】四、循环程序设计典例整理(最大公因数、阶乘求和、正整数翻译、打印字符方阵、斐波那契数列……)

目录 前言 四、循环程序设计 4.3 程序设计实例 4.3.1 求两数最大公因数 4.3.2 阶乘求和 4.3.3 正整数翻译 4.3.4 打印字符方阵 4.3.5 百钱百鸡问题 4.3.6 斐波那契数列 4.3.7 迭代法解方程 前言 ChatGPT C语言是一种通用的、过程式的计算机编程语言&#xff0c;由贝…

SQL与关系数据库基本操作

SQL与关系数据库基本操作 文章目录 第一节 SQL概述一、SQL的发展二、SQL的特点三、SQL的组成 第二节 MySQL预备知识一、MySQL使用基础二、MySQL中的SQL1、常量&#xff08;1&#xff09;字符串常量&#xff08;2&#xff09;数值常量&#xff08;3&#xff09;十六进制常量&…

JAVA学习(3)-全网最详细~

回顾 昨天学了 Java 中的数据类型-整型 int - integer,以及什么是标识符identifier和它的命名规则,什么是保留字(reserved word key word),最后还谈到了Java变量包括局部变量和成员变量(在类内部,方法外部的变量),变量必须要初始化,否则会报错.如果有遗忘或者是感兴趣的小伙伴…

ESLint自动修复代码规范错误

基于 vscode 插件 ESLint 高亮错误&#xff0c;并通过配置 自动 帮助我们修复错误 在设置中 settings.json添加这段代码就自动修复错误 // 当保存的时候&#xff0c;eslint自动帮我们修复错误 "editor.codeActionsOnSave": { "source.fixAll": true }, /…

免费app签名分发平台应用cdn分发平台为什么会免费?商业分析他的盈利模式

近年来&#xff0c;随着移动应用的迅速发展&#xff0c;免费app签名分发平台和应用CDN分发平台日益受到开发者和用户的关注。本报告旨在分析这些平台的商业模式&#xff0c;探讨其利润点、营销点以及所采取的优势。 一、商业模式分析&#xff1a; 广告收入&#xff1a; 免费a…

Hive【Hive(五)函数-高级聚合函数、炸裂函数】

高级聚合函数 多进一出&#xff08;多行输入&#xff0c;一个输出&#xff09; 普通聚合函数&#xff1a;count、sum ... 1&#xff09;collect_list&#xff08;&#xff09;&#xff1a;收集并形成 list 集合&#xff0c;结果不去重 select sex,collect_list(job) from e…

机器学习笔记(二)

过拟合 如下图左边,模型出现了过拟合现象 为了解决过拟合现象, 其中一个做法是多收集数据,如右图。 第二种做法是减少模型的特征数量,即x 第三种做法是正则化 正则化就是减少x前面的参数 w的数值, 不用消除x 正则化的梯度下降如下, 因为只是缩小了w的值,而 b的值保持不变 …

项目测试练习

项目背景项目功能测试计划Bug总结升级自动化测试正常登录流程 项目背景 1&#xff1a;博客之站系统是采用前后端分离的方式来实现&#xff1b;使用MySQL、Redis数据库储存相关数据&#xff1b;同时部署到云服务器上。 2&#xff1a;包含注册页、登录页、博客列表页、个人列表页…