【排序】快速排序(递归和非递归)

news2024/10/7 10:17:01

快速排序

    • 前言
    • 图解
    • 大致思路
      • 对于hoare版本
      • 对于挖坑法
      • 对于前后指针法
    • 实现方法
      • 递归
      • 非递归
    • 快排的优化(基于递归的优化)
      • 三数取中法
      • 小区间优化
    • 时间复杂度和空间复杂度

在这里插入图片描述

前言

快速排序,听名字就比较霸道,效率根名字一样,非常的快,但也还是O(N * logN)级别的。

我所学到的快排有三个版本:

  1. 原始版本hoare版本,也就是hoare这个人发明的
  2. 基于hoare版本改进的版本,挖坑法(还有别的叫法,我这里就说成挖坑法了)。
  3. 是跟上面两种方法不一样的方法,前后指针法。

这里讲之前跟我前面的博客一样,先给图解。

图解

首先hoare版本的:
在这里插入图片描述

挖坑法
在这里插入图片描述

前后指针法
在这里插入图片描述

大致思路

首先,快排讲的是一个分治的思想,什么叫分治呢,根二叉树的前序遍历一样,先处理根,再处理左树和右树。那么快排也就是这样,先处理当前的(定关键字key的位置),然后再处理左半边的,后右半边的。

那这里的关键字key是什么呢?其实就是每趟排序的时候,首先选出来的一个数(一般选择最左端或者最右端),这个数决定了你是从左往右排还是从右往左排,什么意思呢?

对于hoare版本

当你的key选取在最左端时,就先让R(right)先走,R找小,找到了之后再让L(L找大)走。

当你的key选取在最右端时,就先让L(left)先走,L找大,找到了之后再让R(R找小)走。

这样能够保证L能和R相遇

下面以key在最左端为例
此时会出现以下情况:

当R找到小的了,L也找到大的了,就让两个位置上的数交换。再让R走,找小,找到了停,L找大,重复上述步骤。

当R和L相遇时,就将key位置上的数与相遇位置的数交换位置。本趟排序结束。

然后就开始递归,递归排序相遇位置的左边和相遇位置的右边。

对于挖坑法

当你的key选取在最左端时,就先让R(right)先走,R找小

当你的key选取在最右端时,就先让L(left)先走,L找大

此时key位置就是坑的位置
这样能够保证L能和R相遇,根hoare版本一样

下面以key在最左端为例
此时会出现以下情况:

当R找到小的了,就直接把R的值放到原坑中,R位置作为新的坑,再让L先走,找大,找到大的,将L的值放到原坑中,L位置作为新坑,不断重复上述步骤。

当R和L相遇,相遇位置的数放到原坑中,相遇位置作为新坑,将key的值放到新坑中。本趟排序结束

然后就开始递归,递归排序相遇位置的左边和相遇位置的右边。根hoare相似。

对于前后指针法

当你的key选取在最左端时,就让cur从左往右走

当你的key选取在最右端时,就让cur从右往左走

下面以key在最左端为例
此时会出现以下情况:

当cur位置上的数小于key时,prev++,交换cur和prev上的数,cur++

当cur位置上的数大于等于key时,prev什么也不做,cur++
直至cur越界,再交换key和prev位置上的数。本趟排序结束。

记下cur越界时prev的位置。
然后就开始递归,递归排序该位置的左和该位置的右。

实现方法

递归

hoare版本的单趟

//hoare版本
int PartSort1(int* a, int left, int right)
{
	int keyi = left;

	while (left < right)
	{
		//right找小				等于的时候也得走,不然程序会崩掉,这里是个易错点
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

		//left找大				等于的时候也得走,不然程序会崩掉,这里是个易错点
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		//找到了就交换,或者二者相遇,同一个位置交换不交换都是一样的
		swap(&a[left], &a[right]);
	}

	//相遇
	swap(&a[left], &a[keyi]);
	keyi = left;

	//返回相遇的位置,keyi、left和right都可以
	return keyi;
}

挖坑法的单趟

//挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];

	while (left < right)
	{
		//right找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		//找到了把数放到原坑,right做新坑,相遇的时候也不影响
		a[left] = a[right];

		//left找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		//找到了把数放到原坑,left做新坑,相遇的时候也不影响
		a[right] = a[left];
	}

	//相遇的位置放key
	a[left] = key;

	//返回相遇的位置
	return left;
}

前后指针法的单趟

//前后指针法
int PartSort3(int* a, int left, int right)
{
	int keyi = left;
	int previ = left;
	int curi = previ + 1;

	while (curi <= right)
	{
		//小了并且previ+1不等于curi再交换
		if (a[curi] < a[keyi] && ++prev != curi))
			swap(&a[previ], &a[curi]);
		
		curi++;
	}

	swap(&a[previ], &a[keyi]);

	return previ;
}

全趟排序
上面三个的代码只是单趟的排序,还要放到整体的排序中:

//快排
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

 	//hoare
	//int keyi = PartSort1(a, begin, end);
	
	//挖坑法
	//int keyi = PartSort2(a, begin, end);
	
	//前后指针法
	int keyi = PartSort3(a, begin, end);


	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

上面的就是三种方法的代码了。

非递归

非递归的话,得要用到栈。

图解:
在这里插入图片描述

代码实现:

//快排非递归
//快排非递归
void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);

	//初始情况下就入左右两端
	//先入右边再入左边
	StackPush(&st, end);
	StackPush(&st, begin);

	//栈不为空才继续循环
	while (!StackEmpty(&st))
	{
		//取左
		int lefti = StackTop(&st);
		StackPop(&st);

		//取右
		int righti = StackTop(&st);
		StackPop(&st);
		
		//得关键字位置
		int keyi = PartSort3(a, lefti, righti);

		//区间存在才入栈
		if (lefti < keyi - 1)
		{
			//先入右边再入左边
			StackPush(&st, keyi - 1);
			StackPush(&st, lefti);
		}

		//区间存在才入栈
		if (righti > keyi + 1)
		{
			//先入右边再入左边
			StackPush(&st, righti);
			StackPush(&st, keyi + 1);
		}

	}

}

快排的优化(基于递归的优化)

三数取中法

上面递归的三个单趟排序其实是可以再优化一下的(以升序为例),因为当数组有序的时候每次选择key时,选出来的都是最小值,这时候key最终放的位置就是最左端,而递归的时候需要把key的左端和key的右端继续排,然而此时key处于最左端,那么递归排别的段的时候就只能排key的右端了,这个时候就相当于是排一次去处一个数,那么就变成了N,N-1,N-2,…2,1,时间复杂度就会变成O(N^2),就相当于是冒泡排序了。这样的话快排就排的不快了。
在这里插入图片描述

但我们所希望的是这样的:
在这里插入图片描述

怎么做呢?
虽然我们不能取到所有有序的数中最中间的数,但是我们可以取到所有无序的数中位于最中间的数,每次排序前把这个数和左右两端的数比较以下,求出值为三者中不大也不小的那个数,然后再把这个数放到最前面,就可以实现类似于图二的方法,虽然做不到完全二分,但是也比图一强。

三数取中

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

然后把这个函数放在每一个单趟排序函数的最前面就行。

hoare版本的优化

//hoare版本
int PartSort1(int* a, int left, int right)
{
	//三数取中优化
	int mid = GetMid(a, left, right);
	swap(&a[left], &a[mid]);

	int keyi = left;

	while (left < right)
	{
		//right找小				等于的时候也得走,不然程序会崩掉,这里是个易错点
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

		//left找大				等于的时候也得走,不然程序会崩掉,这里是个易错点
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		//找到了就交换,或者二者相遇,同一个位置交换不交换都是一样的
		swap(&a[left], &a[right]);
	}

	//相遇
	swap(&a[left], &a[keyi]);
	keyi = left;

	//返回相遇的位置,keyi、left和right都可以
	return keyi;
}

挖坑法的优化

//挖坑法
int PartSort2(int* a, int left, int right)
{
	//三数取中优化
	int mid = GetMid(a, left, right);
	swap(&a[left], &a[mid]);

	int key = a[left];

	while (left < right)
	{
		//right找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		//找到了把数放到原坑,right做新坑,相遇的时候也不影响
		a[left] = a[right];

		//left找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		//找到了把数放到原坑,left做新坑,相遇的时候也不影响
		a[right] = a[left];
	}

	//相遇的位置放key
	a[left] = key;

	//返回相遇的位置
	return left;
}

前后指针法的优化

//前后指针法
int PartSort3(int* a, int left, int right)
{
	//三数取中优化
	int mid = GetMid(a, left, right);
	swap(&a[left], &a[mid]);

	int keyi = left;
	int previ = left;
	int curi = previ + 1;

	while (curi <= right)
	{
		//小了并且previ+1不等于curi再交换
		if (a[curi] < a[keyi] && ((previ + 1) != curi))
			swap(&a[previ], &a[curi]);
		
		curi++;
	}

	swap(&a[previ], &a[keyi]);

	return previ;
}

小区间优化

小区间优化的作用在于当数据量比较大时,递归可能会导致栈溢出。

而快排递归的时候是类似于二叉树的,所以最后一层递归所开辟的空间基本上要占用总空间的一半(完全二分的情况下),这个时候如果我们控制一下最后一层,不让这一层的数递归排序,就可以很好地避免栈溢出的问题。

怎么做呢。很简单,判断一下当递归某一区间段的时候(右端下标 - 左端下标 < X(X可取10~20之间的数)),这时候就可以用一下别的排序,而10~20的数据量还是很小的,所以不需要用比较复杂的排序,用一个N^2级别的排序就可以了,而N^2级别的排序中插入排序就是最好的,所以这时候替换成插入排序就可以了。

小区间优化

//快排
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	else if (end - begin + 1 <= 10)
	{
		//小区间优化
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		//int keyi = PartSort1(a, begin, end);
		//int keyi = PartSort2(a, begin, end);
		int keyi = PartSort3(a, begin, end);


		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	
}

时间复杂度和空间复杂度

我这里之说优化之后的。

时间复杂度就是O(N* logN),空间复杂度是O(logN)。

就说一下空间复杂度,因为快排是基于分治思想的,而且递归排序的过程类似于二叉树的前序遍历,所以在栈上开辟空间时就要不断的堆栈,然后当函数递归达到最大深度(二叉树的最下面一层)的时候,也就是logN,函数返回,最深层的函数系统就会自动回收空间,这时候就不会再开辟更大的空间了,而且开辟的空间是慢慢的缩小再增大,再减少,再增大。。。直到排序结束。所以空间开辟的时候最大也就开辟logN大小的空间。

到此结束。。。

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

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

相关文章

winForm登录页面知识点

先看界面 引用知识 控件&#xff1a;label,Textbox,button还有各自的属性和事件Trim()方法的使用&#xff0c;IsNullOrEmpty()方法的使用&#xff0c;Show()方法的使用 Label 属性 NameText:设置或获取文本信息image:显示图像ImageList:图像集控件SizeTag:与控件相关的自定…

Gorm的关联模型

Belongs To 将一个模型与另一个模型建立一对一的关系 例如&#xff1a;一张银行卡只能分配给一个用户&#xff0c;在User结构体里面创建一个CreditCardId外键关系&#xff0c;然后在User结构体里面嵌套一个CreditCard结构体 // Belongs To // 用户 type User struct {gorm.M…

十个超级好用的Javascript技巧

概览&#xff1a;在实际的开发工作过程中&#xff0c;积累了一些常见又超级好用的Javascript技巧和代码片段&#xff0c;包括整理的其他大神的JS使用技巧&#xff0c;今天筛选了10个&#xff0c;以供大家参考。 动态加载JS文件 在一些特殊的场景下&#xff0c;特别是一些库和…

自己动手做chatgpt:解析gpt底层模型transformer的输入处理

前面我们完成了一些基本概念&#xff0c;如果你对深度学习的基本原理还不了解&#xff0c;你可以通过这里获得更多信息&#xff0c;由于深度学习的教程汗牛充栋&#xff0c;因此我在这里不会重复&#xff0c;而是集中精力到chatgpt模型原理的分析&#xff0c;实现和实践上。Cha…

初识C语言————3

博主这篇文章浅谈一下自己对函数和数组的理解。之后会详细说明。 文章目录 一、函数 二、数组 1、数组定义 2、数组的下标 3、数组的使用 一、函数 函数可以理解为一个模块的代码&#xff0c;完成一个独立的功能。 #include <stdio.h> int main() {int num1 0;int num…

ASEMI代理ADM3051CRZ-REEL7原装ADI车规级ADM3051CRZ-REEL7

编辑&#xff1a;ll ASEMI代理ADM3051CRZ-REEL7原装ADI车规级ADM3051CRZ-REEL7 型号&#xff1a;ADM3051CRZ-REEL7 品牌&#xff1a;ADI/亚德诺 封装&#xff1a;SOIC-8 批号&#xff1a;2023 引脚数量&#xff1a;8 安装类型&#xff1a;表面贴装型 ADM3051CRZ-REEL7汽…

【Python】读取rdata类型数据转为csv excel格式文件, 无需安装r语言基于pyreadr+pandas实现数据分析(保姆级注释)

目录 环境配置取得数据名 datas.keys()取得pandas的DataFrame类型数据一些数据分析例程供入门同学学习转化为csv excel格式所有数据 转化为csv取前面100行数据 快速测试能否转化csv取前面100行数据 快速测试能否转化xlsx 完整例程总结 欢迎关注 『Python』 系列&#xff0c;持续…

简述AutoGPT原理(提示词)

启动时需要设置三个项目&#xff1a;机器人名字、设定给机器人的角色、要完成的目标。 根据你的设定利用ChatGPT进行下一步的抉择&#xff0c;具体的&#xff0c;实际上归功于提示词&#xff1a; 下面这段提示词在干什么呢&#xff1f; 将设定的名字、角色、目标告诉ChatGPT&…

【U8+】用友U8+对账不平案例及方法总结

【问题需求】 在使用用友U8软件过程中&#xff0c;由于软件涉及到多方面的对账。 所以经常会遇到期初或结账时对账不平。 【经验分享】 在众多对账中&#xff0c; 只有当【总账上下级】&#xff08;即&#xff1a;总账与明细账对账&#xff09;不平的时候&#xff0c; 软件才…

春秋云境:CVE-2022-26965(后台RCE)

目录 一、题目 二、 利用cms主题构造木马 一、题目 介绍&#xff1a; Pluck-CMS-Pluck-4.7.16 后台RCE 进入题目&#xff1a; cms页面 点击admin进行登录&#xff1a; 弱口令admin登录&#xff1a; 成功登录进去&#xff1a; 国产化一下&#xff1a; 选项---选择主题 点击…

漏洞分析丨cve20144113

一、漏洞简述 Microsoft Windows下的 win32k.sys是Windows子系统的内核部分&#xff0c;是一个内核模式设备驱动程序&#xff0c;它包含有窗口管理器、后者控制窗口显示和管理屏幕输出等。如果Windows内核模式驱动程序不正确地处理内存中的对象&#xff0c;则存在一个特权提升…

js对象案例练习

个人名片&#xff1a; &#x1f60a;作者简介&#xff1a;一名大一在校生&#xff0c;web前端开发专业 &#x1f921; 个人主页&#xff1a;python学不会123 &#x1f43c;座右铭&#xff1a;懒惰受到的惩罚不仅仅是自己的失败&#xff0c;还有别人的成功。 &#x1f385;**学习…

SQL Server的日志传送

日志传送和复制 一、前言二、相关术语和定义三、日志传送和复制3.1、在主数据库丢失时从辅助数据库进行复制的要求和过程3.2、使用事务复制进行日志传送3.3、使用合并复制进行日志传送 一、前言 日志传送允许您自动将事务日志备份从主服务器实例上的主数据库发送到单独辅助服务…

思维导图手撕MyBatis源码

文章目录 前置准备通过类加载器读取配置文件流创建sqlSessionFactory建造者模式的使用 打开SqlSession获取Mapper接口对象执行Mapper接口方法 前置准备 既然要读MyBatis的源码&#xff0c;那么我们就要先弄清楚MyBatis的入口在哪。这里我们直接写一个标准的MyBatis使用程序&am…

Node 【Buffer 与 Stream】

文章目录 &#x1f31f;前言&#x1f31f;Buffer&#x1f31f; Buffer结构&#x1f31f; 什么时候用Buffer&#x1f31f; Buffer的转换&#x1f31f; Buffer使用&#x1f31f; 创建Buffer&#x1f31f; 字符串转Buffer&#x1f31f; Buffer转字符串&#x1f31f; 拼接Buffer&am…

微信小程序登录注册页面

// login.js // 获取应用实例 var app getApp() var api require("../../utils/api.js")Page({data: {motto: zhenbei V1.0.0,userInfo: {},hasUserInfo: false,disabled: true,btnstate: default,username: ,password: ,canIUse: wx.canIUse(button.open-type.get…

UE4/5 C++网络服务器编程纪录【零】--准备篇

前言 之前利用业余时间重新复习UE4/5的C开发&#xff0c;闲来无事做了个基于独立服务器的多人在线&#xff08;目前限定客户数量是20人以内&#xff09;DEMO&#xff0c;核心功能在我之前发的B站视频里面有&#xff0c;战斗、动作、交互以及场景演示都有了&#xff0c;有朋友看…

Linux使用:环境变量指南和CPU和GPU利用情况查看

Linux使用&#xff1a;环境变量指南和CPU和GPU利用情况查看 Linux环境变量初始化与对应文件的生效顺序Linux的变量种类设置环境变量直接运行export命令定义变量修改系统环境变量修改用户环境变量修改环境变量配置文件 环境配置文件的区别profile、 bashrc、.bash_profile、 .ba…

函数(有点难,要注重实战)

目录 1. 函数是什么2. C语言中函数的分类2.1 库函数2.1.1 如何学会使用库函数&#xff1f; 2.2 自定义函数 3. 函数的参数3.1 实际参数&#xff08;实参&#xff09;&#xff1a;3.2 形式参数&#xff08;形参&#xff09;&#xff1a; 4. 函数的调用&#xff1a;4.1 传值调用4…

Spark SQL实战(08)-整合Hive

1 整合原理及使用 Apache Spark 是一个快速、可扩展的分布式计算引擎&#xff0c;而 Hive 则是一个数据仓库工具&#xff0c;它提供了数据存储和查询功能。在 Spark 中使用 Hive 可以提高数据处理和查询的效率。 场景 历史原因积累下来的&#xff0c;很多数据原先是采用Hive…