<数据结构>NO10.快速排序|递归|非递归|优化

news2025/2/2 21:00:39

文章目录

  • 快速排序
    • 递归实现快速排序
      • hoare版本
      • DigHole版本
      • 前后指针版本
    • 非递归实现快速排序
    • 算法优化
      • 1. 针对有序数组进行优化
      • 2. 针对全相等数组进行优化
    • 算法分析
      • 时间复杂度
      • 空间复杂度

快速排序

快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),是一种排序算法。在平均状况下,排序n个项目要 O ( n l o g n ) O(nlogn) O(nlogn)次比较。最坏的情况下需要 O ( n 2 ) O(n^2) O(n2)次比较,但是最坏的情况并不常见。快速排序通常情况下比其他的排序速度更快。

递归实现快速排序

快速排序是一种分治的思想。在待排序项目中取出一个值作为基准值,将基准值通过一趟排序放入合适的位置,**保证基准值经过一趟排序后的左边所有项目都小于该基准值,右边所有项目都大于该基准值。**在对左边重复上述过程,对右边重复上述过程。

因此快速排序的步骤是

  1. 挑选基准值:选取待排序项目中某一个数据作为基准值
  2. 分割:通过合适的方法将基准值放入有序时适当的位置,使得比基准值小的数据放在基准值左边,比基准值大的数据放在基准值右边
  3. 递归子序列:对基准值左边重复上述1.2.对基准值右边重复上述1.2.

基准值通常选取待排序项目中的第一个项目、最后一个项目或者中间的项目

因此我们可以写出快速排序的基本框架

//区间为左闭右闭[begin, end]
void QuickSort(int* arr, int begin, int end)
{
    //结束条件1.区间只有一个元素 2.区间不存在
	if (end <= begin )            
		return;
    
	int keyi = PartSort(arr, begin, end);//这一步是分割,保证了基准值在合适的位置,并且返回基准值的位置
	QuickSort(arr, 0, keyi - 1);//递归处理基准值左边的部分
	QuickSort(arr, keyi + 1, end);//递归处理基准值右边的部分
}

我们下面介绍3种方法完成对基准值的分割。

hoare版本

hoare完成单趟排序基本思路

  1. 选取第一个待排数据为基准值(key)
  2. L从begin开始,R从end开始相向运动
  3. R先向左运动寻找比key小的
  4. L后向右运动寻找比key大的
  5. 交换L和R位置的数值
  6. 重复(3,4)直至L和R相遇
  7. 交换key和相遇位置的值

注意:若选取第一个待排数据为基准值那么一定要R先动才能保证相遇位置的值比key小,若选取最后一个待排数据为基准值,那么一定要L先动才能保证相遇位置的值比key大。
在这里插入图片描述

代码

//hoare法
int PartSort1(int* arr, int begin, int end)
{
	//以begin为keyi(keyi是基准值的下标)
	int keyi = begin;
	int L = begin;
	int R = end;
	while (L < R)//L和R相遇前
	{
		//R寻找小于key的
		while (L < R && arr[R] >= arr[keyi]) R--;
		//L寻找大于key的
		while (L < R && arr[L] <= arr[keyi]) L++;
		//交换L和R位置的值
		Swap(&arr[R], &arr[L]);
	}
	//交换key与相遇位置的值
	Swap(&arr[keyi], &arr[L]);
	return L;
}

运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BWAPI2s4-1689562847800)(C:\Users\石阳\AppData\Roaming\Typora\typora-user-images\image-20230712215303795.png)]

hoare版本的注意点比较多,一不留神容易死循环或者越界

注意

  1. L只能从begin开始,不能从begin后面一个位置开始,否则出现下面这种情况

  2. R寻找小于key的循环条件是arr[keyi] <= arr[R] && L < R第一个条件中的等号不可以省略,第二条件不可以省略。
    如果第一个条件中的省略号省略了,可能会死循环例如

    如果第二个条件省略了可能会造成越界例如

hoare版本最后一个需要注意如果我们每次将key值锁定为区间第一个元素,那么我们一定要先移动R,后移动L,这样一定可以保证L与R相遇处的值小于等于key。

**解释:**相遇时只有2种情况,R遇见L和L遇见R
无论L和动还是R先动,相遇前最后的状态一定是这样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vlqb8Mvt-1689562622854)(C:\Users\石阳\AppData\Roaming\Typora\typora-user-images\image-20230712132400678.png)]

下面是2中R遇见L和L遇见R两种情况:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IA3sVEND-1689562622855)(C:\Users\石阳\AppData\Roaming\Typora\typora-user-images\image-20230712153642206.png)]

hoare版本的细节非常的多,一不注意就会踩坑,有人就提出了更不容易写错的版本,挖坑法~

DigHole版本

挖坑法处理单趟排序的主要思路

  1. 将第一个数据放在key中,形成临时坑位
  2. R先向左移动直至找到小于key的值,并将该值填入坑中,R所处位置变成新坑
  3. L向右移动直至找到大于key的值,并将该值填入坑中,L所处的位置变成新坑
  4. 重复(2.3)直至LR相遇,将key填入相遇位置的坑

代码

//挖坑法
int PartSort2(int* arr, int left, int right)
{
	int key = arr[left];//保留首个数据到key中
	int hole = left;//第一个坑为第一个数据
	int L = left;
	int R = right;
	while (L < R)//循环条件为LR为相遇
	{
		//R找小与key的
		while (R > L && arr[R] >= key) R--;
		arr[hole] = arr[R];//将R位置的值填入坑中
		hole = R;//坑变为R所处的位置
		
		//L找大于key的
		while (R > L && arr[L] <= key) L++;
		arr[hole] = arr[L];//将L位置的值填入坑中
		hole = L;//坑变为L所处的位置
	}
	arr[R] = key;//将key填入相遇处的坑
	return R;//返回相遇位置
}

运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-43icCBTu-1689562622855)(C:\Users\石阳\AppData\Roaming\Typora\typora-user-images\image-20230712215450021.png)]

hoare版本和DigHole版本都比较繁琐,循环条件稍不留神就会出错,因此有另一种简介的方法完成单趟排序—双指针

前后指针版本

双指针法完成单趟排序基本思路

  1. 选择待排数据首元素为基准值key
  2. 定义cur指针指向待排数据的当前元素;pre指针指向cur指针后面一个元素(最开始保证cur与pre相邻)
  3. 若cur指针指向的值大于key,cur++保证cur和pre之间的元素全部大于基准值
  4. 若cur指针指向的值不大于key,pre++并且交换pre和cur所指的值
  5. 重复(3.4)直至cur超过右边界
  6. 交换pre与keyi所指的值,返回pre

实际上3.4)步保证了大于key的值在最右边,小于key的值在最左边

代码

int PartSort3(int* arr, int left, int right)
{
	int prev = left;						//prev指向第一个元素
	int cur = prev + 1;						//cur指向prev后面一个元素
	int keyi = left;						//key的值选为待排数据第一个元素
	while (cur <= right)					//循环条件为cur为超过右边界
	{
		if (arr[cur] < arr[keyi])			//cur指向的值小于prev,prev++并且交换与cur所指向的值
		{
				Swap(&arr[++prev], &arr[cur]);//交换++prev和cur所指向的值
		}
		cur++;								//cur向后移动一位
	}
	Swap(&arr[keyi], &arr[prev]);			//交换keyi和prev所指向的值
	return prev;
}

执行结果
在这里插入图片描述

个人认为双指针版本最不容易理解,也是最不容易写错的

非递归实现快速排序

思路:

  1. 将待排数据最后一个元素下标end入栈,再将待排数据首元素下标begin入栈
  2. 先取出栈顶元素为left,再取出栈顶元素为right
  3. 单趟处理[left,right]得到keyi
  4. keyi的右区间存在,重复(1),若keyi的左区间存在,重复(1)。若左右区间不存在则不入栈
  5. 重复(2,3,4)直至栈为空

在这里插入图片描述

代码

//快排非递归
void QuickSortNonR(int* arr, int begin, int end)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, end);
	StackPush(&st, begin);
	while (!StackEmpty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);
		int right = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort1(arr, left, right);
		//如果右区间个数大于1则入栈
		if (keyi + 1 < right)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}
		//如果左区间个数大于1则入栈
		if (keyi - 1 > left)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}
	StackDestroy(&st);
}

运行结果
在这里插入图片描述


算法优化

1. 针对有序数组进行优化

前面说过,当待排数据本身有序时QuickSort的时间复杂度为 O ( n 2 ) O(n^2) O(n2)(即使这种情况很少见)因此我们可以通过更改基准值实现优化的目的。

三数取中优化将基准值取为待排数据中首元素、尾元素和中间元素的中间值,这保证了无论元数据是否有序,基准值都不会是最大值或最小值

代码

int GetMidi(int* arr, int begin, int end)
{
	int midi = (begin + end) / 2;
	if (arr[begin] > arr[end])
	{
		if (arr[midi] > arr[begin])
			return begin;
		else if (arr[end] > arr[midi])
			return end;
		else if (arr[end] < arr[midi])
			return midi;
	}
	else //arr[begin] < arr[end]
	{
		if (arr[midi] < arr[begin])
			return begin;
		else if (arr[end] < arr[midi])
			return end;
		else if (arr[end] > arr[midi])
			return midi;
	}
}

int PartSort3(int* arr, int left, int right)
{
	//保证第一个元素不是最值
	int midi = GetMidi(arr, left, right);
	Swap(&arr[left], &arr[midi]);

	int prev = left;						//prev指向第一个元素
	int cur = prev + 1;						//cur指向prev后面一个元素
	int keyi = left;						//key的值选为待排数据第一个元素
	while (cur <= right)					//循环条件为cur为超过右边界
	{
		if (arr[cur] < arr[keyi])			//cur指向的值小于prev,prev++并且交换与cur所指向的值
		{
			if (cur != prev + 1);			//cur和prev之间有大于key的元素
			{
				Swap(&arr[++prev], &arr[cur]);//交换++prev和cur所指向的值
			}
		}
		cur++;								//cur向后移动一位
	}
	Swap(&arr[keyi], &arr[prev]);			//交换keyi和prev所指向的值
	return prev;
}

void QuickSort(int* arr, int begin, int end)
{
	//结束条件1.区间只有一个元素 2.区间不存在  
	if (end <= begin )          
		return;

	int keyi = PartSort3(arr, begin, end);
	QuickSort(arr, 0, keyi - 1);
	QuickSort(arr, keyi + 1, end);
}

下面是优化后的快排和未优化的快排排100000个随机数效率的差异

在这里插入图片描述

**注意:**如果三数取中仍然过不了可以使用随机数取中,只需要将midi = GetMidi(arr, begin, end)替换成midi = begin + rand() % (end - begin + 1)

2. 针对全相等数组进行优化

当数组元素全部相等时,即使三数取中,Quicksort的时间复杂度仍然是 O ( n 2 ) O(n{2}) O(n2)
在这里插入图片描述

根本原因是因为PartSort得到的keyi只区分了小于等于key和大于等于key的,只保证了小于等于key的在keyi左边,大于等于key的在keyi右边,因此当数组全部相等时,每次得到的keyi都是从0,1,2……n-1,每趟比较的次数就成等差数列。

我们可以使用三路划分来优化这种情况。
三路划分:将待排数据严格分为3部分,左边是小于key的数据,中间是等于key的数据,右边是大于key的数据。每次只递归左边和右边。当数组元素全部相等时,作区间不存在,右区间也不存在,因此递归左右区间一次就会停止排序。针对相等数组的时间复杂度为 O ( n ) O(n) O(n)三路划分的本质是小的甩到左边,大的甩到右边。相等的推到中间
在这里插入图片描述

最后只需要递归区间[begin,l-1],[r + 1,end]即可

代码

//快排递归
void QuickSort(int* arr, int begin, int end)
{
	//结束条件1.区间只有一个元素 2.区间不存在  
	if (end <= begin )          
		return;
   // int midi = GetMidi(arr, begin, end);
    int midi = begin + rand() % (end - begin + 1);//三数取中过不去使用随机数取中
    Swap(&arr[begin], &arr[midi]);
    //三路划分
	int l = begin, c = begin + 1, r = end;
    int key = arr[begin];
    while (c <= r)
    {
        if (arr[c] > key)
        {
            Swap(&arr[c], &arr[r]);
            r--;
        }
        else if (arr[c] < key)
        {
            Swap(&arr[c], &arr[l]);
            l++,c++;
        }
        else c++;
    }
    //三路划分
	QuickSort(arr, begin, l - 1);
	QuickSort(arr, r + 1, end);
}

运行结果
在这里插入图片描述


算法分析

时间复杂度

快速排序最好时间复杂度最坏时间按复杂度平均时间复杂度
递归快排(未优化) O ( n l o g n ) O(nlogn) O(nlogn) O ( n 2 ) O(n^{2}) O(n2) O ( n l o g n ) O(nlogn) O(nlogn)
递归快排(优化) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn)
非递归快排(未优化) O ( n l o g n ) O(nlogn) O(nlogn) O ( n 2 ) O(n^{2}) O(n2) O ( n l o g n ) O(nlogn) O(nlogn)
非递归快排(优化) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn)

对于单趟排序,不管哪一种方法实现一趟排序的时间复杂度都是 O ( n ) O(n) O(n)

当待排数据本身有序时,如果没有优化,那么时间复杂度为 O ( n 2 ) O(n^{2}) O(n2),因为每趟的比较次数为等差数列
在这里插入图片描述

空间复杂度

快速排序空间复杂度
递归快速排序(未优化)最好 O ( l o g n ) O(logn) O(logn);最坏 O ( n ) O(n) O(n)
递归快速排序(优化) O ( l o g n ) O(logn) O(logn)
非递归快速排序 O ( 1 ) O(1) O(1)

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

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

相关文章

0基础学习VR全景平台篇 第64篇:高级功能-自定义LOGO和密码访问

一、功能说明 VR视频的高级功能目前有两项&#xff0c;分别是自定义LOGO和密码访问。 二、后台编辑界面 1、自定义LOGO&#xff1a;支持JPG、PNG、GIF格式的图片&#xff0c;大小不超过5M&#xff0c;建议高度不超过500px&#xff0c;设置后显示在VR视频的左上角位置。 2、密…

Vue学习随堂记录

计算属性和监听器 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </he…

MobPush:Android SDK 集成指南

开发工具&#xff1a;Android Studio 集成方式&#xff1a;Gradle在线集成 安卓版本支持&#xff1a;minSdkVersion 19 集成准备 注册账号 使用PushSDK之前&#xff0c;需要先在MobTech官网注册开发者账号&#xff0c;并获取MobTech提供的AppKey和AppSecret&#xff0c;详情可…

《程序是怎样跑起来的》简介

目录 1. 前言2. 主要内容3. 总结 1. 前言 闲暇之余&#xff0c;读了一遍《程序是怎样跑起来的》这本书。颇感欣喜。借此机会分享一下。 本书可以这样定位&#xff1a; 对学生&#xff1a;作为专业课之前的开胃菜&#xff0c;非常合适&#xff0c;尤其是作为《计算机组成原理…

华为OD机试真题 Java 实现【最少数量线段覆盖】【2023Q1 200分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路四、Java算法源码五、效果展示1、输入2、输出3、说明4、复杂一点5、理性分析一下 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》。 刷的越多&#xff…

FLStudio21中文版水果软件最新版下载安装图文教程

FL Studio21简称FL&#xff0c;全称&#xff1a;Fruity Loops Studio&#xff0c;因此国人习惯叫它"水果"。目前版本是FL Studio20&#xff0c;它让你的计算机就像是全功能的录音室&#xff0c;大混音盘&#xff0c;非常先进的制作工具&#xff0c;让你的音乐突破想象…

数据结构--图定义与基本术语

数据结构–图定义与基本术语 图的定义 图G由 顶点集 V \color{red}顶点集V 顶点集V和 边集 E \color{red}边集E 边集E组成&#xff0c;记为G (V, E)&#xff0c;其中V(G)表示图G中顶点的有限非空集&#xff1b; E(G)表示图G中顶点之间的关系&#xff08;边&#xff09;集合。…

现代化 Android 开发:Jetpack Compose 最佳实践

作者&#xff1a;古哥E下 如果一直关注 Compose 的发展的话&#xff0c;可以明显感受到 2022 年和 2023 年的 Compose 使用讨论的声音已经完全不一样了, 2022 年还多是观望&#xff0c;2023 年就有很多团队开始采纳 Compose 来进行开发了。不过也有很多同学接触了下 Compose&am…

22.JavaWeb-Minio存储服务器

MinIO是一个开源的对象存储服务器&#xff0c;它兼容Amazon S3 API。它提供了一个简单而强大的存储解决方案&#xff0c;可以用于存储和检索任意大小的文件对象&#xff0c;如图片、视频、文档等。 1.安装与配置Minio https://dl.min.io/server/minio/release/windows-amd64/…

Leetcode 1352: 最后K个数的乘积

题目描述 链接&#xff1a;https://leetcode.cn/problems/product-of-the-last-k-numbers/ 结果 耗时&#xff1a;12min-13min 思路 暴力法&#xff0c;直接从后面读取数组计算。 Java代码 import java.util.ArrayList;class ProductOfNumbers {ArrayList<Integer…

4个简单易上手的免费抠图工具,让你轻松在线抠图!

本文介绍了四个简单易上手的免费抠图工具&#xff0c;它们分别是记灵在线工具、Remove、FocoClipping和免费抠图工具。无论你是初学者还是有经验的设计师&#xff0c;这些工具都能帮助你快速、高效地进行在线抠图操作。 在现代设计和摄影中&#xff0c;抠图是一项重要且常见的…

新建一个Vue项目后,如何在vue.config,js中配置后端访问地址

在 Vue 2 项目中&#xff0c;可以通过配置 vue.config.js 文件来设置后端访问地址。下面是一个简单的示例&#xff1a; 在项目根目录下新建 vue.config.js 文件&#xff08;如果已存在&#xff0c;则直接编辑该文件&#xff09;。在 vue.config.js 文件中添加以下内容&#xf…

ClickHouse原理剖析

1.ClickHouse简介 ClickHouse是一款开源的面向联机分析处理的列式数据库&#xff0c;其独立于Hadoop大数据体系&#xff0c;最核心的特点是极致压缩率和极速查询性能。同时&#xff0c;ClickHouse支持SQL查询&#xff0c;且查询性能好&#xff0c;特别是基于大宽表的聚合分析查…

yolo系列学习

文章目录 理论基础YOLO-V1YOLO-V2YOLOV3 教学视频 理论基础 不同阶段算法优缺点分析 two-stage (两阶段) &#xff1a;Faster-rcnn、Mask-Rcnn &#xff0c;多了预选框操作RPNOne-stage (单阶段)&#xff1a;YOLO 指标分析 精度 Precision 查准率&#xff0c;预测为正且实际…

亚马逊、lazada店铺销售策略揭秘:如何利用测评自养号突破瓶颈?

在跨境平台上&#xff0c;想要取得突破性的销售成绩并不容易。随着竞争的日益激烈&#xff0c;商家们需要采取有效的销售策略来突破销售瓶颈。本文将揭示三种结合测评自养号的销售策略&#xff0c;帮助卖家在跨境平台上取得更好的销售业绩。 一、建立完善的自养号评价体系 自…

git rebase (合并代码和整理提交记录)图文详解

git rebase详解&#xff0c;附带操作过程命令&#xff0c;运行图片 合并代码初始代码分支结构merge合并代码rebase合并代码 整理提交记录背景-整理提交记录步骤-图文详解 建议在看这篇文章之前一定要看完&#xff1a;git reset 命令详解 git revert命令详解。 看完上面的文章后…

基于scrcpy的Android群控项目重构,获取Android屏幕元素信息并编写自动化事件

系列文章目录 基于scrcpy的远程调试方案 基于scrcpy的Android群控项目重构 基于scrcpy的Android群控项目重构 进阶版 基于scrcpy的Android群控项目重构&#xff0c;获取Android屏幕元素信息并编写自动化事件&#xff08;视频&#xff09; 基于scrcpy的Android群控项目重构…

struct详解

导入 我们有没有这种情况&#xff0c;总想有一个数组&#xff0c;其中可以有int,double,char。。。各种类型&#xff0c;但是对于内置的数据类型显然是做不到的&#xff0c;于是就有了结构体类型 结构体是将多种不同的结构打包在一起&#xff0c;形成全新的类型进行使用 stru…

Spring的两种事务管理机制,面试这样答当场入职!

前言&#xff1a; 博主在最近的几次面试中&#xff0c;大中小厂都问到了Spring的事务相关问题&#xff0c;这块知识确实是面试中的重点内容&#xff0c;因此结合所看的书籍&#xff0c;在这篇文章中总结下。该专栏比较适合刚入坑Java的小白以及准备秋招的大佬阅读&#xff0c;感…

刻录到光盘功能看不见怎么办

刻录到光盘功能看不见怎么办 1、 打开组策略 同时按键WINR&#xff0c;打开运行对话框&#xff0c;输入gpedit.msc&#xff0c;打开组策略&#xff08;如果发现输入gpedit.msc后无法打开组策略&#xff0c;请参照文件后面的方法进行操作&#xff09; 2 、查找“删除CD刻录…