快速排序详解

news2024/10/5 20:18:44

快速排序,简称快排。其实看快速排序的名字就知道它肯定是一个很牛的排序,C语言中的qsort和C++中的sort底层都是快排。

快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用,因此很多软件公司的笔试面试,包括像腾讯,微软等知名IT公司都喜欢考这个,还有大大小的程序方面的考试如软考,考研中也常常出现快速排序的身影。

目录

快排的单趟排序

1. hoare版本

2. 挖坑法

3. 前后指针版本

快排的递归实现

快排的非递归实现

快排的优化

1.随机选数

2.三数取中

3.小区间优化

4.三路并排


快排基本思想

  • 1.先从数列中取出一个数作为基准数。
  • 2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
  • 3.再对左右区间重复第二步,直到各区间只有一个数或者这个区间不存在。  

动图演示:

将区间按照基准值划分为左右两半部分的常见方式有:

1. hoare版本

2. 挖坑法

3. 前后指针版本

单趟排序后的目标:左边的值比key要小,右边的值比key要大。

快排的单趟排序

1. hoare版本

查看源图像

hoare版本时,如果我们选择最左边的值做key,一定要让右边的指针先走,如果我们选择最右边的值做key,一定要让左边的指针先走,这是为了保证左右指针相遇时指向的值比key小,最后把相遇的值和key值交换,达到单趟排序的目的。

左右指针指向的值和key做比较时要加上等于,如果不加等于,当数组里都是同一个数时,会陷入死循环。

代码:  

int Partion(int* a,int left,int right)
{
	int keyi = left;
	while (left < right)
	{
		//右边先走,找小
        //防止特殊情况,保证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]);
	keyi = left;
	return keyi;
}

2. 挖坑法

查看源图像

1.begin =L; end = R; 将key挖出形成第一个坑a[begin]。

2.end--由后向前找比key小的数,找到后挖出此数填前一个坑a[begin]中,此数的位置变成新的坑位。

3.begin++由前向后找比key大的数,找到后也挖出此数填到前一个坑a[end]中,此数的位置变成新的坑位。

4.再重复执行2,3二步,直到begin ==end ,将基准数填入a[begin]中。

代码:

// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int keyi = a[left];
	int hole = left;
	int begin = left;
	int end = right;
	while (begin < end)
	{
		while (begin < end && a[end] >= a[keyi])
			end--;
		a[hole] = a[end];
		hole = end;

		while (begin < end && a[begin] <= keyi)
			begin++;
		a[hole] = a[begin];
		hole = begin;
		
	}
	a[hole] = keyi;
	return hole;
}

3. 前后指针版本

查看源图像

快速排序的前后指针法相比于Hoare版和挖坑版在思路上有点不同,前后指针版的思路是引入两个指针cur和prev(待排序数的下标),在开始的时候先规定一个基准值key(一般为最右边或者最左边的那个数据),然后让两个指针指向key的下一个数(也可以prev指向),开始下面循环: 若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指向的数,然后cur++;如果cur指向的内容大于key,则cur++。 

可以选取最左边的值或者最右边的值做key,但是要注意一些细节性的问题,最左边做key,结束的时候直接将key和prev的值交换,最右边做key,结束的时候需要把prev++,再将key和prev的值交换。

最左边的值做key示意图:

 代码:

// 快速排序前后指针法
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] && a[++prev] != a[cur])
		{
			Swap(&a[prev], &a[cur]);
		}	
		cur++;
		
	}
	Swap(&a[prev], &a[keyi]);
	return prev;
}

快排的递归实现

快排的递归思想类似于二叉树的前序遍历。

代码:

void QuickSort(int* a, int left, int right)
{
    //结束条件:当区间只有一个数或者区间不存在
	if (left >= right)
		return;

    int keyi = PartSort1(a, left, right);
	//int keyi = PartSort2(a, left, right);
    //int keyi = PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

快排的非递归实现

计算机在实现递归时会调用系统的堆栈,这很消耗计算机内存资源,所以采用非递归算法的本质就是手动模拟系统的堆栈调用来降低computer资源的消耗。

基本思路:

1.对原数组进行一次划分,分别将最左边的元素下标和最右边的元素下标入栈 stack。

2.判断 stack 是否为空,若是,直接结束;若不是,将栈顶元素下标取出,进行一次划分。

3.判断左边的元素长度(这里指 right - left + 1)大于 1,将左边的元素下标入栈;同理,右边的元素下标。

4.循环步骤 2、3。

用C语言实现非递归的快排需要自己实现栈,用C++可以直接使用自带的栈。

C语言代码(栈需要自己实现):

void QuickSortRon(int* a, int left, int right)
{
	ST st;
	StackInit(&st);
	StackPush(&st,left);
	StackPush(&st, right);
	while (!StackEmpty(&st))
	{
		int end = StackTop(&st);
		StackPop(&st);
		
		int begin = StackTop(&st);
		StackPop(&st);
        int keyi = Partion1(a, begin, end);
        //int keyi = Partion2(a, begin, end);
		//int keyi = Partion3(a, begin, end);


		//[left, keyi - 1] keyi [keyi + 1, right]
		if (keyi + 1 < end)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, end);
		}
		if (begin < keyi - 1)
		{
			StackPush(&st, begin);
			StackPush(&st, keyi - 1);
		}
		
	}
	
	StackDestroy(&st);
	
}

C++代码:

#include <iostream>
#include <stack>
using namespace std;

void QuickSortRon(int* a, int left, int right)
{
	//手动利用栈来存储每次分块快排的起始点
	//栈非空时循环获取中轴入栈
	stack<int> s;
	if (left < right)
	{
		int keyi = PartSort1(a, left, right);

		if (keyi - 1 > left) //确保左分区存在 
		{
			//将左分区端点入栈 
			s.push(left);
			s.push(keyi - 1);
		}
		if (keyi + 1 < right) //确保右分区存在 
		{
			s.push(keyi + 1);
			s.push(right);
		}

		while (!s.empty())
		{
			//得到某分区的左右边界 
			int end = s.top();
			s.pop();
			int begin = s.top();
			s.pop();

			keyi = PartSort1(a, begin, end);
			if (keyi - 1 > begin) //确保左分区存在 
			{
				//将左分区端点入栈 
				s.push(begin);
				s.push(keyi - 1);
			}
			if (keyi + 1 < end) //确保右分区存在 
			{
				s.push(keyi + 1);
				s.push(end);
			}
		}
	}
}

快排的优化

上述选key值的方法都存在一定的缺陷:

当选到的key值都是中位数,快排效率最好,类似于二分。当选到的key值都是最大或最小值,快排效率会很慢。快排的递归调用会创建函数栈帧,递归层数太多,可能会栈溢出。

1.随机选数

int key = left + rand() % (left + right);

2.三数取中

选取左边,中间,右边,既不是最大,也不是最小的那个数做key。(面对最坏的情况,选中位数做key,变成最好的情况)

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

3.小区间优化

对于递归的快排小区间优化,当分割到小区间时,不再用递归分割的思路给这段子区间排序,直接使用插入排序,减少递归次数,因为最后几层的递归几乎占了递归的绝大部分。

//递归实现快速排序O(N*logN)
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
		return;

	//小区间优化,当分割到小区间时,不再用递归分割的思路让这段子区间有序
	//对于递归快排,减少递归次数
	if (right - left + 1 < 10)//区间可以不确定,保证是比较小的区间就可以了
	{
		InsertSort(arr + left, right - left + 1);
	}
	else
	{
		int keyi = Partion3(arr, left, right);
		//[left,keyi] keyi [keyi+1,right]
		QuickSort(arr, left, keyi - 1);
		QuickSort(arr, keyi + 1, right);
	}
	
}

4.三路并排

快排有一个缺陷:当需要排序的数全部相同时,或者大量数据相同时,性能会下降,快排会非常慢,我们可以采用一些方法优化。

在上述的快排中,我们采用的都是两路并排的方法,即选一个key值,把比key小的值甩到一边,比key大的值甩到一边。为了优化排序的数全部相同或者大部分相同这些情况,我们采用三路并排,即将比key小的值甩到左边,比key大的值甩到右边,和key相等的值放到中间。

1.a[cur]<key 交换left和cur   

2.a[cur]==key ,cur++ ,保证等于key的值在中间

3.a[cur]>key 交换right和cur 

 代码:

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	if ((right - left + 1) < 15)
	{
		//小区间使用直接插入,减少递归次数
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		//三数取中
		int mid = GetMidIndex(a, left, right);
		Swap(&a[left], &a[mid]);

		int begin = left, end = right;
		int key = a[begin];
		int cur = begin + 1;
		while (cur <= end)
		{
			if (a[cur] < key)
			{
				Swap(&a[begin], &a[cur]);
				cur++;
				begin++;
			}
			else if (a[cur] > key)
			{
				Swap(&a[end], &a[cur]);
				end--;
			}
			else  //a[cur]==key
			{
				cur++;
			}
		}
		QuickSort(a, left, end - 1);
		QuickSort(a, end + 1, right);
	}
	
}

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

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

相关文章

Opencv 基本操作五 各种连通域处理方法

在深度学习中&#xff0c;尤其是语义分割模型部署的结果后处理中&#xff0c;离不开各类形态学处理方法&#xff0c;其中以连通域处理为主&#xff1b;同时在一些传统的图像处理算法中&#xff0c;也需要一些形态学、连通域处理方法。为此&#xff0c;整理了一些常用的连通域处…

leetcode每日一题寒假版:1691. 堆叠长方体的最大高度 (hard)( 换了皮的最长递增子序列)

2022-12-10 1691. 堆叠长方体的最大高度 (hard) &#x1f6a9; 学如逆水行舟&#xff0c;不进则退。 —— 《增广贤文》 题目描述&#xff1a; 给你 n 个长方体 cuboids &#xff0c;其中第 i 个长方体的长宽高表示为 cuboids[i] [width(i), length(i), height(i)]&#xf…

Docker补充知识点--自定义网络实现直连容器

前面介绍docker镜像的秘密这篇知识点的时候&#xff0c;https://blog.csdn.net/dudadudadd/article/details/128200522&#xff0c;提到了docker容器也有属于自己的IP的概念&#xff0c;默认的Docker容器是采用的是bridge网络模式。并且提到了一嘴自定义网卡配置&#xff0c;本…

java基于Springboot的健身房课程预约平台-计算机毕业设计

项目介绍 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven 本健身网站系统是针…

Unity纹理优化:缩小包体

Android打包apk大小约&#xff1a;475M 查看打包日志&#xff1a;Console→Open Editor Log; 或者依赖第三方插件&#xff1a;build reports tool&#xff08;在unity store里可以下载&#xff09;&#xff1b; 定位问题 经过排查后&#xff0c;发现项目中纹理占比很高&#…

分布式能源的不确定性——风速测试(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清…

(6)Pytorch数据处理

Pytorch 数据处理 要点总结 1、功能 Dataset&#xff1a;准备数据集&#xff0c;一般会针对自己的数据集格式重写Dataset&#xff0c;定义数据输入输出格式 Dataloader&#xff1a;用于加载数据&#xff0c;通常不用改这部分内容 2、看代码时请关注 Dataloader中collate_fn 传入…

【云原生】K8s Ingress rewrite与TCP四层转发讲解与实战操作

文章目录一、背景二、K8s Ingress安装三、K8s Ingress rewrite 讲解与使用1&#xff09;配置说明2&#xff09;示例演示1、部署应用2、配置ingress rewrite转发&#xff08;http&#xff09;3、配置ingress rewrite转发&#xff08;https&#xff09;【1】创建证书&#xff08;…

音视频- iOS图像采集

本文主要总结一下&#xff0c;如何使用AVFoundation的功能来实现图像的采集&#xff0c;主要用到了AVFoundation中的一些类&#xff0c;采集的结构如下图&#xff0c;引用自iOS开发者官网&#xff1a; AVCaptureSession 采集会话&#xff0c;其主要功能从整体上来掌管图像采集的…

MOSFET 和 IGBT 栅极驱动器电路的基本原理学习笔记(五)交流耦合栅极驱动电路

交流耦合栅极驱动电路 1.计算耦合电容 2.耦合电容器的启动瞬变 3.总结 栅极驱动路径中的交流耦合可为栅极驱动信号提供简单的电平位移。交流耦合的主要作用是修改主MOSFET 的开通和关断栅极电压&#xff0c;而高侧栅极驱动则不同&#xff0c;它最需要关注的是缩小较大的电势差…

软件安全测试-web安全测试基础

目录 1. Web安全的测试范围 2.Web安全的四要素 3. Web安全的分类 4. Web安全的类别排名​ 5. 零时差攻击 6. Web安全的载体 7. 了解软件安全测试相关的Cooike&#xff0c;Session&#xff0c;Token 7.1 会话级鉴权及认证技术 7.2 会话安全管理需要授权和鉴权两个步骤 …

单例模式(史上最全)

文章很长&#xff0c;而且持续更新&#xff0c;建议收藏起来&#xff0c;慢慢读&#xff01;疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 &#xff1a; 免费赠送 :《尼恩Java面试宝典》 持续更新 史上最全 面试必备 2000页 面试必备 大厂必备 涨薪必备 免费赠送 经典…

0121 动态规划 Day10

剑指 Offer 46. 把数字翻译成字符串 给定一个数字&#xff0c;我们按照如下规则把它翻译为字符串&#xff1a;0 翻译成 “a” &#xff0c;1 翻译成 “b”&#xff0c;……&#xff0c;11 翻译成 “l”&#xff0c;……&#xff0c;25 翻译成 “z”。一个数字可能有多个翻译。…

Python——翻转字符串

题目介绍 以空格为分割&#xff0c;将字符串中的每个单词的字母位置不变&#xff0c;单词顺序从后往前翻转 例如&#xff1a;I am a student. 变成&#xff1a;student. a am I Python中的标准库是为了提高程序员开发效率&#xff0c;减少学习成本&#xff0c;而设计的一系列方…

spring——Spring Bean定义

在 XML 配置的<beans> 元素中可以包含多个属性或子元素&#xff0c;常用的属性或子元素如下表所示。 属性名称描述idBean 的唯一标识符&#xff0c;Spring IoC 容器对 Bean 的配置和管理都通过该属性完成。id 的值必须以字母开始&#xff0c;可以使用字母、数字、下划线等…

SpringCloud Gateway网关的使用与介绍

目录 1. gateway简介 1.1 是什么 1.2 作用 1.3 主要特征 1.4 与zuul的主要区别 1.5 主要组件 1.6 架构图 2. 开发示例 2.1 创建一个gateway模块 2.2 与nacos结合使用 2.2.1 默认规则 2.2.2 通过配置文件配置路由 2.2.3 动态路由 1. gateway简介 1.1 是什么 Spri…

Vulnhub靶机:PRIME_ 1

目录介绍信息收集主机信息探测主机信息探测网站探测目录爆破排雷dirsearch强制访问文件包含漏洞利用WordPress提权wordpress配置文件内核提权介绍 系列&#xff1a;Prime&#xff08;此系列共1台&#xff09; 发布日期&#xff1a;2019年9月1日 难度&#xff1a;初-中 运行环境…

在 Istio 服务网格中使用 Argo Rollouts 实现智能的渐进式发布

1 Argo Rollouts 介绍 Kubernetes 原生的 Deployment 利用 Rolling Update 滚动更新的策略在应用升级时提供基本的安全保证&#xff08;例如就绪探针&#xff09;。然而默认的滚动更新策略存在着一些明显的缺点&#xff0c;例如&#xff1a; 无法控制流向新版本的流量。无法控…

tensorflow入门(四)如何用tensorflow训练神经网络

参考 如何用tensorflow训练神经网络 - 云社区 - 腾讯云 在使用神经网络解决实际的分类或回归问题时需要设置好参数取值。下面介绍使用监督学习的方式来合理地设置参数取值&#xff0c;同时也将给出tensorflow程序来完成这个过程。设置神经网络参数的过程就是神经网络的训练过…

基于JDBC的MySQL数据库编程

✨博客主页: 荣 ✨系列专栏: MySQL ✨一句短话: 难在坚持,贵在坚持,成在坚持! 文章目录一. JDBC概述二. JDBC前置工作1. 准备好MySQL驱动包2. 创建项目三. JDBC的使用步骤1. 创建数据源DataSourece2. 连接数据库3. 构造并执行sql语句4. 释放资源5. sql语句不要写死(以插入为例)…