(排序6)快速排序(小区间优化,非递归实现)

news2024/10/7 16:17:26

TIPS

  1. 快速排序本质上是一个分治递归的一个排序。
  2. 快速排序的时间复杂度是NlogN,这是在理想的情况之下,但是它最坏可以到达N^2。决定快速排序的效率是在单趟排序之后这个key最终落在的位置,越落在中间就越接近二分,越接近2分就越接近满二叉树,越接近二叉树它的深度就更加均匀。深度更加均匀的话,不仅可以防止栈溢出,减少递归的层次,效率上也有提高。
  3. 因此,由此说来,选key是十分关键的。不能一直把数组当中最左边的那个字作为关键值。选key有三数取中与随机数优化版,推荐三数取中。特别是在要排列的那个数组是有序的情况之下,如果用三数取中优化法,那么就可以确保最中间的那个值就是key。
  4. 有了三数取中选key优化,这个快排它的递归深度相对来说都不会特别深。
  5. 之前有关于快排代码回顾:
//交换函数
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//Hoare大佬原始版本
void QuickSort1(int* arr, int left , int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])
		{
			right--;
		}
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
		Swap(arr + left, arr + right);
	}
	Swap(arr + left, arr + keyi);
	keyi = left;
	QuickSort1(arr, begin, keyi - 1);
	QuickSort1(arr, keyi + 1, end);
}

//Hoare大佬原始版本+随机数选key优化
void QuickSort2(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int randi = left + rand() % (right - left);
	Swap(arr + randi, arr + left);
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])
		{
			right--;
		}
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
		Swap(arr + left, arr + right);
	}
	Swap(arr + left, arr + keyi);
	keyi = left;
	QuickSort2(arr, begin, keyi - 1);
	QuickSort2(arr, keyi + 1, end);
}

//Hoare大佬原始版本+三数取中选key优化
int GetMidNumi(int* arr, int left, int right)
{
	int mid = (left + right) / 2;
	if (arr[mid] < arr[right])
	{
		if (arr[left] < arr[mid])
		{
			return mid;
		}
		else  //arr[left]>arr[mid]
		{
			return arr[left] < arr[right] ? left : right;
		}
	}
	else
	{
		if (arr[right] > arr[left])
		{
			return right;
		}
		else
		{
			return arr[left] < arr[mid] ? left : mid;
		}
	}
}
void QuickSort3(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int midi = GetMidNumi(arr, left, right);
	Swap(arr + midi, arr + left);
	int keyi = left;
	while (left < right)
	{
		while (left < right && arr[right] >= arr[keyi])
		{
			right--;
		}
		while (left < right && arr[left] <= arr[keyi])
		{
			left++;
		}
		Swap(arr + left, arr + right);
	}
	Swap(arr + left, arr + keyi);
	keyi = left;
	QuickSort3(arr, begin, keyi - 1);
	QuickSort3(arr, keyi + 1, end);
}

//挖坑法+默认三数取中选key优化
void QuickSort4(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int midi = GetMidNumi(arr, left, right);
	Swap(arr + midi, arr + left);
	int key = arr[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && arr[right] >= key)
		{
			right--;
		}
		arr[hole] = arr[right];
		hole = right;
		while (left < right && arr[left] <= key)
		{
			left++;
		}
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;
	QuickSort4(arr, begin, hole - 1);
	QuickSort4(arr, hole + 1, end);
}

//前后指针法+默认三数取中选key优化
void QuickSort5(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int begin = left;
	int end = right;
	int midi = GetMidNumi(arr, left, right);
	Swap(arr + midi, arr + left);
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (arr[cur] < arr[keyi])
		{
			prev++;
			Swap(arr + prev, arr + cur);
		}
		cur++;
	}
	Swap(arr + prev, arr + keyi);
	keyi = prev;
	QuickSort5(arr, begin, keyi - 1);
	QuickSort5(arr, keyi + 1, end);
}

快速排序的小区间优化(假设我原先用前后指针法)

  1. 快速排序实际上还可以继续优化:我们知道快速排序实际上是递归分治这么进行下去的,尤其是当单趟排序完成之后这个key如果一直落在中间的话,那么如果你把每一个递归都给它放出来的话,你会发现它就是一个接近于满二叉树。
  2. 这时候你想象一下,假设我有1万个数,在第一次递归的时候被分成5000与5000,然后再被分成2500,2500。但随着不断的这么进行递归下去,尤其是在最后面末流,比如说递归的区间范围是五个数,然后把五个数这个区间在递归分治分成两个数与两个数,然后对于两个数的区间也在进行递归分治划分,就会发现有点大题小做,还挺麻烦的。你会发现为了让这五个数有序,我们实际上居然递归了六次。
  3. 在每一次分治递归的时候,你这个QuickSort的使命是让left与right这个区间里面的数有序,你用什么方法有序的话都可以,你可以把它继续切分成子问题,然后这么进行下去,让这个区间有序;可以通过直接的方法让他去有序。
  4. 因此也就是说当这个需要排序的区间缩小到一定程度的时候,就可以不再考虑用递归的方式继续分成子问题,然后使得这个区间有序。而是用别的排序方法。
  5. 插入排序是最好的,如果用希尔排序的话,希尔排序针对插入排序的优势主要体现在当数据量很大的时候,这时候我的gap取得很大,就可以让那些大的数尽快的跳到后面。但现在本身的逻辑前提就是说这个区间已经小到一定程度,怎么杀鸡焉用牛刀,根本用不着希尔排序。然后选择排序与冒泡排序的话虽然他们的时间复杂度都与直接插入排序一样N^2,但是之前就有过比较,这两种排序是无法与直接插入排序最终抗衡的。因为对于直接插入排序而言,只要在这个数据段当中,有几部分小段小段是有序的,对于他的优势就非常大。对于堆排也麻烦还要建堆。库函数也是这么优化的。
  6. 然后以理想化的方式来看待,就以一颗满二叉树来看待,如果说把最后一层的递归给他全部消灭掉的话,能够使得总的函数调用次数减少掉一半。
  7. 比如说当整个待排序的数组里面的元素个数(right - left + 1)小于等于十个的时候,这时候就不进行继续递归分治的玩法,还是采用直接插入排序。想一想这样的话就能够减少掉往下面的三层递归(以理想化的满二叉树为例),那么算下来可以减少50%+25%+12.5%即87.5%的递归(以理想化的满二叉树为例),效果贼棒
  8. 然后进行直接插入排序的时候,那个函数传参又是一个很坑的地方。insertSort的第一个参数应该为arr+left
//前后指针法(默认三数取中选key优化)+小区间优化
void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	if (right - left + 1 <= 10)
	{
		InsertSort(arr + left, right - left + 1);
		return;
	}
	int begin = left;
	int end = right;
	int midi = GetMidNumi(arr, left, right);
	Swap(arr + midi, arr + left);
	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	{
		if (arr[cur] < arr[keyi])
		{
			prev++;
			Swap(arr + prev, arr + cur);
		}
		cur++;
	}
	Swap(arr + prev, arr + keyi);
	keyi = prev;
	QuickSort(arr, begin, keyi - 1);
	QuickSort(arr, keyi + 1, end);
}

快速排序的非递归实现

递归改成非递归有两种方式:1. 直接改成循环。2. 使用栈辅助改循环
在这里插入图片描述

  1. 快排在实际应用当中绝对会有问题,只要是递归,就会有问题。
  2. 递归的真正问题在于:当深度太深的时候,递归是撑不住的。有时候程序本身是没有任何错误与问题的。实际上递归写着好着呢,但是程序就是跑不出来。这时候其实就是一个栈溢出的问题。
  3. 因为递归是有消耗的,这个消耗就是建立函数栈帧。当栈的空间不够你继续去建立函数栈帧,完了栈溢出了。
  4. 递归真正的问题有两个:效率慢一些(有是有,但现阶段影响不大),深度太深会栈溢出。但是release环境下可能会有一定的优化,包括函数栈帧可能变小了等等,release优化非常明显。但是在debug环境下早就撑不住了,但在debug下面不能跑也是个问题啊,我不能调试了啊。
  5. 然后对于快速排序的递归方法进行看待的话,你会发现在一次又一次的递归当中,首先指向数组首元素的那个指针肯定是雷打不动的,当然,数组里面的元素都在不断的发生变化。递归的话主要是那个待处理数组区域的左右区间在不断的发生变化。这一点非常关键。数组的话就这么一个就这么放在内存里面,然后数组里面的那些数字都在不断的发生交换与变化,再进行这么的一个排序。然后递归递归,你仔细去看一看这个递归函数里面记录的到底真正是什么?你会发现就是左右区间。
  6. 然后我现在自己手搓一个栈进行辅助(我不用递归了)。因为我分析出来我已经知道这个递归函数他真正实际上是在记录左右区间,那我就用我自己的一个栈用来记录左右区间。
  7. 当然,就单趟排序而言,与递归函数的单趟排序没有任何的区别。只不过就是说在单趟排序之后,把接下来需要继续处理的数组区间的左右边界(肯定是有左边和右边两组)给他压到栈里面去。
  8. 子区间入栈。根据栈这个数据结构的特性,先进后出,后进先出。然后等会儿要进行下一轮的单趟排序的时候,那我该怎么知道我需要处理哪个数组区域的数据呢?这时候就把栈顶的元素出栈便能够获取到一对左右区间。每次的过程就是:从栈里面取一段区间,单趟排序,将单趟排序后分割的子区间入栈,如果说子区间只有一个值或者不存在就不入栈
  9. 栈的意义就是把区间存起来。
  10. 以前的话单趟排序完成之后就对两个子区间进行分治递归,但现在已经说了分治递归的话,有个潜在的问题就在于可能会发生栈溢出的情况。因此现在当把单趟排序完成之后,就把两个新的子区间给他入栈。然后对于两个新的子区间在入栈之前先进行判断一下,就是说判断这个区间到底值不值得入栈,如果区间不存在或者只有一个值,这段区间我才不要继续对你单趟排序
  11. 虽然说整个过程并不是递归,但是会发现他好像也与递归差不多,因为递归本质上不也在这么干嘛。只是现在不用递归去玩,只是把数据存到栈这个数据结构里面。
  12. 以后但凡就是说递归要改成非递归的话。你就去看一下这个递归函数它的参数到底是什么?参数条件变化的是什么?那么这样的话,栈里面基本上存的就是什么(如果用栈辅助的话)
#include "Stack.h"
void QuickSortNonR(int* arr, int left, int right)
{
	ST stack;
	STInit(&stack);
	STPush(&stack, right);
	STPush(&stack, left);
	while (!STEmpty(&stack))
	{
		int begin = STTop(&stack);
		STPop(&stack);
		int end = STTop(&stack);
		STPop(&stack);
		if (begin >= end)
		{
			continue;
		}
		int midi = GetMidNumi(arr, begin, end);
		Swap(arr + midi, arr + begin);
		int keyi = begin;
		int prev = begin;
		int cur = prev + 1;
		while (cur <= end)
		{
			if (arr[cur] < arr[keyi])
			{
				prev++;
				Swap(arr + prev, arr + cur);
			}
			cur++;
		}
		Swap(arr + keyi, arr + prev);
		keyi = prev;
		STPush(&stack, end);
		STPush(&stack, keyi + 1);
		STPush(&stack, keyi - 1);
		STPush(&stack, begin);
	}
	STDestroy(&stack);
}

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

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

相关文章

Android创建项目

目录 创建Android项目 配置项目结构 创建安卓模拟器 模拟器运行 HelloWorld 应用 真机运行 HelloWorld 应用 创建Android项目 打开 Android studio 工具&#xff0c;选择Project&#xff0c;选择 New Project 由于现在是教程博客&#xff0c;所以我们随便选择 一个 空 Ac…

JS内置对象2

数组对象&#xff1a; &#xff08;1&#xff09;检测是否为数组&#xff1a; …

【数据结构】第十站:堆与堆排序

目录 一、二叉树的顺序结构 二、堆的概念及结构 三、堆的实现 1.堆的创建 2.堆的各接口实现 四、堆排序 1.堆排序的基本思想 2.堆排序的实现 3.堆排序时间复杂度 四、TOP-K问题 五、堆的完整代码 一、二叉树的顺序结构 二叉树有顺序结构和链式结构&#xff0c;分别使…

Android之adb安装busybox使用wget、telnet等服务

一、adb里面安装busybox BusyBox 是一个集成了三百多个最常用Linux命令和工具的软件。BusyBox 包含了一些简单的工具&#xff0c;例如ls、cat和echo等等&#xff0c;还包含了一些更大、更复杂的工具&#xff0c;例grep、find、mount以及telnet。 1、下载busybox busybox—bi…

有哪些通过PMP认证考试的心得值得分享?

回顾这100多天来艰辛的备考经历&#xff0c;感慨颇多 一&#xff0c;对于pmp的认知 百度百科&#xff1a;PMP&#xff08;Project Management Professional&#xff09;指项目管理专业人士&#xff08;人事&#xff09;资格认证。美国项目管理协会&#xff08;PMI&#xff09;举…

使用 Floyd Warshall 检测负循环

我们得到了一个有向图。我们需要计算图形是否有负循环。负循环是循环的总和为负的循环。 在图形的各种应用中都可以找到负权重。例如,如果我们沿着这条路走,我们可能会得到一些好处,而不是为一条路付出代价。 例子:

PVE相关的各种一键脚本(一键安装PVE)(一键开设KVM虚拟化的NAT服务器-自带内外网端口转发)

PVE 原始仓库&#xff1a;https://github.com/spiritLHLS/pve 前言 建议debian在使用前尽量使用最新的系统 非debian11可使用 debian一键升级 来升级系统 当然不使用最新的debian系统也没问题&#xff0c;只不过得不到官方支持 请确保使用前机器可以重装系统&#xff0c;…

RK3568平台开发系列讲解(驱动基础篇)自动创建设备节点

🚀返回专栏总目录 文章目录 一、自动创建设备节点1.1、创建和删除类函数1.2、创建设备函数二、创建类函数三、创建设备函数沉淀、分享、成长,让自己和他人都能有所收获!😄 📢自动创建设备节点分为两个步骤: 步骤一:使用 class_create 函数创建一个类。步骤二:使用 d…

C++算法初级7——二分查找

C算法初级7——二分查找 文章目录C算法初级7——二分查找在升序的数组上进行二分查找总结应用范围应用二分查找的原理&#xff1a;每次排除掉一半答案&#xff0c;使可能的答案区间快速缩小。 二分查找的时间复杂度&#xff1a;O(log n)&#xff0c;因为每次询问会使可行区间的…

【MyBatis Plus】001 -- MyBatis-Plus快速入门(介绍、QuickStart)

目录 1、了解MyBatis-Plus 1.1 MyBatis-Plus介绍 1.2 代码及文档 1.3 特性 1.4 架构 1.5 作者 2、快速开始 2.1 创建数据库以及表 2.2 创建工程 2.3 MyBatis MP 2.3.1 创建子module 2.3.2 MyBatis实现查询User&#xff08;无Service方法&#xff0c;直接通过Mapper实现查询&am…

海外虚拟主机空间:如何使用CDN加速提升用户体验?

随着互联网的迅速发展和全球化的趋势&#xff0c;越来越多的企业和个人选择海外虚拟主机空间。然而&#xff0c;由于服务器的地理位置和网络延迟等原因&#xff0c;这些网站在国内访问时可能会遇到较慢的加载速度和不稳定的用户体验。为了解决这一问题&#xff0c;使用CDN加速是…

Web漏洞-文件包含漏洞超详细全解(附实例)

目录 一、导图 二、文件包含漏洞 1.脚本代码 2.原理演示 3.漏洞成因 4.检测方法 5.类型分类 三、本地文件包含漏洞的利用 <无限制本地文件包含> <有限制本地文件包含> 四、远程文件包含漏洞的利用 <无限制远程文件包含> <有限制远程文件包含…

开心档之C++ 多线程

C 多线程 目录 C 多线程 创建线程 终止线程 实例 实例 实例 向线程传递参数 实例 连接和分离线程 实例 std::thread 实例 多线程是多任务处理的一种特殊形式&#xff0c;多任务处理允许让电脑同时运行两个或两个以上的程序。一般情况下&#xff0c;两种类型的多任务…

NumPy 初学者指南中文第三版:11~14

原文&#xff1a;NumPy: Beginner’s Guide - Third Edition 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 十一、玩转 Pygame 本章适用于希望使用 NumPy 和 Pygame 快速轻松创建游戏的开发人员。 基本的游戏开发经验会有所帮助&#xff0c;但这不是必需的。 您将学…

LinuxGUI自动化测试框架搭建(八)-安装LinuxGUI自动化测试工具Dogtail

(八)-安装LinuxGUI自动化测试工具Dogtail 1 Dogtail简介2 Dogtail技术原理3 Dogtail安装4 Dogtail的sniff组件1 Dogtail简介 官网:Dogtail官网文档; Linux平台能够支持Accessibility去获取元素控件的工具,主要有Dogtail和LDTP两个工具;dogtail 用 Python 编写,是python …

SpingBoot——SB整合MB的web项目模板

这里是我以后用到的项目都要先创建的模板 第一步 新建一个springboot项目&#xff0c;这里jdk版本和java版本根据需求选择 第二步 ——选择springboot版本和他提供的可以选择安装的依赖 这里因为是开发web项目&#xff0c;所以选择一个spring web 同时因为还要用到mysql&am…

分享4个不可或缺的 VSCode 插件,让 Tailwind CSS开发更简单

本文将为大家分享我在使用 Tailwind 进行开发时常用的四个 VSCode 扩展程序&#xff0c;这些扩展程序都包含在 VSCode 的 TailwindCSS Kit 扩展程序包中。1.Tailwind CSS IntelliSenseTailwind CSS IntelliSense 是一款功能强大的工具&#xff0c;可以帮助开发者更快、更高效地…

python-day1

第001天&#xff1a;初识python 本博客主要涉及到以下几个部分 1、配置镜像源 2、变量名及其命名规范 3、input函数和数据类型 4、指令和程序 5、运算符 6、练习 1、配置镜像源 此处我配置的是豆瓣源&#xff0c;操作步骤如下&#xff1a; 1、进入D:XXX\Scripts文件夹&#xff…

NumPy 初学者指南中文第三版:1~5

原文&#xff1a;NumPy: Beginner’s Guide - Third Edition 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 一、NumPy 快速入门 让我们开始吧。 我们将在不同的操作系统上安装 NumPy 和相关软件&#xff0c;并看一些使用 NumPy 的简单代码。 本章简要介绍了 IPython…

数据结构和算法学习记录——初识二叉树(定义、五种基本形态、几种特殊的二叉树、二叉树的重要性质、初识基本操作函数)

目录 二叉树的定义 二叉树具体的五种基本形态 1.空树 2.只有一个节点 3.有左子树&#xff0c;但右子树为空 4.有右子树&#xff0c;但左子树为空 5.左右两子树都不为空 特殊二叉树 斜二叉树 满二叉树 完全二叉树 二叉树的几个重要性质 初识二叉树的几个操作函数 …